diff --git a/TombIDE/TombIDE.ProjectMaster/Forms/FormImportLevel.cs b/TombIDE/TombIDE.ProjectMaster/Forms/FormImportLevel.cs index c3c3429bd..fccef50fc 100644 --- a/TombIDE/TombIDE.ProjectMaster/Forms/FormImportLevel.cs +++ b/TombIDE/TombIDE.ProjectMaster/Forms/FormImportLevel.cs @@ -1,6 +1,4 @@ -#nullable disable // For now - -using DarkUI.Controls; +using DarkUI.Controls; using DarkUI.Forms; using System; using System.Collections.Generic; @@ -8,55 +6,80 @@ using System.IO; using System.Linq; using System.Windows.Forms; +using TombIDE.ProjectMaster.Services.Level.Import; +using TombIDE.Shared; using TombIDE.Shared.NewStructure; -using TombIDE.Shared.NewStructure.Implementations; using TombIDE.Shared.SharedClasses; -using TombLib.LevelData; namespace TombIDE.ProjectMaster { - public partial class FormImportLevel : DarkForm + public partial class FormImportLevel : DarkForm, IProgressReportingForm { - public ILevelProject ImportedLevel { get; private set; } - public List GeneratedScriptLines { get; private set; } + public ILevelProject? ImportedLevel { get; private set; } + public ScriptGenerationResult? GeneratedScript { get; private set; } - private IGameProject _targetProject; + private readonly IGameProject _targetProject; + private readonly ILevelImportService _importService; + private readonly string _sourcePrj2FilePath; #region Initialization - public FormImportLevel(IGameProject targetProject, string prj2FilePath) + public FormImportLevel(IGameProject targetProject, string prj2FilePath, ILevelImportService importService) { _targetProject = targetProject; + _importService = importService; + _sourcePrj2FilePath = prj2FilePath; InitializeComponent(); - // Setup some information - textBox_Prj2Path.BackColor = Color.FromArgb(48, 48, 48); // Mark as uneditable - textBox_Prj2Path.Text = prj2FilePath; - textBox_Prj2Path.Tag = prj2FilePath; // Keep the full path in the Tag (because we are visually changing the text later) + ConfigureUI(); + } - textBox_LevelName.Text = Path.GetFileNameWithoutExtension(prj2FilePath); + private void ConfigureUI() + { + // Setup path display + textBox_Prj2Path.BackColor = Color.FromArgb(48, 48, 48); + textBox_Prj2Path.Text = _sourcePrj2FilePath; - if (targetProject.GameVersion is TRVersion.Game.TR1 or TRVersion.Game.TR2X or TRVersion.Game.TombEngine) + textBox_LevelName.Text = Path.GetFileNameWithoutExtension(_sourcePrj2FilePath); + + // Configure script generation visibility + if (!GameVersionHelper.IsScriptGenerationSupported(_targetProject)) { checkBox_GenerateSection.Checked = checkBox_GenerateSection.Visible = false; panel_ScriptSettings.Visible = false; panel_04.Visible = false; } - else if (targetProject.GameVersion is not TRVersion.Game.TR4 and not TRVersion.Game.TRNG) + else if (!GameVersionHelper.IsHorizonSettingAvailable(_targetProject)) { checkBox_EnableHorizon.Visible = false; panel_ScriptSettings.Height -= 30; } - if (_targetProject.GameVersion == TRVersion.Game.TR2) - numeric_SoundID.Value = 33; - else if (_targetProject.GameVersion == TRVersion.Game.TR3) - numeric_SoundID.Value = 28; + // Set default ambient sound + int defaultSoundId = GameVersionHelper.GetDefaultAmbientSoundId(_targetProject); + + if (defaultSoundId > 0) + numeric_SoundID.Value = defaultSoundId; } #endregion Initialization + #region ILevelImportProgress + + void IProgressReportingForm.SetTotalProgress(int total) + { + progressBar.Visible = true; + progressBar.BringToFront(); + progressBar.Maximum = total; + progressBar.Value = 0; + } + + void IProgressReportingForm.IncrementProgress(int value) + => progressBar.Increment(value); + + #endregion ILevelImportProgress + #region Level importing methods private void button_Import_Click(object sender, EventArgs e) @@ -65,171 +88,89 @@ private void button_Import_Click(object sender, EventArgs e) try { - string levelName = PathHelper.RemoveIllegalPathSymbols(textBox_LevelName.Text.Trim()); - levelName = LevelHandling.RemoveIllegalNameSymbols(levelName); + var importMode = GetSelectedImportMode(); + bool shouldUpdateSettings = false; - if (string.IsNullOrWhiteSpace(levelName)) - throw new ArgumentException("You must enter a valid name for the level."); + // For KeepInPlace mode, ask the user if they want to update settings + if (importMode == LevelImportMode.KeepInPlace) + { + DialogResult dialogResult = DarkMessageBox.Show(this, + "Do you want to update the \"Game\" settings of all the .prj2 files in the\n" + + "specified folder to match the project settings?", + "Update settings?", MessageBoxButtons.YesNo, MessageBoxIcon.Question); - if (radioButton_SelectedCopy.Checked && treeView.SelectedNodes.Count == 0) - throw new ArgumentException("You must select which .prj2 files you want to import."); + shouldUpdateSettings = dialogResult == DialogResult.Yes; + } - string dataFileName = textBox_CustomFileName.Text.Trim(); + var options = new LevelImportOptions + { + LevelName = textBox_LevelName.Text, + SourcePrj2FilePath = _sourcePrj2FilePath, + DataFileName = textBox_CustomFileName.Text, + ImportMode = importMode, + SelectedFilePaths = GetSelectedFilePaths(), + GenerateScript = checkBox_GenerateSection.Checked, + AmbientSoundId = (int)numeric_SoundID.Value, + EnableHorizon = checkBox_EnableHorizon.Checked, + UpdatePrj2SettingsForExternalLevel = shouldUpdateSettings + }; - if (string.IsNullOrWhiteSpace(dataFileName)) - throw new ArgumentException("You must specify the custom DATA file name."); + LevelImportResult result = _importService.ImportLevel(_targetProject, options, this); - if (radioButton_SpecifiedCopy.Checked || radioButton_SelectedCopy.Checked) - ImportAndCopyFiles(levelName, dataFileName); - else if (radioButton_FolderKeep.Checked) - ImportButKeepFiles(levelName, dataFileName); + ImportedLevel = result.ImportedLevel; + GeneratedScript = result.GeneratedScript; } catch (Exception ex) { DarkMessageBox.Show(this, ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); button_Import.Enabled = true; - DialogResult = DialogResult.None; } } - private void ImportAndCopyFiles(string levelName, string dataFileName) + private LevelImportMode GetSelectedImportMode() { - string fullSpecifiedPrj2FilePath = textBox_Prj2Path.Tag.ToString(); - - string levelFolderPath = Path.Combine(_targetProject.LevelsDirectoryPath, levelName); // A path inside the project's /Levels/ folder - string specificFileName = Path.GetFileName(fullSpecifiedPrj2FilePath); // The user-specified file name - - // Create the level folder - if (!Directory.Exists(levelFolderPath)) - Directory.CreateDirectory(levelFolderPath); - - if (Directory.EnumerateFileSystemEntries(levelFolderPath).ToArray().Length > 0) // 99% this will never accidentally happen - throw new ArgumentException("A folder with the same name as the \"Level name\" already exists in\n" + - "the project's /Levels/ folder and it's not empty."); - if (radioButton_SpecifiedCopy.Checked) - { - // Only copy the specified file into the created level folder - string destPath = Path.Combine(levelFolderPath, specificFileName); - File.Copy(fullSpecifiedPrj2FilePath, destPath); - } + return LevelImportMode.CopySpecifiedFile; else if (radioButton_SelectedCopy.Checked) - { - // Check if the user-specified file was selected on the list, if not, then set the TargetPrj2FileName property to null - bool specificFileSelected = false; - - // Copy all selected files into the created levelFolderPath - foreach (DarkTreeNode node in treeView.SelectedNodes) - { - if (node.Text == specificFileName) - specificFileSelected = true; - - string nodePrj2Path = node.Tag.ToString(); // node.Tag is the full source path of the currently processed file - string destPath = Path.Combine(levelFolderPath, Path.GetFileName(nodePrj2Path)); - File.Copy(nodePrj2Path, destPath); - } - - if (!specificFileSelected) // If the user-specified file was not selected on the list - specificFileName = null; - } - - CreateAndAddLevelToProject(levelName, levelFolderPath, dataFileName, specificFileName); - } - - private void ImportButKeepFiles(string levelName, string dataFileName) - { - string fullSpecifiedPrj2FilePath = textBox_Prj2Path.Tag.ToString(); - - string levelFolderPath = Path.GetDirectoryName(fullSpecifiedPrj2FilePath); - string specificFile = Path.GetFileName(fullSpecifiedPrj2FilePath); - - CreateAndAddLevelToProject(levelName, levelFolderPath, dataFileName, specificFile); - } - - private void CreateAndAddLevelToProject(string levelName, string levelFolderPath, string dataFileName, string specificFileName) - { - // Create the LevelProject instance - var importedLevel = new LevelProject(levelName, levelFolderPath, specificFileName); - - UpdateLevelSettings(importedLevel, dataFileName); - - if (checkBox_GenerateSection.Checked) - { - int ambientSoundID = (int)numeric_SoundID.Value; - bool horizon = checkBox_EnableHorizon.Checked; - - // // // // - GeneratedScriptLines = LevelHandling.GenerateScriptLines(levelName, dataFileName, _targetProject.GameVersion, ambientSoundID, horizon); - // // // // - } - - importedLevel.Save(); - - // // // // - ImportedLevel = importedLevel; - // // // // - } - - private void UpdateLevelSettings(ILevelProject importedLevel, string dataFileName) - { - if (radioButton_SpecifiedCopy.Checked) - { - string specifiedFileName = Path.GetFileName(textBox_Prj2Path.Tag.ToString()); - string internalFilePath = Path.Combine(importedLevel.DirectoryPath, specifiedFileName); - - LevelHandling.UpdatePrj2GameSettings(internalFilePath, _targetProject, dataFileName); - } - else if (radioButton_SelectedCopy.Checked) - { - UpdateAllPrj2FilesInLevelDirectory(importedLevel, dataFileName); - } - else if (radioButton_FolderKeep.Checked) - { - DialogResult result = DarkMessageBox.Show(this, "Do you want to update the \"Game\" settings of all the .prj2 files in the\n" + - "specified folder to match the project settings?", "Update settings?", MessageBoxButtons.YesNo, MessageBoxIcon.Question); - - if (result == DialogResult.Yes) - UpdateAllPrj2FilesInLevelDirectory(importedLevel, dataFileName); - } + return LevelImportMode.CopySelectedFiles; + else + return LevelImportMode.KeepInPlace; } - private void UpdateAllPrj2FilesInLevelDirectory(ILevelProject importedLevel, string dataFileName) + private IReadOnlyList GetSelectedFilePaths() { - string[] files = Directory.GetFiles(importedLevel.DirectoryPath, "*.prj2", SearchOption.TopDirectoryOnly); - - progressBar.Visible = true; - progressBar.BringToFront(); - progressBar.Maximum = files.Length; - - foreach (string file in files) - { - if (!Prj2Helper.IsBackupFile(file)) - LevelHandling.UpdatePrj2GameSettings(file, _targetProject, dataFileName); + if (!radioButton_SelectedCopy.Checked) + return []; - progressBar.Increment(1); - } + return treeView.SelectedNodes + .Cast() + .Select(node => node.Tag?.ToString() ?? string.Empty) + .Where(filePath => !string.IsNullOrEmpty(filePath)) + .ToList(); } #endregion Level importing methods - #region Other level importing events / methods + #region UI Event Handlers private void textBox_LevelName_TextChanged(object sender, EventArgs e) { if (!checkBox_CustomFileName.Checked) - textBox_CustomFileName.Text = textBox_LevelName.Text.Trim().Replace(' ', '_'); + textBox_CustomFileName.Text = LevelNameHelper.SuggestDataFileName(textBox_LevelName.Text); } private void checkBox_CustomFileName_CheckedChanged(object sender, EventArgs e) { if (checkBox_CustomFileName.Checked) + { textBox_CustomFileName.Enabled = true; + } else { textBox_CustomFileName.Enabled = false; - textBox_CustomFileName.Text = textBox_LevelName.Text.Trim().Replace(' ', '_'); + textBox_CustomFileName.Text = LevelNameHelper.SuggestDataFileName(textBox_LevelName.Text); } } @@ -237,7 +178,7 @@ private void textBox_CustomFileName_TextChanged(object sender, EventArgs e) { int cachedCaretPosition = textBox_CustomFileName.SelectionStart; - textBox_CustomFileName.Text = textBox_CustomFileName.Text.Replace(' ', '_'); + textBox_CustomFileName.Text = LevelNameHelper.MakeValidVariableName(textBox_CustomFileName.Text); textBox_CustomFileName.SelectionStart = cachedCaretPosition; } @@ -246,8 +187,9 @@ private void radioButton_SpecifiedCopy_CheckedChanged(object sender, EventArgs e if (!radioButton_SpecifiedCopy.Checked) return; - textBox_Prj2Path.Text = textBox_Prj2Path.Tag.ToString(); // Switch back to the full path - textBox_Prj2Path.BackColor = Color.FromArgb(48, 48, 48); // Reset the BackColor + textBox_Prj2Path.Text = _sourcePrj2FilePath; + textBox_Prj2Path.BackColor = Color.FromArgb(48, 48, 48); + ClearAndDisableTreeView(); } @@ -256,9 +198,12 @@ private void radioButton_SelectedCopy_CheckedChanged(object sender, EventArgs e) if (!radioButton_SelectedCopy.Checked) return; - textBox_Prj2Path.Text = Path.GetDirectoryName(textBox_Prj2Path.Tag.ToString()); // Switch to just the folder path - textBox_Prj2Path.BackColor = Color.FromArgb(64, 80, 96); // Change the BackColor to indicate the change - EnableAndFillTreeView(); + string directoryPath = Path.GetDirectoryName(_sourcePrj2FilePath) ?? string.Empty; + + textBox_Prj2Path.Text = directoryPath; + textBox_Prj2Path.BackColor = Color.FromArgb(64, 80, 96); + + EnableAndFillTreeView(directoryPath); } private void radioButton_SpecificKeep_CheckedChanged(object sender, EventArgs e) @@ -266,8 +211,11 @@ private void radioButton_SpecificKeep_CheckedChanged(object sender, EventArgs e) if (!radioButton_FolderKeep.Checked) return; - textBox_Prj2Path.Text = Path.GetDirectoryName(textBox_Prj2Path.Tag.ToString()); // Switch to just the folder path - textBox_Prj2Path.BackColor = Color.FromArgb(64, 80, 96); // Change the BackColor to indicate the change + string directoryPath = Path.GetDirectoryName(_sourcePrj2FilePath) ?? string.Empty; + + textBox_Prj2Path.Text = directoryPath; + textBox_Prj2Path.BackColor = Color.FromArgb(64, 80, 96); + ClearAndDisableTreeView(); } @@ -294,7 +242,7 @@ private void ClearAndDisableTreeView() button_DeselectAll.Enabled = false; } - private void EnableAndFillTreeView() + private void EnableAndFillTreeView(string directoryPath) { treeView.Enabled = true; button_SelectAll.Enabled = true; @@ -302,11 +250,8 @@ private void EnableAndFillTreeView() treeView.Nodes.Clear(); - foreach (string file in Directory.GetFiles(textBox_Prj2Path.Text, "*.prj2", SearchOption.TopDirectoryOnly)) + foreach (string file in Prj2Helper.GetValidFiles(directoryPath)) { - if (Prj2Helper.IsBackupFile(file)) - continue; - var node = new DarkTreeNode { Text = Path.GetFileName(file), @@ -317,7 +262,7 @@ private void EnableAndFillTreeView() } } - #endregion Other level importing events / methods + #endregion UI Event Handlers #region Script section generating @@ -337,8 +282,8 @@ private void checkBox_GenerateSection_CheckedChanged(object sender, EventArgs e) } } - private void button_OpenAudioFolder_Click(object sender, EventArgs e) => - SharedMethods.OpenInExplorer(Path.Combine(_targetProject.GetEngineRootDirectoryPath(), "audio")); + private void button_OpenAudioFolder_Click(object sender, EventArgs e) + => SharedMethods.OpenInExplorer(Path.Combine(_targetProject.GetEngineRootDirectoryPath(), "audio")); #endregion Script section generating } diff --git a/TombIDE/TombIDE.ProjectMaster/Forms/FormLevelSetup.cs b/TombIDE/TombIDE.ProjectMaster/Forms/FormLevelSetup.cs index 0ffcc55ff..f58acb63d 100644 --- a/TombIDE/TombIDE.ProjectMaster/Forms/FormLevelSetup.cs +++ b/TombIDE/TombIDE.ProjectMaster/Forms/FormLevelSetup.cs @@ -1,54 +1,56 @@ using DarkUI.Forms; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Windows.Forms; +using TombIDE.ProjectMaster.Services.Level.Setup; using TombIDE.Shared.NewStructure; -using TombIDE.Shared.NewStructure.Implementations; using TombIDE.Shared.SharedClasses; -using TombLib; using TombLib.LevelData; -using TombLib.LevelData.IO; -using TombLib.Utils; namespace TombIDE.ProjectMaster { public partial class FormLevelSetup : DarkForm { public ILevelProject? CreatedLevel { get; private set; } - public List GeneratedScriptLines { get; private set; } = []; + public ScriptGenerationResult? GeneratedScript { get; private set; } - private IGameProject _targetProject; + private readonly IGameProject _targetProject; + private readonly ILevelSetupService _levelSetupService; #region Initialization - public FormLevelSetup(IGameProject targetProject) + public FormLevelSetup(IGameProject targetProject, ILevelSetupService levelSetupService) { _targetProject = targetProject; + _levelSetupService = levelSetupService; InitializeComponent(); - if (targetProject.GameVersion is TRVersion.Game.TR1 or TRVersion.Game.TR2X) + ConfigureUIForGameVersion(); + } + + private void ConfigureUIForGameVersion() + { + if (!GameVersionHelper.IsScriptGenerationSupported(_targetProject)) { checkBox_GenerateSection.Checked = checkBox_GenerateSection.Visible = false; panel_ScriptSettings.Visible = false; } - else if (targetProject.GameVersion is not TRVersion.Game.TR4 and not TRVersion.Game.TRNG and not TRVersion.Game.TombEngine) + else if (!GameVersionHelper.IsHorizonSettingAvailable(_targetProject)) { checkBox_EnableHorizon.Visible = false; panel_ScriptSettings.Height -= 35; } - if (targetProject.GameVersion is TRVersion.Game.TombEngine) + if (_targetProject.GameVersion is TRVersion.Game.TombEngine) { checkBox_GenerateSection.Text = "Generate Lua script"; } - if (_targetProject.GameVersion == TRVersion.Game.TR2) - numeric_SoundID.Value = 33; - else if (_targetProject.GameVersion == TRVersion.Game.TR3) - numeric_SoundID.Value = 28; + int defaultSoundId = GameVersionHelper.GetDefaultAmbientSoundId(_targetProject); + + if (defaultSoundId > 0) + numeric_SoundID.Value = defaultSoundId; } #endregion Initialization @@ -67,17 +69,19 @@ protected override void OnShown(EventArgs e) private void textBox_LevelName_TextChanged(object sender, EventArgs e) { if (!checkBox_CustomFileName.Checked) - textBox_CustomFileName.Text = textBox_LevelName.Text.Trim().Replace(' ', '_'); + textBox_CustomFileName.Text = LevelNameHelper.SuggestDataFileName(textBox_LevelName.Text); } private void checkBox_CustomFileName_CheckedChanged(object sender, EventArgs e) { if (checkBox_CustomFileName.Checked) + { textBox_CustomFileName.Enabled = true; + } else { textBox_CustomFileName.Enabled = false; - textBox_CustomFileName.Text = textBox_LevelName.Text.Trim().Replace(' ', '_'); + textBox_CustomFileName.Text = LevelNameHelper.SuggestDataFileName(textBox_LevelName.Text); } } @@ -85,7 +89,7 @@ private void textBox_CustomFileName_TextChanged(object sender, EventArgs e) { int cachedCaretPosition = textBox_CustomFileName.SelectionStart; - textBox_CustomFileName.Text = LevelHandling.MakeValidVariableName(textBox_CustomFileName.Text); + textBox_CustomFileName.Text = LevelNameHelper.MakeValidVariableName(textBox_CustomFileName.Text); textBox_CustomFileName.SelectionStart = cachedCaretPosition; } @@ -95,106 +99,19 @@ private void button_Create_Click(object sender, EventArgs e) try { - string levelName = PathHelper.RemoveIllegalPathSymbols(textBox_LevelName.Text.Trim()); - levelName = LevelHandling.RemoveIllegalNameSymbols(levelName); - - if (!levelName.IsANSI()) - throw new ArgumentException("The level name contains illegal characters. Please use only English characters and numbers."); - - if (string.IsNullOrWhiteSpace(levelName)) - throw new ArgumentException("You must enter a valid name for your level."); - - string dataFileName = LevelHandling.MakeValidVariableName(textBox_CustomFileName.Text.Trim()); - - if (!dataFileName.IsANSI()) - throw new ArgumentException("The data file name contains illegal characters. Please use only English characters and numbers."); - - if (string.IsNullOrWhiteSpace(dataFileName)) - throw new ArgumentException("You must specify the custom PRJ2 / DATA file name."); - - string levelFolderPath = Path.Combine(_targetProject.LevelsDirectoryPath, levelName); - - // Create the level folder - if (!Directory.Exists(levelFolderPath)) - Directory.CreateDirectory(levelFolderPath); - - if (Directory.EnumerateFileSystemEntries(levelFolderPath).ToArray().Length > 0) // 99% this will never accidentally happen - throw new ArgumentException("A folder with the same name as the \"Level name\" already exists in\n" + - "the project's /Levels/ folder and it's not empty."); - - ILevelProject createdLevel = new LevelProject(levelName, levelFolderPath); - - // Create a simple .prj2 file with pre-set project settings (game paths etc.) - var level = Level.CreateSimpleLevel(); - - string prj2FilePath = Path.Combine(createdLevel.DirectoryPath, dataFileName) + ".prj2"; - string exeFilePath = _targetProject.GetEngineExecutableFilePath(); - string engineDirectory = _targetProject.GetEngineRootDirectoryPath(); - - string dataFilePath = Path.Combine(engineDirectory, "data", dataFileName + _targetProject.DataFileExtension); - - level.Settings.LevelFilePath = prj2FilePath; - - level.Settings.GameDirectory = level.Settings.MakeRelative(engineDirectory, VariableType.LevelDirectory); - level.Settings.GameExecutableFilePath = level.Settings.MakeRelative(exeFilePath, VariableType.LevelDirectory); - level.Settings.ScriptDirectory = level.Settings.MakeRelative(_targetProject.GetScriptRootDirectory(), VariableType.LevelDirectory); - level.Settings.GameLevelFilePath = level.Settings.MakeRelative(dataFilePath, VariableType.LevelDirectory); - level.Settings.GameVersion = _targetProject.GameVersion is TRVersion.Game.TR1 ? TRVersion.Game.TR1X : _targetProject.GameVersion; // Map TR1 to TR1X - we never supported vanilla TR1 in TombIDE - - level.Settings.WadSoundPaths.Clear(); - level.Settings.WadSoundPaths.Add(new WadSoundPath(LevelSettings.VariableCreate(VariableType.LevelDirectory) + LevelSettings.Dir + ".." + LevelSettings.Dir + ".." + LevelSettings.Dir + "Sounds")); - - if (_targetProject.GameVersion.Native() <= TRVersion.Game.TR3) - { - level.Settings.AgressiveTexturePacking = true; - level.Settings.TexturePadding = 1; - } - - if (_targetProject.GameVersion == TRVersion.Game.TombEngine) - level.Settings.TenLuaScriptFile = Path.Combine(LevelSettings.VariableCreate(VariableType.ScriptDirectory), "Levels", LevelSettings.VariableCreate(VariableType.LevelName) + ".lua"); - - level.Settings.LoadDefaultSoundCatalog(); - - string? defaultWadPath = _targetProject.GameVersion switch + var options = new LevelSetupOptions { - TRVersion.Game.TombEngine => Path.Combine(_targetProject.DirectoryPath, "Assets", "Wads", "TombEngine.wad2"), - _ => null + LevelName = textBox_LevelName.Text, + DataFileName = textBox_CustomFileName.Text, + GenerateScript = checkBox_GenerateSection.Checked, + AmbientSoundId = (int)numeric_SoundID.Value, + EnableHorizon = checkBox_EnableHorizon.Checked }; - if (defaultWadPath is not null && File.Exists(defaultWadPath)) - level.Settings.LoadWad(defaultWadPath); - - var texturePath = Path.Combine(_targetProject.DirectoryPath, "Assets", "Textures", "default.png"); - - if (File.Exists(texturePath)) - { - level.Settings.Textures.Add(new LevelTexture(level.Settings, Path.Combine(_targetProject.DirectoryPath, "Assets", "Textures", "default.png"))); - var texture = new TextureArea() { Texture = level.Settings.Textures[0] }; - - texture.TexCoord0 = new VectorInt2(0, 0); - texture.TexCoord1 = new VectorInt2(texture.Texture.Image.Width, 0); - texture.TexCoord2 = new VectorInt2(texture.Texture.Image.Width, texture.Texture.Image.Height); - texture.TexCoord3 = new VectorInt2(0, texture.Texture.Image.Height); - level.Settings.DefaultTexture = texture; - } - - Prj2Writer.SaveToPrj2(prj2FilePath, level); - - if (checkBox_GenerateSection.Checked) - { - int ambientSoundID = (int)numeric_SoundID.Value; - bool horizon = checkBox_EnableHorizon.Checked; - - // // // // - GeneratedScriptLines = LevelHandling.GenerateScriptLines(levelName, dataFileName, _targetProject.GameVersion, ambientSoundID, horizon); - // // // // - } - - createdLevel.Save(); + LevelSetupResult result = _levelSetupService.CreateLevel(_targetProject, options); - // // // // - CreatedLevel = createdLevel; - // // // // + CreatedLevel = result.CreatedLevel; + GeneratedScript = result.GeneratedScript; } catch (Exception ex) { diff --git a/TombIDE/TombIDE.ProjectMaster/Forms/FormRenameLevel.cs b/TombIDE/TombIDE.ProjectMaster/Forms/FormRenameLevel.cs index 68e9dfcc0..f216a2dc3 100644 --- a/TombIDE/TombIDE.ProjectMaster/Forms/FormRenameLevel.cs +++ b/TombIDE/TombIDE.ProjectMaster/Forms/FormRenameLevel.cs @@ -1,89 +1,66 @@ using DarkUI.Forms; using System; -using System.IO; using System.Windows.Forms; +using TombIDE.ProjectMaster.Services.Level.Rename; using TombIDE.Shared; -using TombIDE.Shared.SharedClasses; namespace TombIDE.ProjectMaster { public partial class FormRenameLevel : DarkForm { - private IDE _ide; + private readonly IDE _ide; + private readonly ILevelRenameService _levelRenameService; + private readonly LevelRenameState _initialState; #region Initialization - public FormRenameLevel(IDE ide) + public FormRenameLevel(IDE ide, ILevelRenameService levelRenameService) { _ide = ide; + _levelRenameService = levelRenameService; InitializeComponent(); - // Disable renaming external level folders (level folders which are outside of the project's /Levels/ folder) - if (_ide.SelectedLevel.IsExternal(_ide.Project.LevelsDirectoryPath)) - { - checkBox_RenameDirectory.Text = "Can't rename external level folders"; - checkBox_RenameDirectory.Checked = false; - checkBox_RenameDirectory.Enabled = false; - } + _initialState = _levelRenameService.GetInitialRenameState( + _ide.SelectedLevel, + _ide.Project, + _ide.ScriptEditor_IsScriptDefined, + _ide.ScriptEditor_IsStringDefined); - if (_ide.Project.GameVersion == TombLib.LevelData.TRVersion.Game.TombEngine) - { - checkBox_RenameScriptEntry.Text = "Rename language entry as well (Recommended)"; + ApplyInitialState(); + } - if (!_ide.ScriptEditor_IsStringDefined(_ide.SelectedLevel.Name)) - { - checkBox_RenameScriptEntry.Checked = false; - checkBox_RenameScriptEntry.Enabled = false; - label_LanguageError.Visible = true; - } - } - else if (_ide.Project.GameVersion - is TombLib.LevelData.TRVersion.Game.TR1 - or TombLib.LevelData.TRVersion.Game.TR2X - or TombLib.LevelData.TRVersion.Game.TR2 - or TombLib.LevelData.TRVersion.Game.TR3) - { - checkBox_RenameScriptEntry.Text = "Rename script entry as well (Recommended)"; + private void ApplyInitialState() + { + // Configure directory rename checkbox + checkBox_RenameDirectory.Text = _initialState.DirectoryRenameText; + checkBox_RenameDirectory.Enabled = _initialState.CanRenameDirectory; + checkBox_RenameDirectory.Checked = _initialState.CanRenameDirectory && _initialState.ShouldRenameDirectory; + + // Configure script rename checkbox + checkBox_RenameScriptEntry.Text = _initialState.ScriptRenameText; + checkBox_RenameScriptEntry.Enabled = _initialState.CanRenameScript; + checkBox_RenameScriptEntry.Checked = _initialState.CanRenameScript && _initialState.ShouldRenameScript; + + // Configure error labels + label_ScriptError.Visible = _initialState.ShowScriptError; + label_LanguageError.Visible = _initialState.ShowLanguageError; + + // Adjust form height based on error state + AdjustFormHeight(); + } - if (!_ide.ScriptEditor_IsScriptDefined(_ide.SelectedLevel.Name)) - { - checkBox_RenameScriptEntry.Checked = false; - checkBox_RenameScriptEntry.Enabled = false; - label_ScriptError.Visible = true; - } - } - else - { - // Check if there are errors in the script - if (!_ide.ScriptEditor_IsScriptDefined(_ide.SelectedLevel.Name) || !_ide.ScriptEditor_IsStringDefined(_ide.SelectedLevel.Name)) - { - // Disable the checkBox if so - checkBox_RenameScriptEntry.Checked = false; - checkBox_RenameScriptEntry.Enabled = false; - - // Display ScriptError + LanguageError - if (!_ide.ScriptEditor_IsScriptDefined(_ide.SelectedLevel.Name) && !_ide.ScriptEditor_IsStringDefined(_ide.SelectedLevel.Name)) - { - label_ScriptError.Visible = true; - label_LanguageError.Visible = true; - } - // Display ScriptError only - else if (!_ide.ScriptEditor_IsScriptDefined(_ide.SelectedLevel.Name) && _ide.ScriptEditor_IsStringDefined(_ide.SelectedLevel.Name)) - { - Height = 212; - label_ScriptError.Visible = true; - } - // Display LanguageError only - else if (_ide.ScriptEditor_IsScriptDefined(_ide.SelectedLevel.Name) && !_ide.ScriptEditor_IsStringDefined(_ide.SelectedLevel.Name)) - { - Height = 212; - label_LanguageError.Visible = true; - } - } - else // No errors - Height = 193; - } + private void AdjustFormHeight() + { + bool showNoErrors = !_initialState.ShowScriptError && !_initialState.ShowLanguageError; + bool showOneError = _initialState.ShowScriptError != _initialState.ShowLanguageError; + + if (showNoErrors) + Height = 193; + else if (showOneError) + Height = 212; + + // Default height for both errors } protected override void OnShown(EventArgs e) @@ -102,41 +79,28 @@ private void button_Apply_Click(object sender, EventArgs e) { try { - string newName = PathHelper.RemoveIllegalPathSymbols(textBox_NewName.Text.Trim()); - newName = LevelHandling.RemoveIllegalNameSymbols(newName); + var options = new LevelRenameOptions + { + NewName = textBox_NewName.Text, + RenameDirectory = checkBox_RenameDirectory.Checked, + RenameScriptEntry = checkBox_RenameScriptEntry.Checked + }; - bool renameDirectory = checkBox_RenameDirectory.Checked; - bool renameScriptEntry = checkBox_RenameScriptEntry.Checked; + LevelRenameResult result = _levelRenameService.RenameLevel( + _ide.SelectedLevel, + _ide.Project, + options); - if (newName == _ide.SelectedLevel.Name) + if (!result.ChangesMade) { - // If the name hasn't changed, but the directory name is different and the user wants to rename it - if (Path.GetFileName(_ide.SelectedLevel.DirectoryPath) != newName && renameDirectory) - { - string newDirectory = Path.Combine(Path.GetDirectoryName(_ide.SelectedLevel.DirectoryPath) ?? string.Empty, newName); - - if (Directory.Exists(newDirectory)) - throw new ArgumentException("A directory with the same name already exists in the parent directory."); - - _ide.SelectedLevel.Rename(newName, true); - _ide.RaiseEvent(new IDE.SelectedLevelSettingsChangedEvent()); - } - else - DialogResult = DialogResult.Cancel; + DialogResult = DialogResult.Cancel; + return; } - else - { - string newDirectory = Path.Combine(Path.GetDirectoryName(_ide.SelectedLevel.DirectoryPath) ?? string.Empty, newName); - - if (renameDirectory && Directory.Exists(newDirectory) && !newDirectory.Equals(_ide.SelectedLevel.DirectoryPath, StringComparison.OrdinalIgnoreCase)) - throw new ArgumentException("A directory with the same name already exists in the parent directory."); - if (renameScriptEntry) - _ide.ScriptEditor_RenameLevel(_ide.SelectedLevel.Name, newName); + if (result.ScriptRenameNeeded && result.OldName is not null && result.NewName is not null) + _ide.ScriptEditor_RenameLevel(result.OldName, result.NewName); - _ide.SelectedLevel.Rename(newName, renameDirectory); - _ide.RaiseEvent(new IDE.SelectedLevelSettingsChangedEvent()); - } + _ide.RaiseEvent(new IDE.SelectedLevelSettingsChangedEvent()); } catch (Exception ex) { @@ -147,62 +111,26 @@ private void button_Apply_Click(object sender, EventArgs e) private void textBox_NewName_TextChanged(object sender, EventArgs e) { - string textBoxContent = PathHelper.RemoveIllegalPathSymbols(textBox_NewName.Text.Trim()); - textBoxContent = LevelHandling.RemoveIllegalNameSymbols(textBoxContent); - - // If the name hasn't changed, but the level folder name is different - if (textBoxContent == _ide.SelectedLevel.Name && Path.GetFileName(_ide.SelectedLevel.DirectoryPath) != textBoxContent) + LevelRenameState state = _levelRenameService.GetRenameStateForText( + _ide.SelectedLevel, + _ide.Project, + textBox_NewName.Text, + _initialState.ShowScriptError, + _initialState.ShowLanguageError); + + // Update directory checkbox + if (_initialState.CanRenameDirectory) // Only update if initially allowed (not external) { - // If the level is not an external level - if (!_ide.SelectedLevel.IsExternal(_ide.Project.LevelsDirectoryPath)) - { - checkBox_RenameDirectory.Enabled = true; - checkBox_RenameDirectory.Checked = true; - } - - checkBox_RenameScriptEntry.Checked = false; - checkBox_RenameScriptEntry.Enabled = false; + checkBox_RenameDirectory.Enabled = state.CanRenameDirectory; + checkBox_RenameDirectory.Checked = state.ShouldRenameDirectory; } - // If the name changed, but the level folder name is the same - else if (textBoxContent != _ide.SelectedLevel.Name && Path.GetFileName(_ide.SelectedLevel.DirectoryPath) == textBoxContent) - { - checkBox_RenameDirectory.Checked = false; - checkBox_RenameDirectory.Enabled = false; - // If there are no errors in the script (in this case, if no errors are displayed) - if (!label_ScriptError.Visible && !label_LanguageError.Visible) - { - checkBox_RenameScriptEntry.Enabled = true; - checkBox_RenameScriptEntry.Checked = true; - } - } - // If the name hasn't changed and the level folder name is the same - else if (textBoxContent == _ide.SelectedLevel.Name) - { - checkBox_RenameDirectory.Checked = false; - checkBox_RenameDirectory.Enabled = false; - - checkBox_RenameScriptEntry.Checked = false; - checkBox_RenameScriptEntry.Enabled = false; - } - else // Basically every other scenario - { - // If the level is not an external level - if (!_ide.SelectedLevel.IsExternal(_ide.Project.LevelsDirectoryPath)) - { - checkBox_RenameDirectory.Enabled = true; - checkBox_RenameDirectory.Checked = true; - } - - // If there are no errors in the script (in this case, if no errors are displayed) - if (!label_ScriptError.Visible && !label_LanguageError.Visible) - { - checkBox_RenameScriptEntry.Enabled = true; - checkBox_RenameScriptEntry.Checked = true; - } - } + // Update script checkbox + checkBox_RenameScriptEntry.Enabled = state.CanRenameScript; + checkBox_RenameScriptEntry.Checked = state.ShouldRenameScript; - button_Apply.Enabled = !string.IsNullOrWhiteSpace(textBoxContent); + // Update apply button + button_Apply.Enabled = _levelRenameService.CanApply(textBox_NewName.Text); } #endregion Events diff --git a/TombIDE/TombIDE.ProjectMaster/LevelManager.cs b/TombIDE/TombIDE.ProjectMaster/LevelManager.cs index c7c0179fc..94447f43f 100644 --- a/TombIDE/TombIDE.ProjectMaster/LevelManager.cs +++ b/TombIDE/TombIDE.ProjectMaster/LevelManager.cs @@ -7,7 +7,10 @@ using TombIDE.ProjectMaster.Services.EngineUpdate; using TombIDE.ProjectMaster.Services.EngineVersion; using TombIDE.ProjectMaster.Services.FileExtraction; -using TombIDE.ProjectMaster.Services.LevelCompile; +using TombIDE.ProjectMaster.Services.Level.Compile; +using TombIDE.ProjectMaster.Services.Level.Import; +using TombIDE.ProjectMaster.Services.Level.Rename; +using TombIDE.ProjectMaster.Services.Level.Setup; using TombIDE.Shared; using TombLib.LevelData; @@ -20,15 +23,21 @@ public partial class LevelManager : UserControl private readonly IEngineVersionService _engineVersionService; private readonly IEngineUpdateServiceFactory _engineUpdateServiceFactory; private readonly ILevelCompileService _levelBuildService; + private readonly ILevelSetupService _levelSetupService; + private readonly ILevelRenameService _levelRenameService; + private readonly ILevelImportService _levelImportService; private readonly IUIResourceService _uiResourceService; - public LevelManager() : this(null, null, null, null) + public LevelManager() : this(null, null, null, null, null, null, null) { } public LevelManager( IEngineVersionService? engineVersionService, IEngineUpdateServiceFactory? engineUpdateServiceFactory, ILevelCompileService? levelBuildService, + ILevelSetupService? levelSetupService, + ILevelRenameService? levelRenameService, + ILevelImportService? levelImportService, IUIResourceService? uiResourceService) { InitializeComponent(); @@ -39,6 +48,9 @@ public LevelManager( _engineUpdateServiceFactory = engineUpdateServiceFactory ?? new EngineUpdateServiceFactory(fileExtractionService); _engineVersionService = engineVersionService ?? new EngineVersionService(_engineUpdateServiceFactory); _levelBuildService = levelBuildService ?? new LevelCompileService(); + _levelSetupService = levelSetupService ?? new LevelSetupService(); + _levelRenameService = levelRenameService ?? new LevelRenameService(); + _levelImportService = levelImportService ?? new LevelImportService(); _uiResourceService = uiResourceService ?? new UIResourceService(); } @@ -51,7 +63,7 @@ public void Initialize(IDE ide) UpdateVersionLabel(); - section_LevelList.Initialize(ide, _levelBuildService); + section_LevelList.Initialize(ide, _levelBuildService, _levelSetupService, _levelRenameService, _levelImportService); section_LevelProperties.Initialize(ide); } diff --git a/TombIDE/TombIDE.ProjectMaster/Sections/SectionLevelList.cs b/TombIDE/TombIDE.ProjectMaster/Sections/SectionLevelList.cs index b28da66eb..56234aac5 100644 --- a/TombIDE/TombIDE.ProjectMaster/Sections/SectionLevelList.cs +++ b/TombIDE/TombIDE.ProjectMaster/Sections/SectionLevelList.cs @@ -6,7 +6,10 @@ using System.Diagnostics; using System.IO; using System.Windows.Forms; -using TombIDE.ProjectMaster.Services.LevelCompile; +using TombIDE.ProjectMaster.Services.Level.Compile; +using TombIDE.ProjectMaster.Services.Level.Import; +using TombIDE.ProjectMaster.Services.Level.Rename; +using TombIDE.ProjectMaster.Services.Level.Setup; using TombIDE.Shared; using TombIDE.Shared.NewStructure; using TombIDE.Shared.SharedClasses; @@ -18,6 +21,9 @@ public partial class SectionLevelList : UserControl { private IDE _ide = null!; private ILevelCompileService _levelCompileService = null!; + private ILevelSetupService _levelSetupService = null!; + private ILevelRenameService _levelRenameService = null!; + private ILevelImportService _levelImportService = null!; #region Initialization @@ -26,10 +32,18 @@ public SectionLevelList() InitializeComponent(); } - public void Initialize(IDE ide, ILevelCompileService levelCompileService) + public void Initialize( + IDE ide, + ILevelCompileService levelCompileService, + ILevelSetupService levelSetupService, + ILevelRenameService levelRenameService, + ILevelImportService levelImportService) { _ide = ide; _levelCompileService = levelCompileService; + _levelSetupService = levelSetupService; + _levelRenameService = levelRenameService; + _levelImportService = levelImportService; _ide.IDEEventRaised += OnIDEEventRaised; FillLevelList(); // With levels taken from the .trproj file (current _ide.Project) @@ -166,10 +180,10 @@ private void treeView_KeyUp(object sender, KeyEventArgs e) private void ShowLevelSetupForm() { - using var form = new FormLevelSetup(_ide.Project); + using var form = new FormLevelSetup(_ide.Project, _levelSetupService); if (form.ShowDialog(this) == DialogResult.OK && form.CreatedLevel is not null) - OnLevelAdded(form.CreatedLevel, form.GeneratedScriptLines); + OnLevelAdded(form.CreatedLevel, form.GeneratedScript); } private void ImportLevel() @@ -188,10 +202,10 @@ private void ImportLevel() if (Prj2Helper.IsBackupFile(dialog.FileName)) throw new ArgumentException("You cannot import backup files."); - using var form = new FormImportLevel(_ide.Project, dialog.FileName); + using var form = new FormImportLevel(_ide.Project, dialog.FileName, _levelImportService); - if (form.ShowDialog(this) == DialogResult.OK) - OnLevelAdded(form.ImportedLevel, form.GeneratedScriptLines); + if (form.ShowDialog(this) == DialogResult.OK && form.ImportedLevel is not null) + OnLevelAdded(form.ImportedLevel, form.GeneratedScript); } catch (Exception ex) { @@ -205,7 +219,7 @@ private void ShowRenameLevelForm() if (!IsValidLevel(_ide.SelectedLevel)) return; - using var form = new FormRenameLevel(_ide); + using var form = new FormRenameLevel(_ide, _levelRenameService); form.ShowDialog(this); // After the form is done, it will trigger IDE.SelectedLevelSettingsChangedEvent } @@ -347,7 +361,7 @@ private void OpenSelectedLevel() #region Methods - private void OnLevelAdded(ILevelProject addedLevel, List scriptLines) + private void OnLevelAdded(ILevelProject addedLevel, ScriptGenerationResult? generatedScript) { AddLevelToList(addedLevel, true); @@ -363,9 +377,9 @@ private void OnLevelAdded(ILevelProject addedLevel, List scriptLines) } } - if (scriptLines != null && scriptLines.Count > 0) + if (generatedScript is not null && generatedScript.HasContent) { - _ide.ScriptEditor_AppendScriptLines(scriptLines); + _ide.ScriptEditor_AppendScript(generatedScript); _ide.ScriptEditor_AddNewLevelString(addedLevel.Name); } } @@ -409,7 +423,9 @@ private bool IsValidLevel(ILevelProject level) errorMessage = "The selected level is null."; } else + { isValid = level.IsValid(out errorMessage); + } if (!isValid) { diff --git a/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionInfo.cs b/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionInfo.cs new file mode 100644 index 000000000..48b5f99b1 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionInfo.cs @@ -0,0 +1,60 @@ +using System; + +namespace TombIDE.ProjectMaster.Services.EngineVersion; + +/// +/// Represents version information for an engine, including current and latest versions, +/// update status, and auto-update availability. +/// +public sealed record EngineVersionInfo +{ + /// + /// Gets the currently installed version of the engine. + /// + /// + /// The current , or if the version cannot be determined. + /// + public Version? CurrentVersion { get; init; } + + /// + /// Gets the latest available version of the engine. + /// + /// + /// The latest , or if the version cannot be determined. + /// + public Version? LatestVersion { get; init; } + + /// + /// Gets a value indicating whether the current engine version is outdated. + /// + /// + /// if both and are available + /// and the current version is lower than the latest version; otherwise, . + /// + public bool IsOutdated => CurrentVersion is not null && LatestVersion is not null && CurrentVersion < LatestVersion; + + /// + /// Gets a value indicating whether the current engine version is up-to-date. + /// + /// + /// if both and are available + /// and the current version is equal to or greater than the latest version; otherwise, . + /// + public bool IsLatest => CurrentVersion is not null && LatestVersion is not null && CurrentVersion >= LatestVersion; + + /// + /// Gets a value indicating whether the engine supports automatic updates. + /// + /// + /// if the engine can be automatically updated; otherwise, . + /// + public bool SupportsAutoUpdate { get; init; } + + /// + /// Gets the reason why auto-update is blocked, if applicable. + /// + /// + /// A message explaining why auto-update is not available, or if auto-update is supported. + /// + public string? AutoUpdateBlockReason { get; init; } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionService.cs b/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionService.cs index 3a4d339fc..ff21ca9ce 100644 --- a/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionService.cs +++ b/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/EngineVersionService.cs @@ -12,25 +12,26 @@ public EngineVersionService(IEngineUpdateServiceFactory updateServiceFactory) public EngineVersionInfo GetVersionInfo(IGameProject project) { - var info = new EngineVersionInfo - { - CurrentVersion = project.GetCurrentEngineVersion(), - LatestVersion = project.GetLatestEngineVersion() - }; + var currentVersion = project.GetCurrentEngineVersion(); + var latestVersion = project.GetLatestEngineVersion(); // Check if auto-update is supported var updateService = _updateServiceFactory.GetUpdateService(project.GameVersion); - if (updateService is not null && info.CurrentVersion is not null) - { - info.SupportsAutoUpdate = updateService.CanAutoUpdate(info.CurrentVersion, out string? blockReason); - info.AutoUpdateBlockReason = blockReason; - } - else + bool supportsAutoUpdate = false; + string? autoUpdateBlockReason = null; + + if (updateService is not null && currentVersion is not null) { - info.SupportsAutoUpdate = false; + supportsAutoUpdate = updateService.CanAutoUpdate(currentVersion, out autoUpdateBlockReason); } - return info; + return new EngineVersionInfo + { + CurrentVersion = currentVersion, + LatestVersion = latestVersion, + SupportsAutoUpdate = supportsAutoUpdate, + AutoUpdateBlockReason = autoUpdateBlockReason + }; } } diff --git a/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/IEngineVersionService.cs b/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/IEngineVersionService.cs index efd7ac49b..d019985d4 100644 --- a/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/IEngineVersionService.cs +++ b/TombIDE/TombIDE.ProjectMaster/Services/EngineVersion/IEngineVersionService.cs @@ -1,23 +1,7 @@ -using System; using TombIDE.Shared.NewStructure; namespace TombIDE.ProjectMaster.Services.EngineVersion; -/// -/// Represents version information for an engine. -/// -public class EngineVersionInfo -{ - public Version? CurrentVersion { get; set; } - public Version? LatestVersion { get; set; } - - public bool IsOutdated => CurrentVersion is not null && LatestVersion is not null && CurrentVersion < LatestVersion; - public bool IsLatest => CurrentVersion is not null && LatestVersion is not null && CurrentVersion >= LatestVersion; - - public bool SupportsAutoUpdate { get; set; } - public string? AutoUpdateBlockReason { get; set; } -} - /// /// Provides functionality for checking and managing engine versions. /// diff --git a/TombIDE/TombIDE.ProjectMaster/Services/LevelCompile/ILevelCompileService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Compile/ILevelCompileService.cs similarity index 92% rename from TombIDE/TombIDE.ProjectMaster/Services/LevelCompile/ILevelCompileService.cs rename to TombIDE/TombIDE.ProjectMaster/Services/Level/Compile/ILevelCompileService.cs index 725181092..0aea404b5 100644 --- a/TombIDE/TombIDE.ProjectMaster/Services/LevelCompile/ILevelCompileService.cs +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Compile/ILevelCompileService.cs @@ -1,6 +1,6 @@ using TombIDE.Shared.NewStructure; -namespace TombIDE.ProjectMaster.Services.LevelCompile; +namespace TombIDE.ProjectMaster.Services.Level.Compile; /// /// Provides functionality for batch building levels. diff --git a/TombIDE/TombIDE.ProjectMaster/Services/LevelCompile/LevelCompileService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Compile/LevelCompileService.cs similarity index 96% rename from TombIDE/TombIDE.ProjectMaster/Services/LevelCompile/LevelCompileService.cs rename to TombIDE/TombIDE.ProjectMaster/Services/Level/Compile/LevelCompileService.cs index af1b919e6..63876a2ce 100644 --- a/TombIDE/TombIDE.ProjectMaster/Services/LevelCompile/LevelCompileService.cs +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Compile/LevelCompileService.cs @@ -4,7 +4,7 @@ using TombIDE.Shared.NewStructure; using TombLib.LevelData; -namespace TombIDE.ProjectMaster.Services.LevelCompile; +namespace TombIDE.ProjectMaster.Services.Level.Compile; public sealed class LevelCompileService : ILevelCompileService { diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/ILevelImportService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/ILevelImportService.cs new file mode 100644 index 000000000..a290df4bf --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/ILevelImportService.cs @@ -0,0 +1,19 @@ +using TombIDE.Shared; +using TombIDE.Shared.NewStructure; + +namespace TombIDE.ProjectMaster.Services.Level.Import; + +/// +/// Provides functionality for importing existing levels into a project. +/// +public interface ILevelImportService +{ + /// + /// Imports a level into the target project. + /// + /// The project to import the level into. + /// The import options. + /// Optional progress reporter form for file processing. + /// The result of the import operation. + LevelImportResult ImportLevel(IGameProject targetProject, LevelImportOptions options, IProgressReportingForm? progressForm = null); +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportMode.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportMode.cs new file mode 100644 index 000000000..f66a821ea --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportMode.cs @@ -0,0 +1,22 @@ +namespace TombIDE.ProjectMaster.Services.Level.Import; + +/// +/// Specifies how to import a level. +/// +public enum LevelImportMode +{ + /// + /// Copy only the specified .prj2 file into the project's Levels folder. + /// + CopySpecifiedFile, + + /// + /// Copy selected .prj2 files from the source directory into the project's Levels folder. + /// + CopySelectedFiles, + + /// + /// Keep files in their original location and link to them as an external level. + /// + KeepInPlace +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportOptions.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportOptions.cs new file mode 100644 index 000000000..b101e2b42 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportOptions.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace TombIDE.ProjectMaster.Services.Level.Import; + +/// +/// Options for importing a level. +/// +public sealed record LevelImportOptions +{ + /// + /// The name for the imported level. + /// + public string LevelName { get; init; } = string.Empty; + + /// + /// The full path to the originally specified .prj2 file. + /// + public string SourcePrj2FilePath { get; init; } = string.Empty; + + /// + /// The custom data file name (without extension). + /// + public string DataFileName { get; init; } = string.Empty; + + /// + /// The import mode to use. + /// + public LevelImportMode ImportMode { get; init; } + + /// + /// When using , the list of .prj2 file paths to copy. + /// + public IReadOnlyList SelectedFilePaths { get; init; } = []; + + /// + /// Whether to generate script lines for the level. + /// + public bool GenerateScript { get; init; } + + /// + /// The ambient sound ID to use in generated script. + /// + public int AmbientSoundId { get; init; } + + /// + /// Whether to enable horizon in the generated script. + /// + public bool EnableHorizon { get; init; } + + /// + /// Whether to update .prj2 game settings when keeping files in place. + /// Only used when is . + /// + public bool UpdatePrj2SettingsForExternalLevel { get; init; } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportResult.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportResult.cs new file mode 100644 index 000000000..9a4b4610b --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportResult.cs @@ -0,0 +1,25 @@ +using TombIDE.Shared.NewStructure; +using TombIDE.Shared.SharedClasses; + +namespace TombIDE.ProjectMaster.Services.Level.Import; + +/// +/// Result of a level import operation. +/// +public sealed record LevelImportResult +{ + /// + /// The imported level project. Null if import failed. + /// + public ILevelProject? ImportedLevel { get; init; } + + /// + /// Script generation result for the level, if requested. + /// + public ScriptGenerationResult? GeneratedScript { get; init; } + + /// + /// Indicates whether the operation was successful. + /// + public bool Success => ImportedLevel is not null; +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportService.cs new file mode 100644 index 000000000..88ea1d85a --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Import/LevelImportService.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using TombIDE.Shared; +using TombIDE.Shared.NewStructure; +using TombIDE.Shared.NewStructure.Implementations; +using TombIDE.Shared.SharedClasses; + +namespace TombIDE.ProjectMaster.Services.Level.Import; + +public sealed class LevelImportService : ILevelImportService +{ + public LevelImportResult ImportLevel(IGameProject targetProject, LevelImportOptions options, IProgressReportingForm? progressForm = null) + { + string levelName = LevelNameHelper.ValidateLevelName(options.LevelName) + ?? throw new ArgumentException("You must enter a valid name for the level."); + + string dataFileName = LevelNameHelper.ValidateDataFileName(options.DataFileName) + ?? throw new ArgumentException("You must specify a valid custom DATA file name."); + + if (options.ImportMode == LevelImportMode.CopySelectedFiles && options.SelectedFilePaths.Count == 0) + throw new ArgumentException("You must select which .prj2 files you want to import."); + + return options.ImportMode switch + { + LevelImportMode.CopySpecifiedFile => ImportCopySpecifiedFile(targetProject, options, levelName, dataFileName), + LevelImportMode.CopySelectedFiles => ImportCopySelectedFiles(targetProject, options, levelName, dataFileName, progressForm), + LevelImportMode.KeepInPlace => ImportKeepInPlace(targetProject, options, levelName, dataFileName, progressForm), + _ => throw new ArgumentException($"Unknown import mode: {options.ImportMode}") + }; + } + + private static LevelImportResult ImportCopySpecifiedFile( + IGameProject targetProject, + LevelImportOptions options, + string levelName, + string dataFileName) + { + string levelFolderPath = Path.Combine(targetProject.LevelsDirectoryPath, levelName); + string specificFileName = Path.GetFileName(options.SourcePrj2FilePath); + + EnsureLevelFolderEmpty(levelFolderPath); + + // Copy the specified file + string destinationPath = Path.Combine(levelFolderPath, specificFileName); + File.Copy(options.SourcePrj2FilePath, destinationPath); + + // Update settings for the copied file + Prj2Helper.UpdateGameSettings(destinationPath, targetProject, dataFileName); + + return CreateLevelAndResult(targetProject, options, levelName, levelFolderPath, dataFileName, specificFileName); + } + + private static LevelImportResult ImportCopySelectedFiles( + IGameProject targetProject, + LevelImportOptions options, + string levelName, + string dataFileName, + IProgressReportingForm? progress) + { + string levelFolderPath = Path.Combine(targetProject.LevelsDirectoryPath, levelName); + string specificFileName = Path.GetFileName(options.SourcePrj2FilePath); + + EnsureLevelFolderEmpty(levelFolderPath); + + // Check if the originally specified file was selected + bool specificFileSelected = false; + + // Copy all selected files + foreach (string sourcePath in options.SelectedFilePaths) + { + string fileName = Path.GetFileName(sourcePath); + + if (fileName == specificFileName) + specificFileSelected = true; + + string destinationPath = Path.Combine(levelFolderPath, fileName); + File.Copy(sourcePath, destinationPath); + } + + // If the specified file wasn't selected, set target to null + string? targetFileName = specificFileSelected ? specificFileName : null; + + // Update settings for all copied files + UpdateAllPrj2Files(levelFolderPath, targetProject, dataFileName, progress); + + return CreateLevelAndResult(targetProject, options, levelName, levelFolderPath, dataFileName, targetFileName); + } + + private static LevelImportResult ImportKeepInPlace( + IGameProject targetProject, + LevelImportOptions options, + string levelName, + string dataFileName, + IProgressReportingForm? progress) + { + string levelFolderPath = Path.GetDirectoryName(options.SourcePrj2FilePath) + ?? throw new ArgumentException("Could not determine the directory of the source file."); + + string specificFileName = Path.GetFileName(options.SourcePrj2FilePath); + + // Update settings if requested + if (options.UpdatePrj2SettingsForExternalLevel) + UpdateAllPrj2Files(levelFolderPath, targetProject, dataFileName, progress); + + return CreateLevelAndResult(targetProject, options, levelName, levelFolderPath, dataFileName, specificFileName); + } + + private static void EnsureLevelFolderEmpty(string levelFolderPath) + { + if (!Directory.Exists(levelFolderPath)) + Directory.CreateDirectory(levelFolderPath); + + if (Directory.EnumerateFileSystemEntries(levelFolderPath).Any()) + { + throw new ArgumentException("A folder with the same name as the \"Level name\" already exists in\n" + + "the project's /Levels/ folder and it's not empty."); + } + } + + private static void UpdateAllPrj2Files( + string levelFolderPath, + IGameProject targetProject, + string dataFileName, + IProgressReportingForm? progress) + { + string[] files = Directory.GetFiles(levelFolderPath, "*.prj2", SearchOption.TopDirectoryOnly); + + progress?.SetTotalProgress(files.Length); + + foreach (string file in files) + { + if (!Prj2Helper.IsBackupFile(file)) + Prj2Helper.UpdateGameSettings(file, targetProject, dataFileName); + + progress?.IncrementProgress(1); + } + } + + private static LevelImportResult CreateLevelAndResult( + IGameProject targetProject, + LevelImportOptions options, + string levelName, + string levelFolderPath, + string dataFileName, + string? targetFileName) + { + var importedLevel = new LevelProject(levelName, levelFolderPath, targetFileName); + ScriptGenerationResult? generatedScript = null; + + if (options.GenerateScript) + generatedScript = ScriptGenerator.GenerateScripts(levelName, dataFileName, targetProject.GameVersion, options.AmbientSoundId, options.EnableHorizon); + + var result = new LevelImportResult + { + ImportedLevel = importedLevel, + GeneratedScript = generatedScript + }; + + importedLevel.Save(); + + return result; + } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/ILevelRenameService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/ILevelRenameService.cs new file mode 100644 index 000000000..e20edbb62 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/ILevelRenameService.cs @@ -0,0 +1,63 @@ +using System; +using TombIDE.Shared.NewStructure; + +namespace TombIDE.ProjectMaster.Services.Level.Rename; + +/// +/// Provides functionality for renaming levels. +/// +public interface ILevelRenameService +{ + /// + /// Renames a level with the specified options. + /// + /// The level to rename. + /// The project containing the level. + /// The rename options. + /// The result of the rename operation. + LevelRenameResult RenameLevel(ILevelProject level, IGameProject project, LevelRenameOptions options); + + /// + /// Validates the new level name. + /// + /// The new name to validate. + /// The sanitized name, or null if invalid. + string? ValidateName(string newName); + + /// + /// Gets the initial rename state for a level based on the project settings. + /// + /// The level to get the state for. + /// The project containing the level. + /// Function to check if script is defined for the level name. + /// Function to check if string is defined for the level name. + /// The initial rename state. + LevelRenameState GetInitialRenameState( + ILevelProject level, + IGameProject project, + Func isScriptDefined, + Func isStringDefined); + + /// + /// Gets the rename state based on the current text input. + /// + /// The level being renamed. + /// The project containing the level. + /// The current text in the name input. + /// Whether there are script errors displayed. + /// Whether there are language errors displayed. + /// The updated rename state. + LevelRenameState GetRenameStateForText( + ILevelProject level, + IGameProject project, + string currentText, + bool hasScriptErrors, + bool hasLanguageErrors); + + /// + /// Determines if the apply button should be enabled based on the current text. + /// + /// The current text in the name input. + /// True if the apply button should be enabled. + bool CanApply(string currentText); +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameOptions.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameOptions.cs new file mode 100644 index 000000000..19a3a4c61 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameOptions.cs @@ -0,0 +1,22 @@ +namespace TombIDE.ProjectMaster.Services.Level.Rename; + +/// +/// Options for renaming a level. +/// +public sealed record LevelRenameOptions +{ + /// + /// The new name for the level. + /// + public string NewName { get; init; } = string.Empty; + + /// + /// Whether to rename the level's directory as well. + /// + public bool RenameDirectory { get; init; } + + /// + /// Whether to rename the script entry as well. + /// + public bool RenameScriptEntry { get; init; } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameResult.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameResult.cs new file mode 100644 index 000000000..f2ad823bc --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameResult.cs @@ -0,0 +1,32 @@ +namespace TombIDE.ProjectMaster.Services.Level.Rename; + +/// +/// Result of a level rename operation. +/// +public sealed record LevelRenameResult +{ + /// + /// Indicates whether the operation was successful. + /// + public bool Success { get; init; } + + /// + /// Indicates whether any changes were made. + /// + public bool ChangesMade { get; init; } + + /// + /// Indicates whether a script rename is needed. + /// + public bool ScriptRenameNeeded { get; init; } + + /// + /// The old level name (for script rename). + /// + public string? OldName { get; init; } + + /// + /// The new level name (for script rename). + /// + public string? NewName { get; init; } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameService.cs new file mode 100644 index 000000000..2c883ec55 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameService.cs @@ -0,0 +1,236 @@ +using System; +using System.IO; +using TombIDE.Shared.NewStructure; +using TombIDE.Shared.SharedClasses; +using TombLib.LevelData; + +namespace TombIDE.ProjectMaster.Services.Level.Rename; + +public sealed class LevelRenameService : ILevelRenameService +{ + public LevelRenameResult RenameLevel(ILevelProject level, IGameProject project, LevelRenameOptions options) + { + string? newName = ValidateName(options.NewName); + if (newName is null) + throw new ArgumentException("You must enter a valid name for the level."); + + bool renameDirectory = options.RenameDirectory; + bool renameScriptEntry = options.RenameScriptEntry; + string oldName = level.Name; + + if (newName == level.Name) + { + // If the name hasn't changed, but the directory name is different and the user wants to rename it + if (Path.GetFileName(level.DirectoryPath) != newName && renameDirectory) + { + string newDirectory = Path.Combine(Path.GetDirectoryName(level.DirectoryPath) ?? string.Empty, newName); + + if (Directory.Exists(newDirectory)) + throw new ArgumentException("A directory with the same name already exists in the parent directory."); + + level.Rename(newName, true); + + return new LevelRenameResult + { + Success = true, + ChangesMade = true, + ScriptRenameNeeded = false + }; + } + + // No changes needed + return new LevelRenameResult + { + Success = true, + ChangesMade = false, + ScriptRenameNeeded = false + }; + } + + // Name changed + string targetDirectory = Path.Combine(Path.GetDirectoryName(level.DirectoryPath) ?? string.Empty, newName); + + if (renameDirectory && Directory.Exists(targetDirectory) && !targetDirectory.Equals(level.DirectoryPath, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentException("A directory with the same name already exists in the parent directory."); + + level.Rename(newName, renameDirectory); + + return new LevelRenameResult + { + Success = true, + ChangesMade = true, + ScriptRenameNeeded = renameScriptEntry, + OldName = oldName, + NewName = newName + }; + } + + public string? ValidateName(string newName) + { + string sanitized = PathHelper.RemoveIllegalPathSymbols(newName.Trim()); + sanitized = LevelNameHelper.RemoveIllegalNameSymbols(sanitized); + + return string.IsNullOrWhiteSpace(sanitized) ? null : sanitized; + } + + public LevelRenameState GetInitialRenameState( + ILevelProject level, + IGameProject project, + Func isScriptDefined, + Func isStringDefined) + { + bool isExternal = level.IsExternal(project.LevelsDirectoryPath); + string directoryRenameText = isExternal + ? "Can't rename external level folders" + : "Rename level folder as well (Recommended)"; + + var state = new LevelRenameState + { + CanRenameDirectory = !isExternal, + ShouldRenameDirectory = true, + DirectoryRenameText = directoryRenameText, + CanRenameScript = true, + ShouldRenameScript = true, + ScriptRenameText = "Rename script entry as well (Recommended)", + ShowScriptError = false, + ShowLanguageError = false + }; + + // Handle TombEngine - only language entries + if (project.GameVersion == TRVersion.Game.TombEngine) + { + state.ScriptRenameText = "Rename language entry as well (Recommended)"; + + if (!isStringDefined(level.Name)) + { + state.CanRenameScript = false; + state.ShouldRenameScript = false; + state.ShowLanguageError = true; + } + + return state; + } + + // Handle TR1, TR2X, TR2, TR3 - only script entries + if (project.GameVersion is TRVersion.Game.TR1 or TRVersion.Game.TR2X or TRVersion.Game.TR2 or TRVersion.Game.TR3) + { + if (!isScriptDefined(level.Name)) + { + state.CanRenameScript = false; + state.ShouldRenameScript = false; + state.ShowScriptError = true; + } + + return state; + } + + // Handle TR4/TRNG - both script and language entries + bool scriptDefined = isScriptDefined(level.Name); + bool stringDefined = isStringDefined(level.Name); + + if (!scriptDefined || !stringDefined) + { + state.CanRenameScript = false; + state.ShouldRenameScript = false; + state.ShowScriptError = !scriptDefined; + state.ShowLanguageError = !stringDefined; + } + + return state; + } + + public LevelRenameState GetRenameStateForText( + ILevelProject level, + IGameProject project, + string currentText, + bool hasScriptErrors, + bool hasLanguageErrors) + { + string textBoxContent = PathHelper.RemoveIllegalPathSymbols(currentText.Trim()); + textBoxContent = LevelNameHelper.RemoveIllegalNameSymbols(textBoxContent); + + bool isExternal = level.IsExternal(project.LevelsDirectoryPath); + string directoryName = Path.GetFileName(level.DirectoryPath); + string directoryRenameText = isExternal + ? "Can't rename external level folders" + : "Rename level folder as well (Recommended)"; + string scriptRenameText = GetScriptRenameText(project); + + // If the name hasn't changed, but the level folder name is different + if (textBoxContent == level.Name && directoryName != textBoxContent) + { + return new LevelRenameState + { + CanRenameDirectory = !isExternal, + ShouldRenameDirectory = !isExternal, + DirectoryRenameText = directoryRenameText, + CanRenameScript = false, + ShouldRenameScript = false, + ScriptRenameText = scriptRenameText, + ShowScriptError = hasScriptErrors, + ShowLanguageError = hasLanguageErrors + }; + } + + // If the name changed, but the level folder name is the same + if (textBoxContent != level.Name && directoryName == textBoxContent) + { + bool canRenameScript = !hasScriptErrors && !hasLanguageErrors; + return new LevelRenameState + { + CanRenameDirectory = false, + ShouldRenameDirectory = false, + DirectoryRenameText = directoryRenameText, + CanRenameScript = canRenameScript, + ShouldRenameScript = canRenameScript, + ScriptRenameText = scriptRenameText, + ShowScriptError = hasScriptErrors, + ShowLanguageError = hasLanguageErrors + }; + } + + // If the name hasn't changed and the level folder name is the same + if (textBoxContent == level.Name) + { + return new LevelRenameState + { + CanRenameDirectory = false, + ShouldRenameDirectory = false, + DirectoryRenameText = directoryRenameText, + CanRenameScript = false, + ShouldRenameScript = false, + ScriptRenameText = scriptRenameText, + ShowScriptError = hasScriptErrors, + ShowLanguageError = hasLanguageErrors + }; + } + + // Basically every other scenario + bool canRename = !hasScriptErrors && !hasLanguageErrors; + return new LevelRenameState + { + CanRenameDirectory = !isExternal, + ShouldRenameDirectory = !isExternal, + DirectoryRenameText = directoryRenameText, + CanRenameScript = canRename, + ShouldRenameScript = canRename, + ScriptRenameText = scriptRenameText, + ShowScriptError = hasScriptErrors, + ShowLanguageError = hasLanguageErrors + }; + } + + public bool CanApply(string currentText) + { + string textBoxContent = PathHelper.RemoveIllegalPathSymbols(currentText.Trim()); + textBoxContent = LevelNameHelper.RemoveIllegalNameSymbols(textBoxContent); + return !string.IsNullOrWhiteSpace(textBoxContent); + } + + private static string GetScriptRenameText(IGameProject project) + { + return project.GameVersion == TRVersion.Game.TombEngine + ? "Rename language entry as well (Recommended)" + : "Rename script entry as well (Recommended)"; + } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameState.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameState.cs new file mode 100644 index 000000000..3d9960ede --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Rename/LevelRenameState.cs @@ -0,0 +1,47 @@ +namespace TombIDE.ProjectMaster.Services.Level.Rename; + +/// +/// Information about the rename checkbox states. +/// +public sealed class LevelRenameState +{ + /// + /// Whether the directory rename checkbox should be enabled. + /// + public bool CanRenameDirectory { get; set; } + + /// + /// Whether the directory rename checkbox should be checked by default. + /// + public bool ShouldRenameDirectory { get; set; } + + /// + /// The text to display for the directory rename checkbox. + /// + public string DirectoryRenameText { get; set; } = "Rename level folder as well (Recommended)"; + + /// + /// Whether the script rename checkbox should be enabled. + /// + public bool CanRenameScript { get; set; } + + /// + /// Whether the script rename checkbox should be checked by default. + /// + public bool ShouldRenameScript { get; set; } + + /// + /// The text to display for the script rename checkbox. + /// + public string ScriptRenameText { get; set; } = "Rename script entry as well (Recommended)"; + + /// + /// Whether to show the script error label. + /// + public bool ShowScriptError { get; set; } + + /// + /// Whether to show the language error label. + /// + public bool ShowLanguageError { get; set; } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/ILevelSetupService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/ILevelSetupService.cs new file mode 100644 index 000000000..617fe6333 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/ILevelSetupService.cs @@ -0,0 +1,17 @@ +using TombIDE.Shared.NewStructure; + +namespace TombIDE.ProjectMaster.Services.Level.Setup; + +/// +/// Provides functionality for creating new levels in a project. +/// +public interface ILevelSetupService +{ + /// + /// Creates a new level project in the target project's Levels directory. + /// + /// The project to create the level in. + /// The level setup options. + /// The result of the setup operation. + LevelSetupResult CreateLevel(IGameProject targetProject, LevelSetupOptions options); +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupOptions.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupOptions.cs new file mode 100644 index 000000000..301a3bec5 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupOptions.cs @@ -0,0 +1,32 @@ +namespace TombIDE.ProjectMaster.Services.Level.Setup; + +/// +/// Options for setting up a new level. +/// +public sealed record LevelSetupOptions +{ + /// + /// The name of the level. + /// + public string LevelName { get; init; } = string.Empty; + + /// + /// The custom data file name (without extension). + /// + public string DataFileName { get; init; } = string.Empty; + + /// + /// Whether to generate script lines for the level. + /// + public bool GenerateScript { get; init; } + + /// + /// The ambient sound ID to use in generated script. + /// + public int AmbientSoundId { get; init; } + + /// + /// Whether to enable horizon in the generated script. + /// + public bool EnableHorizon { get; init; } +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupResult.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupResult.cs new file mode 100644 index 000000000..a754dc874 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupResult.cs @@ -0,0 +1,25 @@ +using TombIDE.Shared.NewStructure; +using TombIDE.Shared.SharedClasses; + +namespace TombIDE.ProjectMaster.Services.Level.Setup; + +/// +/// Result of a level setup operation. +/// +public sealed record LevelSetupResult +{ + /// + /// The created level project. Null if creation failed. + /// + public ILevelProject? CreatedLevel { get; init; } + + /// + /// Script generation result for the level, if requested. + /// + public ScriptGenerationResult? GeneratedScript { get; init; } + + /// + /// Indicates whether the operation was successful. + /// + public bool Success => CreatedLevel is not null; +} diff --git a/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupService.cs b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupService.cs new file mode 100644 index 000000000..5b54b50e2 --- /dev/null +++ b/TombIDE/TombIDE.ProjectMaster/Services/Level/Setup/LevelSetupService.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using TombIDE.Shared.NewStructure; +using TombIDE.Shared.NewStructure.Implementations; +using TombIDE.Shared.SharedClasses; +using TombLib; +using TombLib.LevelData; +using TombLib.LevelData.IO; +using TombLib.Utils; + +namespace TombIDE.ProjectMaster.Services.Level.Setup; + +public sealed class LevelSetupService : ILevelSetupService +{ + public LevelSetupResult CreateLevel(IGameProject targetProject, LevelSetupOptions options) + { + string? levelName = LevelNameHelper.ValidateLevelName(options.LevelName) + ?? throw new ArgumentException("You must enter a valid name for your level."); + + if (!levelName.IsANSI()) + throw new ArgumentException("The level name contains illegal characters. Please use only English characters and numbers."); + + string? dataFileName = LevelNameHelper.ValidateDataFileName(options.DataFileName) + ?? throw new ArgumentException("You must specify a valid custom PRJ2 / DATA file name."); + + if (!dataFileName.IsANSI()) + throw new ArgumentException("The data file name contains illegal characters. Please use only English characters and numbers."); + + string levelFolderPath = Path.Combine(targetProject.LevelsDirectoryPath, levelName); + + // Create the level folder and ensure it's empty + if (!Directory.Exists(levelFolderPath)) + Directory.CreateDirectory(levelFolderPath); + + if (Directory.EnumerateFileSystemEntries(levelFolderPath).Any()) + { + throw new ArgumentException("A folder with the same name as the \"Level name\" already exists in\n" + + "the project's /Levels/ folder and it's not empty."); + } + + ILevelProject createdLevel = new LevelProject(levelName, levelFolderPath); + ScriptGenerationResult? generatedScript = null; + + // Create the .prj2 file + CreatePrj2File(targetProject, createdLevel, dataFileName); + + if (options.GenerateScript) + generatedScript = ScriptGenerator.GenerateScripts(levelName, dataFileName, targetProject.GameVersion, options.AmbientSoundId, options.EnableHorizon); + + var result = new LevelSetupResult + { + CreatedLevel = createdLevel, + GeneratedScript = generatedScript + }; + + createdLevel.Save(); + + return result; + } + + private static void CreatePrj2File(IGameProject targetProject, ILevelProject level, string dataFileName) + { + var prj2Level = TombLib.LevelData.Level.CreateSimpleLevel(); + + string prj2FilePath = Path.Combine(level.DirectoryPath, dataFileName) + ".prj2"; + string exeFilePath = targetProject.GetEngineExecutableFilePath(); + string engineDirectory = targetProject.GetEngineRootDirectoryPath(); + string dataFilePath = Path.Combine(engineDirectory, "data", dataFileName + targetProject.DataFileExtension); + + prj2Level.Settings.LevelFilePath = prj2FilePath; + + prj2Level.Settings.GameDirectory = prj2Level.Settings.MakeRelative(engineDirectory, VariableType.LevelDirectory); + prj2Level.Settings.GameExecutableFilePath = prj2Level.Settings.MakeRelative(exeFilePath, VariableType.LevelDirectory); + prj2Level.Settings.ScriptDirectory = prj2Level.Settings.MakeRelative(targetProject.GetScriptRootDirectory(), VariableType.LevelDirectory); + prj2Level.Settings.GameLevelFilePath = prj2Level.Settings.MakeRelative(dataFilePath, VariableType.LevelDirectory); + prj2Level.Settings.GameVersion = targetProject.GameVersion is TRVersion.Game.TR1 ? TRVersion.Game.TR1X : targetProject.GameVersion; + + prj2Level.Settings.WadSoundPaths.Clear(); + prj2Level.Settings.WadSoundPaths.Add(new WadSoundPath(LevelSettings.VariableCreate(VariableType.LevelDirectory) + LevelSettings.Dir + ".." + LevelSettings.Dir + ".." + LevelSettings.Dir + "Sounds")); + + if (targetProject.GameVersion.Native() <= TRVersion.Game.TR3) + { + prj2Level.Settings.AgressiveTexturePacking = true; + prj2Level.Settings.TexturePadding = 1; + } + + if (targetProject.GameVersion == TRVersion.Game.TombEngine) + prj2Level.Settings.TenLuaScriptFile = Path.Combine(LevelSettings.VariableCreate(VariableType.ScriptDirectory), "Levels", LevelSettings.VariableCreate(VariableType.LevelName) + ".lua"); + + prj2Level.Settings.LoadDefaultSoundCatalog(); + + string? defaultWadPath = targetProject.GameVersion switch + { + TRVersion.Game.TombEngine => Path.Combine(targetProject.DirectoryPath, "Assets", "Wads", "TombEngine.wad2"), + _ => null + }; + + if (defaultWadPath is not null && File.Exists(defaultWadPath)) + prj2Level.Settings.LoadWad(defaultWadPath); + + var texturePath = Path.Combine(targetProject.DirectoryPath, "Assets", "Textures", "default.png"); + + if (File.Exists(texturePath)) + { + prj2Level.Settings.Textures.Add(new LevelTexture(prj2Level.Settings, texturePath)); + + var texture = new TextureArea + { + Texture = prj2Level.Settings.Textures[0], + TexCoord0 = new VectorInt2(0, 0) + }; + + texture.TexCoord1 = new VectorInt2(texture.Texture.Image.Width, 0); + texture.TexCoord2 = new VectorInt2(texture.Texture.Image.Width, texture.Texture.Image.Height); + texture.TexCoord3 = new VectorInt2(0, texture.Texture.Image.Height); + prj2Level.Settings.DefaultTexture = texture; + } + + Prj2Writer.SaveToPrj2(prj2FilePath, prj2Level); + } +} diff --git a/TombIDE/TombIDE.ScriptingStudio/ClassicScriptStudio.cs b/TombIDE/TombIDE.ScriptingStudio/ClassicScriptStudio.cs index 51211abf4..b8b0b1634 100644 --- a/TombIDE/TombIDE.ScriptingStudio/ClassicScriptStudio.cs +++ b/TombIDE/TombIDE.ScriptingStudio/ClassicScriptStudio.cs @@ -1,7 +1,6 @@ using DarkUI.Docking; using DarkUI.Forms; using System; -using System.Collections.Generic; using System.Data; using System.Diagnostics; using System.IO; @@ -75,7 +74,7 @@ protected override void OnIDEEventRaised(IIDEEvent obj) } private bool IsSilentAction(IIDEEvent obj) - => obj is IDE.ScriptEditor_AppendScriptLinesEvent + => obj is IDE.ScriptEditor_AppendScriptEvent || obj is IDE.ScriptEditor_AddNewLevelStringEvent || obj is IDE.ScriptEditor_AddNewPluginEntryEvent || obj is IDE.ScriptEditor_AddNewNGStringEvent @@ -97,9 +96,9 @@ private void IDEEvent_HandleSilentActions(IIDEEvent obj) bool wasLanguageFileAlreadyOpened = languageFileTab != null; bool wasLanguageFileFileChanged = wasLanguageFileAlreadyOpened && EditorTabControl.GetEditorOfTab(languageFileTab).IsContentChanged; - if (obj is IDE.ScriptEditor_AppendScriptLinesEvent asle && asle.Lines.Count > 0) + if (obj is IDE.ScriptEditor_AppendScriptEvent asle && asle.Result.HasContent) { - AppendScriptLines(asle.Lines); + AppendScript(asle.Result.GameFlowScript); EndSilentScriptAction(cachedTab, true, !wasScriptFileFileChanged, !wasScriptFileAlreadyOpened); } else if (obj is IDE.ScriptEditor_AddNewLevelStringEvent anlse) @@ -149,13 +148,13 @@ private void IDEEvent_HandleSilentActions(IIDEEvent obj) } } - private void AppendScriptLines(List inputLines) + private void AppendScript(string scriptText) { EditorTabControl.OpenFile(PathHelper.GetScriptFilePath(ScriptRootDirectoryPath, TombLib.LevelData.TRVersion.Game.TR4)); if (CurrentEditor is TextEditorBase editor) { - editor.AppendText(string.Join(Environment.NewLine, inputLines) + Environment.NewLine); + editor.AppendText(Environment.NewLine + scriptText + Environment.NewLine); editor.ScrollToLine(editor.LineCount); } } diff --git a/TombIDE/TombIDE.ScriptingStudio/GameFlowScriptStudio.cs b/TombIDE/TombIDE.ScriptingStudio/GameFlowScriptStudio.cs index c7e4fef5b..2992aa444 100644 --- a/TombIDE/TombIDE.ScriptingStudio/GameFlowScriptStudio.cs +++ b/TombIDE/TombIDE.ScriptingStudio/GameFlowScriptStudio.cs @@ -1,6 +1,5 @@ using DarkUI.Forms; using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Windows.Forms; @@ -53,7 +52,7 @@ protected override void OnIDEEventRaised(IIDEEvent obj) } private bool IsSilentAction(IIDEEvent obj) - => obj is IDE.ScriptEditor_AppendScriptLinesEvent + => obj is IDE.ScriptEditor_AppendScriptEvent || obj is IDE.ScriptEditor_ScriptPresenceCheckEvent || obj is IDE.ScriptEditor_RenameLevelEvent; @@ -67,9 +66,9 @@ private void IDEEvent_HandleSilentActions(IIDEEvent obj) bool wasScriptFileAlreadyOpened = scriptFileTab != null; bool wasScriptFileFileChanged = wasScriptFileAlreadyOpened && EditorTabControl.GetEditorOfTab(scriptFileTab).IsContentChanged; - if (obj is IDE.ScriptEditor_AppendScriptLinesEvent asle && asle.Lines.Count > 0) + if (obj is IDE.ScriptEditor_AppendScriptEvent asle && asle.Result.HasContent) { - AppendScriptLines(asle.Lines); + AppendScript(asle.Result.GameFlowScript); EndSilentScriptAction(cachedTab, true, !wasScriptFileFileChanged, !wasScriptFileAlreadyOpened); } else if (obj is IDE.ScriptEditor_ScriptPresenceCheckEvent scrpce) @@ -93,13 +92,13 @@ private void IDEEvent_HandleSilentActions(IIDEEvent obj) } } - private void AppendScriptLines(List inputLines) + private void AppendScript(string scriptText) { EditorTabControl.OpenFile(PathHelper.GetScriptFilePath(ScriptRootDirectoryPath, TombLib.LevelData.TRVersion.Game.TR2)); if (CurrentEditor is TextEditorBase editor) { - editor.AppendText(string.Join(Environment.NewLine, inputLines) + Environment.NewLine); + editor.AppendText(Environment.NewLine +scriptText + Environment.NewLine); editor.ScrollToLine(editor.LineCount); } } diff --git a/TombIDE/TombIDE.ScriptingStudio/LuaStudio.cs b/TombIDE/TombIDE.ScriptingStudio/LuaStudio.cs index 80c76e44a..efe1e2b19 100644 --- a/TombIDE/TombIDE.ScriptingStudio/LuaStudio.cs +++ b/TombIDE/TombIDE.ScriptingStudio/LuaStudio.cs @@ -58,7 +58,7 @@ protected override void OnIDEEventRaised(IIDEEvent obj) } private bool IsSilentAction(IIDEEvent obj) - => obj is IDE.ScriptEditor_AppendScriptLinesEvent + => obj is IDE.ScriptEditor_AppendScriptEvent || obj is IDE.ScriptEditor_ScriptPresenceCheckEvent || obj is IDE.ScriptEditor_StringPresenceCheckEvent || obj is IDE.ScriptEditor_RenameLevelEvent; @@ -77,9 +77,9 @@ private void IDEEvent_HandleSilentActions(IIDEEvent obj) bool wasLanguageFileAlreadyOpened = languageFileTab != null; bool wasLanguageFileFileChanged = wasLanguageFileAlreadyOpened && EditorTabControl.GetEditorOfTab(languageFileTab).IsContentChanged; - if (obj is IDE.ScriptEditor_AppendScriptLinesEvent asle && asle.Lines.Count > 0) + if (obj is IDE.ScriptEditor_AppendScriptEvent asle && asle.Result.HasContent) { - AppendScriptLines(asle.Lines, + AppendScript(asle.Result, wasScriptFileAlreadyOpened, wasScriptFileFileChanged, wasLanguageFileAlreadyOpened, wasLanguageFileFileChanged); @@ -106,113 +106,118 @@ private void IDEEvent_HandleSilentActions(IIDEEvent obj) } } - private void AppendScriptLines(List inputLines, + private void AppendScript(ScriptGenerationResult result, bool wasScriptFileAlreadyOpened, bool wasScriptFileFileChanged, bool wasLanguageFileAlreadyOpened, bool wasLanguageFileFileChanged) { - try // !!! This needs some heavy refactoring !!! + try { - EditorTabControl.OpenFile(PathHelper.GetScriptFilePath(ScriptRootDirectoryPath, TombLib.LevelData.TRVersion.Game.TombEngine)); - - if (CurrentEditor is TextEditorBase editor) + if (result.GameFlowScript.Length > 0) { - editor.AppendText(string.Join(Environment.NewLine, inputLines.Take(10)) + Environment.NewLine); - editor.ScrollToLine(editor.LineCount); + EditorTabControl.OpenFile(PathHelper.GetScriptFilePath(ScriptRootDirectoryPath, TombLib.LevelData.TRVersion.Game.TombEngine)); - if (!wasScriptFileFileChanged) - EditorTabControl.SaveFile(EditorTabControl.SelectedTab); + if (CurrentEditor is TextEditorBase editor) + { + editor.AppendText(Environment.NewLine + result.GameFlowScript + Environment.NewLine); + editor.ScrollToLine(editor.LineCount); - if (!wasScriptFileAlreadyOpened) - EditorTabControl.TabPages.Remove(EditorTabControl.SelectedTab); - } + if (!wasScriptFileFileChanged) + EditorTabControl.SaveFile(EditorTabControl.SelectedTab); - EditorTabControl.OpenFile(PathHelper.GetLanguageFilePath(ScriptRootDirectoryPath, TombLib.LevelData.TRVersion.Game.TombEngine), EditorType.Text); + if (!wasScriptFileAlreadyOpened) + EditorTabControl.TabPages.Remove(EditorTabControl.SelectedTab); + } + } - if (CurrentEditor is TextEditorBase stringsEditor) + if (result.LanguageScript.Length > 0) { - var regex = new Regex(@"TEN\.Flow\.SetStrings\((.*)\)", RegexOptions.IgnoreCase); - var collectionNameLine = stringsEditor.Document.Lines.FirstOrDefault(line => regex.IsMatch(stringsEditor.Document.GetText(line).Replace(" ", string.Empty))); + EditorTabControl.OpenFile(PathHelper.GetLanguageFilePath(ScriptRootDirectoryPath, TombLib.LevelData.TRVersion.Game.TombEngine), EditorType.Text); - if (collectionNameLine == null) - return; + if (CurrentEditor is TextEditorBase stringsEditor) + { + var regex = new Regex(@"TEN\.Flow\.SetStrings\((.*)\)", RegexOptions.IgnoreCase); + var collectionNameLine = stringsEditor.Document.Lines.FirstOrDefault(line => regex.IsMatch(stringsEditor.Document.GetText(line).Replace(" ", string.Empty))); - string stringsVariableName = regex.Match(stringsEditor.Document.GetText(collectionNameLine)).Groups[1].Value; + if (collectionNameLine == null) + return; - regex = new Regex(@"local\s+" + Regex.Escape(stringsVariableName) + @"\s*="); - var stringsStartLine = stringsEditor.Document.Lines.FirstOrDefault(line => regex.IsMatch(stringsEditor.Document.GetText(line))); + string stringsVariableName = regex.Match(stringsEditor.Document.GetText(collectionNameLine)).Groups[1].Value; - if (stringsStartLine == null) - return; + regex = new Regex(@"local\s+" + Regex.Escape(stringsVariableName) + @"\s*="); + var stringsStartLine = stringsEditor.Document.Lines.FirstOrDefault(line => regex.IsMatch(stringsEditor.Document.GetText(line))); - var openingBrackets = new Stack(); - DocumentLine stopLine = null; + if (stringsStartLine == null) + return; - foreach (DocumentLine line in stringsEditor.Document.Lines) - { - string lineText = stringsEditor.Document.GetText(line); + var openingBrackets = new Stack(); + DocumentLine stopLine = null; - if (!lineText.Contains('{') && !lineText.Contains('}')) - continue; + foreach (DocumentLine line in stringsEditor.Document.Lines) + { + string lineText = stringsEditor.Document.GetText(line); - foreach (char opener in lineText.Where(c => c == '{')) - openingBrackets.Push(opener); + if (!lineText.Contains('{') && !lineText.Contains('}')) + continue; - foreach (char closer in lineText.Where(c => c == '}')) - openingBrackets.Pop(); + foreach (char opener in lineText.Where(c => c == '{')) + openingBrackets.Push(opener); - if (openingBrackets.Count == 0) - { - stopLine = line; - break; + foreach (char closer in lineText.Where(c => c == '}')) + openingBrackets.Pop(); + + if (openingBrackets.Count == 0) + { + stopLine = line; + break; + } } - } - if (stopLine == null || !stringsEditor.Document.GetText(stopLine).Trim().Equals("}")) - return; + if (stopLine == null || !stringsEditor.Document.GetText(stopLine).Trim().Equals("}")) + return; - for (int i = stopLine.LineNumber - 1; i > 0; i--) - { - DocumentLine line = stringsEditor.Document.GetLineByNumber(i); - string lineText = stringsEditor.Document.GetText(line); + for (int i = stopLine.LineNumber - 1; i > 0; i--) + { + DocumentLine line = stringsEditor.Document.GetLineByNumber(i); + string lineText = stringsEditor.Document.GetText(line); - string cleanLine = Regex.Replace(lineText, "--.*$", string.Empty).TrimEnd(); + string cleanLine = Regex.Replace(lineText, "--.*$", string.Empty).TrimEnd(); - if (cleanLine.EndsWith("}") || cleanLine.EndsWith("},")) - { - stringsEditor.Select(line.EndOffset, 0); + if (cleanLine.EndsWith("}") || cleanLine.EndsWith("},")) + { + stringsEditor.Select(line.EndOffset, 0); - if (cleanLine.EndsWith("}")) - stringsEditor.SelectedText += ","; + if (cleanLine.EndsWith("}")) + stringsEditor.SelectedText += ","; - stringsEditor.SelectedText += Environment.NewLine; + stringsEditor.SelectedText += Environment.NewLine; - stringsEditor.SelectedText += string.Join(Environment.NewLine, inputLines.Skip(10)); - stringsEditor.ResetSelectionAt(line.LineNumber + 1); - stringsEditor.ScrollToLine(line.LineNumber + 1); + stringsEditor.SelectedText += result.LanguageScript; + stringsEditor.ResetSelectionAt(line.LineNumber + 1); + stringsEditor.ScrollToLine(line.LineNumber + 1); - break; + break; + } } - } - if (!wasLanguageFileFileChanged) - EditorTabControl.SaveFile(EditorTabControl.SelectedTab); + if (!wasLanguageFileFileChanged) + EditorTabControl.SaveFile(EditorTabControl.SelectedTab); - if (!wasLanguageFileAlreadyOpened) - EditorTabControl.TabPages.Remove(EditorTabControl.SelectedTab); + if (!wasLanguageFileAlreadyOpened) + EditorTabControl.TabPages.Remove(EditorTabControl.SelectedTab); + } } - string dataName = inputLines[0].Trim().Remove(0, 3).Replace(" level", string.Empty); - string filePath = Path.Combine(ScriptRootDirectoryPath, "Levels", dataName + ".lua"); - - File.WriteAllText(filePath, - $"-- FILE: Levels\\{dataName}.lua\n\n" + - "LevelFuncs.OnLoad = function() end\n" + - "LevelFuncs.OnSave = function() end\n" + - "LevelFuncs.OnStart = function() end\n" + - "LevelFuncs.OnLoop = function() end\n" + - "LevelFuncs.OnEnd = function() end\n" + - "LevelFuncs.OnUseItem = function() end\n" + - "LevelFuncs.OnFreeze = function() end\n"); + // Create any new script files specified by the generation result + foreach (GeneratedScriptFile file in result.FilesToCreate) + { + string filePath = Path.Combine(ScriptRootDirectoryPath, file.RelativePath); + string directory = Path.GetDirectoryName(filePath); + + if (directory is not null && !Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + File.WriteAllText(filePath, file.Content); + } } catch { diff --git a/TombIDE/TombIDE.Shared/IDE.cs b/TombIDE/TombIDE.Shared/IDE.cs index 0b13cc6a8..18d83bfdc 100644 --- a/TombIDE/TombIDE.Shared/IDE.cs +++ b/TombIDE/TombIDE.Shared/IDE.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using TombIDE.Shared.NewStructure; +using TombIDE.Shared.SharedClasses; namespace TombIDE.Shared { @@ -157,20 +158,20 @@ public class PRJ2FileDeletedEvent : IIDEEvent /* Script Editor Events */ - #region ScriptEditor_AppendScriptLines + #region ScriptEditor_AppendScript - public class ScriptEditor_AppendScriptLinesEvent : IIDEEvent + public class ScriptEditor_AppendScriptEvent : IIDEEvent { - public List Lines { get; internal set; } + public ScriptGenerationResult Result { get; internal set; } } /// /// Sends a request to the Script Editor to append new lines of code at the end of the main script file. /// - public void ScriptEditor_AppendScriptLines(List lines) => - RaiseEvent(new ScriptEditor_AppendScriptLinesEvent { Lines = lines }); + public void ScriptEditor_AppendScript(ScriptGenerationResult result) => + RaiseEvent(new ScriptEditor_AppendScriptEvent { Result = result }); - #endregion ScriptEditor_AppendScriptLines + #endregion ScriptEditor_AppendScript #region ScriptEditor_AddNewLevelString diff --git a/TombIDE/TombIDE.Shared/IProgressReportingForm.cs b/TombIDE/TombIDE.Shared/IProgressReportingForm.cs new file mode 100644 index 000000000..5729403ea --- /dev/null +++ b/TombIDE/TombIDE.Shared/IProgressReportingForm.cs @@ -0,0 +1,20 @@ +namespace TombIDE.Shared; + +/// +/// Represents a form that can report progress updates. +/// +public interface IProgressReportingForm +{ + /// + /// Increments the current progress value by the specified amount. + /// + /// The value to increment the progress by. + void IncrementProgress(int value); + + /// + /// Sets the total progress value that represents 100% completion. For example, if you have 10 files to process, + /// you would call SetTotalProgress(10) at the start, and then call IncrementProgress(1) after processing each file. + /// + /// The total progress value. + void SetTotalProgress(int total); +} diff --git a/TombIDE/TombIDE.Shared/NewStructure/Prj2Helper.cs b/TombIDE/TombIDE.Shared/NewStructure/Prj2Helper.cs index 9c918d304..82ee4f3c0 100644 --- a/TombIDE/TombIDE.Shared/NewStructure/Prj2Helper.cs +++ b/TombIDE/TombIDE.Shared/NewStructure/Prj2Helper.cs @@ -1,10 +1,26 @@ -using System; +#nullable enable + +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading; +using TombLib.LevelData; +using TombLib.LevelData.IO; namespace TombIDE.Shared.NewStructure { + /// + /// Provides helper methods for working with .prj2 (Tomb Editor project) files. + /// public static class Prj2Helper { + /// + /// Determines whether a .prj2 file is a backup file by checking its name for backup suffixes + /// or embedded date patterns. + /// + /// The full path to the .prj2 file. + /// if the file appears to be a backup; otherwise, . public static bool IsBackupFile(string filePath) { try @@ -26,5 +42,72 @@ public static bool IsBackupFile(string filePath) return false; } + + /// + /// Returns all non-backup .prj2 files in the specified directory. + /// + /// The directory to scan. + /// + /// Whether to search only the top directory or all subdirectories. + /// Defaults to . + /// + /// A list of full paths to valid (non-backup) .prj2 files, or an empty list if the directory doesn't exist. + public static IReadOnlyList GetValidFiles(string directoryPath, SearchOption searchOption = SearchOption.TopDirectoryOnly) + { + if (!Directory.Exists(directoryPath)) + return Array.Empty(); + + return Directory.GetFiles(directoryPath, "*.prj2", searchOption) + .Where(file => !IsBackupFile(file)) + .ToList(); + } + + /// + /// Loads a .prj2 file and updates its game settings to match the target project configuration. + /// + /// This updates the game directory, executable path, script directory, game version, + /// and optionally the output data file path. It then saves the modified file back to disk. + /// + /// + /// The full path to the .prj2 file to update. + /// The target project whose settings should be applied. + /// + /// Optional custom data file name (without extension). If or empty, + /// the existing data file name is preserved and only its directory path is updated. + /// + public static void UpdateGameSettings(string prj2FilePath, IGameProject destProject, string? dataFileName = null) + { + Level level = Prj2Loader.LoadFromPrj2(prj2FilePath, null, CancellationToken.None, new Prj2Loader.Settings()); + + string exeFilePath = destProject.GetEngineExecutableFilePath(); + string engineDirectory = destProject.GetEngineRootDirectoryPath(); + + level.Settings.LevelFilePath = prj2FilePath; + + level.Settings.GameDirectory = level.Settings.MakeRelative(engineDirectory, VariableType.LevelDirectory); + level.Settings.GameExecutableFilePath = level.Settings.MakeRelative(exeFilePath, VariableType.LevelDirectory); + level.Settings.ScriptDirectory = level.Settings.MakeRelative(destProject.GetScriptRootDirectory(), VariableType.LevelDirectory); + level.Settings.GameVersion = destProject.GameVersion is TRVersion.Game.TR1 ? TRVersion.Game.TR1X : destProject.GameVersion; + + if (string.IsNullOrWhiteSpace(dataFileName)) + { + string? fileName = Path.GetFileName(level.Settings.GameLevelFilePath); + + if (fileName is not null) + { + string filePath = Path.Combine(engineDirectory, "data", fileName); + level.Settings.GameLevelFilePath = level.Settings.MakeRelative(filePath, VariableType.LevelDirectory); + } + } + else + { + string fileName = dataFileName + destProject.DataFileExtension; + string filePath = Path.Combine(engineDirectory, "data", fileName); + + level.Settings.GameLevelFilePath = level.Settings.MakeRelative(filePath, VariableType.LevelDirectory); + } + + Prj2Writer.SaveToPrj2(prj2FilePath, level); + } } } diff --git a/TombIDE/TombIDE.Shared/SharedClasses/GameVersionHelper.cs b/TombIDE/TombIDE.Shared/SharedClasses/GameVersionHelper.cs new file mode 100644 index 000000000..4930be0a7 --- /dev/null +++ b/TombIDE/TombIDE.Shared/SharedClasses/GameVersionHelper.cs @@ -0,0 +1,45 @@ +using TombIDE.Shared.NewStructure; +using TombLib.LevelData; + +namespace TombIDE.Shared.SharedClasses; + +/// +/// Provides helper methods for querying game-version-specific capabilities and defaults. +/// +public static class GameVersionHelper +{ + /// + /// Gets the default ambient sound ID for the given project's game version. + /// + /// The target game project. + /// The default ambient sound ID for the project's game version, or 0 if the game version is unrecognized. + public static int GetDefaultAmbientSoundId(IGameProject project) => project.GameVersion switch + { + TRVersion.Game.TR2 => 33, + TRVersion.Game.TR3 => 28, + + TRVersion.Game.TR4 + or TRVersion.Game.TRNG + or TRVersion.Game.TombEngine => 110, + + _ => 0 + }; + + /// + /// Determines whether automatic script generation is supported for the target project. + /// + /// The target game project. + /// if script generation is supported; otherwise, . + public static bool IsScriptGenerationSupported(IGameProject project) + => project.GameVersion is not TRVersion.Game.TR1 + and not TRVersion.Game.TR1X + and not TRVersion.Game.TR2X; + + /// + /// Determines whether the horizon setting is available for the target project. + /// + /// The target game project. + /// if the horizon setting is available; otherwise, . + public static bool IsHorizonSettingAvailable(IGameProject project) + => project.GameVersion is TRVersion.Game.TR4 or TRVersion.Game.TRNG or TRVersion.Game.TombEngine; +} diff --git a/TombIDE/TombIDE.Shared/SharedClasses/LevelHandling.cs b/TombIDE/TombIDE.Shared/SharedClasses/LevelHandling.cs deleted file mode 100644 index 669cc15ba..000000000 --- a/TombIDE/TombIDE.Shared/SharedClasses/LevelHandling.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using TombIDE.Shared.NewStructure; -using TombLib.LevelData; -using TombLib.LevelData.IO; - -namespace TombIDE.Shared.SharedClasses -{ - public static class LevelHandling - { - public static void UpdatePrj2GameSettings(string prj2FilePath, IGameProject destProject, string dataFileName = null) - { - Level level = Prj2Loader.LoadFromPrj2(prj2FilePath, null, System.Threading.CancellationToken.None, new Prj2Loader.Settings()); - - string exeFilePath = destProject.GetEngineExecutableFilePath(); - string engineDirectory = destProject.GetEngineRootDirectoryPath(); - - level.Settings.LevelFilePath = prj2FilePath; - - level.Settings.GameDirectory = level.Settings.MakeRelative(engineDirectory, VariableType.LevelDirectory); - level.Settings.GameExecutableFilePath = level.Settings.MakeRelative(exeFilePath, VariableType.LevelDirectory); - level.Settings.ScriptDirectory = level.Settings.MakeRelative(destProject.GetScriptRootDirectory(), VariableType.LevelDirectory); - level.Settings.GameVersion = destProject.GameVersion is TRVersion.Game.TR1 ? TRVersion.Game.TR1X : destProject.GameVersion; // Map TR1 to TR1X - we never supported vanilla TR1 in TombIDE - - if (string.IsNullOrWhiteSpace(dataFileName)) - { - string fileName = Path.GetFileName(level.Settings.GameLevelFilePath); - - if (fileName is not null) - { - string filePath = Path.Combine(engineDirectory, "data", fileName); - level.Settings.GameLevelFilePath = level.Settings.MakeRelative(filePath, VariableType.LevelDirectory); - } - } - else - { - string fileName = dataFileName + destProject.DataFileExtension; - string filePath = Path.Combine(engineDirectory, "data", fileName); - - level.Settings.GameLevelFilePath = level.Settings.MakeRelative(filePath, VariableType.LevelDirectory); - } - - Prj2Writer.SaveToPrj2(prj2FilePath, level); - } - - public static List GetValidPrj2FilesFromDirectory(string directoryPath) - { - List validPrj2Files = new List(); - - foreach (string file in Directory.GetFiles(directoryPath, "*.prj2", SearchOption.AllDirectories)) - { - if (!Prj2Helper.IsBackupFile(file)) - validPrj2Files.Add(file); - } - - return validPrj2Files; - } - - public static List GenerateScriptLines(string levelName, string dataFileName, TRVersion.Game gameVersion, int ambientSoundID, bool horizon = false) - { - if (gameVersion == TRVersion.Game.TR2 || gameVersion == TRVersion.Game.TR3) - { - return new List - { - "\nLEVEL: " + levelName, - "", - " LOAD_PIC: " + "pix\\" + (gameVersion == TRVersion.Game.TR2 ? "mansion.pcx" : "house.bmp"), - " TRACK: " + ambientSoundID, - " GAME: data\\" + dataFileName.ToLower() + ".tr2", - " COMPLETE:", - "", - "END:" - }; - } - else if (gameVersion == TRVersion.Game.TR4 || gameVersion == TRVersion.Game.TRNG) - { - return new List - { - "\n[Level]", - "Name= " + levelName, - "Level= DATA\\" + dataFileName.ToUpper() + ", " + ambientSoundID, - "LoadCamera= 0, 0, 0, 0, 0, 0, 0", - "Horizon= " + (horizon ? "ENABLED" : "DISABLED") - }; - } - else if (gameVersion == TRVersion.Game.TombEngine) - { - return new List - { - $"\n-- {dataFileName} level\n", - $"{dataFileName} = TEN.Flow.Level()\n", - $"{dataFileName}.nameKey = \"{dataFileName}\"", - $"{dataFileName}.scriptFile = \"Scripts\\\\Levels\\\\{dataFileName}.lua\"", - $"{dataFileName}.ambientTrack = \"{ambientSoundID}\"", - $"{dataFileName}.horizon1.enabled = " + (horizon ? "true" : "false"), - $"{dataFileName}.levelFile = \"Data\\\\{dataFileName}.ten\"", - $"{dataFileName}.loadScreenFile = \"Screens\\\\loading.png\"\n", - $"TEN.Flow.AddLevel({dataFileName})\n", - "--------------------------------------------------", - $" {dataFileName} = {{ \"{levelName}\" }}" - }; - } - - return new List(); - } - - public static string RemoveIllegalNameSymbols(string levelName) - { - char[] illegalNameChars = { ';', '[', ']', '=', ',', '.', '!' }; - return illegalNameChars.Aggregate(levelName, (current, c) => current.Replace(c.ToString(), string.Empty)); - } - - public static string MakeValidVariableName(string levelName) - { - char[] illegalNameChars = { ' ', ';', ':', '(', ')', '[', ']', '{', '}', '<', '>', '=', ',', '.', '!', '-', '+', '*', '?', '/', '\\', '\"', '\'', '&', '%', '#', '@', '|', '^', '`', '~', '$' }; - string result = illegalNameChars.Aggregate(levelName.Trim(), (current, c) => current.Replace(c.ToString(), "_")); - - if (char.IsDigit(result.FirstOrDefault())) - result = "_" + result; - - // Reduce the amount of '_' chars - while (result.Contains("__")) - result = result.Replace("__", "_"); - - return result; - } - } -} diff --git a/TombIDE/TombIDE.Shared/SharedClasses/LevelNameHelper.cs b/TombIDE/TombIDE.Shared/SharedClasses/LevelNameHelper.cs new file mode 100644 index 000000000..54ab73ea4 --- /dev/null +++ b/TombIDE/TombIDE.Shared/SharedClasses/LevelNameHelper.cs @@ -0,0 +1,92 @@ +#nullable enable + +using System.Linq; + +namespace TombIDE.Shared.SharedClasses; + +/// +/// Provides helper methods for validating and sanitizing level names and data file names. +/// +public static class LevelNameHelper +{ + /// + /// Characters that are not allowed in level names. + /// + private static readonly char[] IllegalNameChars = { ';', '[', ']', '=', ',', '.', '!' }; + + /// + /// Characters that are not allowed in variable-style names (used for data file names). + /// + private static readonly char[] IllegalVariableChars = + { + ' ', ';', ':', '(', ')', '[', ']', '{', '}', '<', '>', '=', ',', '.', '!', + '-', '+', '*', '?', '/', '\\', '"', '\'', '&', '%', '#', '@', '|', '^', '`', '~', '$' + }; + + /// + /// Validates and sanitizes a level name by removing invalid path and name characters. + /// + /// The raw level name to validate. + /// The sanitized level name, or if the result is empty or whitespace. + public static string? ValidateLevelName(string levelName) + { + string sanitized = PathHelper.RemoveIllegalPathSymbols(levelName.Trim()); + sanitized = RemoveIllegalNameSymbols(sanitized); + + return string.IsNullOrWhiteSpace(sanitized) ? null : sanitized; + } + + /// + /// Validates and sanitizes a data file name by converting it to a valid variable-style name. + /// + /// The raw data file name to validate. + /// The sanitized data file name, or if the result is empty or whitespace. + public static string? ValidateDataFileName(string dataFileName) + { + string sanitized = MakeValidVariableName(dataFileName.Trim()); + return string.IsNullOrWhiteSpace(sanitized) ? null : sanitized; + } + + /// + /// Converts a level name to a suggested data file name by replacing spaces with underscores. + /// + /// The level name to convert. + /// A suggested data file name (e.g. "My Level" becomes "My_Level"). + public static string SuggestDataFileName(string levelName) + => levelName.Trim().Replace(' ', '_'); + + /// + /// Removes characters that are illegal in level names. + /// + /// These include semicolons, brackets, equals, commas, dots, and exclamation marks, + /// which would conflict with classic Tomb Raider script file syntax. + /// + /// + /// The level name to sanitize. + /// The level name with illegal characters removed. + public static string RemoveIllegalNameSymbols(string levelName) + => IllegalNameChars.Aggregate(levelName, (current, c) => current.Replace(c.ToString(), string.Empty)); + + /// + /// Converts a string into a valid variable-style name suitable for use as a data file name. + /// + /// Replaces special characters with underscores, prefixes with an underscore if the name + /// starts with a digit, and collapses consecutive underscores into a single one. + /// + /// + /// The name to convert. + /// A valid variable-style name. + public static string MakeValidVariableName(string name) + { + string result = IllegalVariableChars.Aggregate(name.Trim(), (current, c) => current.Replace(c.ToString(), "_")); + + if (char.IsDigit(result.FirstOrDefault())) + result = "_" + result; + + // Collapse consecutive underscores + while (result.Contains("__")) + result = result.Replace("__", "_"); + + return result; + } +} diff --git a/TombIDE/TombIDE.Shared/SharedClasses/ScriptGenerationResult.cs b/TombIDE/TombIDE.Shared/SharedClasses/ScriptGenerationResult.cs new file mode 100644 index 000000000..cd55d6dc9 --- /dev/null +++ b/TombIDE/TombIDE.Shared/SharedClasses/ScriptGenerationResult.cs @@ -0,0 +1,41 @@ +#nullable enable + +using System.Collections.Generic; + +namespace TombIDE.Shared.SharedClasses; + +/// +/// Represents a file that should be created as part of script generation. +/// +/// The path relative to the script root directory (e.g., "Levels\MyLevel.lua"). +/// The full text content of the file. +public sealed record GeneratedScriptFile(string RelativePath, string Content); + +/// +/// Contains the results of script generation, with explicit separation between +/// gameflow script content and language file content. +/// +/// The data file name (without extension) used to generate the script. +public sealed record ScriptGenerationResult(string DataFileName) +{ + /// + /// Text to append to the main gameflow/script file. + /// + public string GameFlowScript { get; init; } = string.Empty; + + /// + /// Text to insert into the language/strings file (e.g., TombEngine's Strings.lua). + /// Empty for game versions that handle language strings via a separate mechanism. + /// + public string LanguageScript { get; init; } = string.Empty; + + /// + /// New script files that should be created on disk, relative to the script root directory. + /// + public IReadOnlyList FilesToCreate { get; init; } = []; + + /// + /// Whether any script content was generated. + /// + public bool HasContent => GameFlowScript.Length > 0 || LanguageScript.Length > 0; +} diff --git a/TombIDE/TombIDE.Shared/SharedClasses/ScriptGenerator.cs b/TombIDE/TombIDE.Shared/SharedClasses/ScriptGenerator.cs new file mode 100644 index 000000000..e449c4eb4 --- /dev/null +++ b/TombIDE/TombIDE.Shared/SharedClasses/ScriptGenerator.cs @@ -0,0 +1,172 @@ +#nullable enable + +using System.IO; +using TombLib.LevelData; + +namespace TombIDE.Shared.SharedClasses; + +/// +/// Generates game-version-specific scripts for registering levels in game script files. +/// +public static class ScriptGenerator +{ + /// + /// Generates the scripts needed to register a level in a game's script system. + /// + /// The display name of the level. + /// The data file name (without extension). + /// The target game version. + /// The ambient sound or track ID. + /// Whether to enable the horizon effect (TR4/TRNG/TombEngine only). + /// + /// A containing the generated scripts and any additional files to create, + /// or if the game version is unsupported. + /// + public static ScriptGenerationResult? GenerateScripts(string levelName, string dataFileName, TRVersion.Game gameVersion, int ambientSoundId, bool horizon = false) + { + return gameVersion switch + { + // TODO: Implement script insertion for TRX + // TRVersion.Game.TR1 or TRVersion.Game.TR1X => GenerateTRXScript(levelName, dataFileName, ambientSoundId, "phd"), + // TRVersion.Game.TR2X => GenerateTRXScript(levelName, dataFileName, ambientSoundId, "tr2"), + + TRVersion.Game.TR2 => new ScriptGenerationResult(dataFileName) + { + GameFlowScript = GenerateTR2Script(levelName, dataFileName, ambientSoundId) + }, + + TRVersion.Game.TR3 => new ScriptGenerationResult(dataFileName) + { + GameFlowScript = GenerateTR3Script(levelName, dataFileName, ambientSoundId) + }, + + TRVersion.Game.TR4 or TRVersion.Game.TRNG => new ScriptGenerationResult(dataFileName) + { + GameFlowScript = GenerateTR4Script(levelName, dataFileName, ambientSoundId, horizon) + }, + + TRVersion.Game.TombEngine => GenerateTombEngineScript(levelName, dataFileName, ambientSoundId, horizon), + _ => null + }; + } + + private static ScriptGenerationResult GenerateTRXScript(string levelName, string dataFileName, int ambientSoundId, string fileExtension) + { + string gameFlowScript = $$""" + // {{levelName}} + { + "path": "data/{{dataFileName.ToLowerInvariant()}}.{{fileExtension}}", + "music_track": {{ambientSoundId}}, + "sequence": [ + {"type": "loading_screen", "path": "data/images/loading.webp", "fade_in_time": 1.0, "fade_out_time": 1.0}, + {"type": "loop_game"}, + {"type": "play_music", "music_track": 37}, + {"type": "level_stats"}, + {"type": "level_complete"}, + ], + }, + """; + + string languageScript = $$""" + { + "title": "{{levelName}}", + }, + """; + + return new ScriptGenerationResult(dataFileName) + { + GameFlowScript = gameFlowScript, + LanguageScript = languageScript + }; + } + + private static string GenerateTR2Script(string levelName, string dataFileName, int ambientSoundId) + { + return $$""" + LEVEL: {{levelName}} + + LOAD_PIC: pix\mansion.pcx + TRACK: {{ambientSoundId}} + GAME: data\{{dataFileName.ToLowerInvariant()}}.tr2 + COMPLETE: + + END: + """; + } + + private static string GenerateTR3Script(string levelName, string dataFileName, int ambientSoundId) + { + return $$""" + LEVEL: {{levelName}} + + LOAD_PIC: pix\house.bmp + TRACK: {{ambientSoundId}} + GAME: data\{{dataFileName.ToLowerInvariant()}}.tr2 + COMPLETE: + + END: + """; + } + + private static string GenerateTR4Script(string levelName, string dataFileName, int ambientSoundId, bool horizon) + { + string horizonValue = horizon ? "ENABLED" : "DISABLED"; + + return $$""" + [Level] + Name= {{levelName}} + Level= DATA\{{dataFileName.ToUpperInvariant()}}, {{ambientSoundId}} + LoadCamera= 0, 0, 0, 0, 0, 0, 0 + Horizon= {{horizonValue}} + """; + } + + private static ScriptGenerationResult GenerateTombEngineScript(string levelName, string dataFileName, int ambientSoundId, bool horizon) + { + string horizonValue = horizon ? "true" : "false"; + + string gameFlowScript = $$""" + -- {{dataFileName}} level + + {{dataFileName}} = TEN.Flow.Level() + + {{dataFileName}}.nameKey = "{{dataFileName}}" + {{dataFileName}}.scriptFile = "Scripts\\Levels\\{{dataFileName}}.lua" + {{dataFileName}}.ambientTrack = "{{ambientSoundId}}" + {{dataFileName}}.horizon1.enabled = {{horizonValue}} + {{dataFileName}}.levelFile = "Data\\{{dataFileName}}.ten" + {{dataFileName}}.loadScreenFile = "Screens\\loading.png" + + TEN.Flow.AddLevel({{dataFileName}}) + """; + + string languageScript = $$""" + {{dataFileName}} = { "{{levelName}}" } + """; + + string levelScript = $$""" + -- FILE: Levels\{{dataFileName}}.lua + + LevelFuncs.OnLoad = function() end + LevelFuncs.OnSave = function() end + LevelFuncs.OnStart = function() end + LevelFuncs.OnLoop = function() end + LevelFuncs.OnEnd = function() end + LevelFuncs.OnUseItem = function() end + LevelFuncs.OnFreeze = function() end + + """; + + return new ScriptGenerationResult(dataFileName) + { + GameFlowScript = gameFlowScript, + LanguageScript = languageScript, + FilesToCreate = + [ + new GeneratedScriptFile( + RelativePath: Path.Combine("Levels", dataFileName + ".lua"), + Content: levelScript) + ] + }; + } +} diff --git a/TombIDE/TombIDE.Shared/SharedForms/FormLoading.cs b/TombIDE/TombIDE.Shared/SharedForms/FormLoading.cs index 18108e778..6830d046e 100644 --- a/TombIDE/TombIDE.Shared/SharedForms/FormLoading.cs +++ b/TombIDE/TombIDE.Shared/SharedForms/FormLoading.cs @@ -5,7 +5,6 @@ using System.Windows.Forms; using TombIDE.Shared.NewStructure; using TombIDE.Shared.NewStructure.Implementations; -using TombIDE.Shared.SharedClasses; namespace TombIDE.Shared.SharedForms { @@ -122,7 +121,7 @@ private void UpdateAllPrj2GameSettings(ILevelProject levelProject) foreach (string filePath in Directory.GetFiles(levelProject.DirectoryPath, "*.prj2", SearchOption.TopDirectoryOnly)) { if (!Prj2Helper.IsBackupFile(filePath)) - LevelHandling.UpdatePrj2GameSettings(filePath, _ide.Project); + Prj2Helper.UpdateGameSettings(filePath, _ide.Project); } } } diff --git a/TombIDE/TombIDE.Shared/TombIDE.Shared.csproj b/TombIDE/TombIDE.Shared/TombIDE.Shared.csproj index 0c41fa6cf..0db429bd7 100644 --- a/TombIDE/TombIDE.Shared/TombIDE.Shared.csproj +++ b/TombIDE/TombIDE.Shared/TombIDE.Shared.csproj @@ -1,6 +1,7 @@  net6.0-windows + 12 Library false true diff --git a/TombIDE/TombIDE/Forms/FormImportProject.cs b/TombIDE/TombIDE/Forms/FormImportProject.cs index 31355a451..be3bdbd0a 100644 --- a/TombIDE/TombIDE/Forms/FormImportProject.cs +++ b/TombIDE/TombIDE/Forms/FormImportProject.cs @@ -119,7 +119,7 @@ private void button_Import_Click(object sender, EventArgs e) if (Directory.Exists(levelsDirectoryPath)) { // Check if the directory contains non-backup .prj2 files - List prj2Files = LevelHandling.GetValidPrj2FilesFromDirectory(levelsDirectoryPath); + IReadOnlyList prj2Files = Prj2Helper.GetValidFiles(levelsDirectoryPath, SearchOption.AllDirectories); if (prj2Files.Count > 0) { diff --git a/TombLib/TombLib/Utils/TextExtensions.cs b/TombLib/TombLib/Utils/TextExtensions.cs index b2ed631b8..2fcab4dbc 100644 --- a/TombLib/TombLib/Utils/TextExtensions.cs +++ b/TombLib/TombLib/Utils/TextExtensions.cs @@ -3,7 +3,6 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; -using System.Windows.Forms; namespace TombLib.Utils { @@ -24,6 +23,11 @@ public static bool IsANSI(this string source) return regex.IsMatch(source); } + public static string[] SplitLines(this string source) + { + return source.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + } + public static string[] SplitParenthesis(this string source) { return Regex.Matches(source, @"[^{},]+|\{[^{}]*\}").Select(m => m.Value.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToArray();