diff --git a/Editor/AGS.Editor/AGSEditor.cs b/Editor/AGS.Editor/AGSEditor.cs index 576a123039c..9f34a34c471 100644 --- a/Editor/AGS.Editor/AGSEditor.cs +++ b/Editor/AGS.Editor/AGSEditor.cs @@ -177,6 +177,7 @@ public class AGSEditor : IAGSEditorDirectories public const int AGS_4_0_0_XML_VERSION_INDEX_PO_TRANSLATIONS = 3999907; public const int AGS_4_0_0_XML_VERSION_INDEX_COLORS_32BIT = 4000009; public const int AGS_4_0_0_XML_VERSION_INDEX_FONT_SOURCES = 4000010; + public const int AGS_4_0_0_XML_VERSION_INDEX_DIALOG_FILES = 4000027; /* * LATEST_XML_VERSION is the X.Y.Z.W string which defines project's user data format. diff --git a/Editor/AGS.Editor/AGSEditor.csproj b/Editor/AGS.Editor/AGSEditor.csproj index e544aaecf3d..5f8b71edd56 100644 --- a/Editor/AGS.Editor/AGSEditor.csproj +++ b/Editor/AGS.Editor/AGSEditor.csproj @@ -180,6 +180,7 @@ + diff --git a/Editor/AGS.Editor/Components/DialogsComponent.cs b/Editor/AGS.Editor/Components/DialogsComponent.cs index 0efdb6843fa..e6a86ae2503 100644 --- a/Editor/AGS.Editor/Components/DialogsComponent.cs +++ b/Editor/AGS.Editor/Components/DialogsComponent.cs @@ -1,12 +1,13 @@ +using AGS.Editor.TextProcessing; +using AGS.Types; using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Linq; using System.Text; using System.Windows.Forms; using System.Xml; -using AGS.Types; -using AGS.Editor.TextProcessing; namespace AGS.Editor.Components { @@ -32,7 +33,8 @@ public DialogsComponent(GUIController guiController, AGSEditor agsEditor) _guiController.ProjectTree.AddTreeRoot(this, TOP_LEVEL_COMMAND_ID, "Dialogs", ICON_KEY); _guiController.OnZoomToFile += GUIController_OnZoomToFile; _guiController.OnGetScriptEditorControl += _guiController_OnGetScriptEditorControl; - RePopulateTreeView(); + Factory.Events.GamePrepareUpgrade += Events_GamePrepareUpgrade; + RePopulateTreeView(); } private void _guiController_OnGetScriptEditorControl(GetScriptEditorControlEventArgs evArgs) @@ -55,9 +57,9 @@ protected override void ItemCommandClick(string controlID) { if (controlID == COMMAND_NEW_ITEM) { - Dialog newItem = new Dialog(); + string name = _agsEditor.GetFirstAvailableScriptName("dDialog"); + Dialog newItem = new Dialog(name); newItem.ID = _agsEditor.CurrentGame.RootDialogFolder.GetAllItemsCount(); - newItem.Name = _agsEditor.GetFirstAvailableScriptName("dDialog"); string newNodeID = AddSingleItem(newItem); _guiController.ProjectTree.SelectNode(this, newNodeID); ShowPaneForDialog(newItem); @@ -413,5 +415,74 @@ protected override IList GetFlatList() { return _agsEditor.CurrentGame.DialogFlatList; } + + + private void Events_GamePrepareUpgrade(UpgradeGameEventArgs args) + { + args.Tasks.Add(new UpgradeGameDialogsToIndividualFiles(ConvertAllDialogsToIndividualFiles)); + } + + private void ConvertAllDialogsToIndividualFiles(Game game, IWorkProgress progress, CompileMessages errors) + { + if (_agsEditor.CurrentGame.SavedXmlVersionIndex >= AGSEditor.AGS_4_0_0_XML_VERSION_INDEX_DIALOG_FILES) + return; // already converted + + // this logic is convoluted + // a person may have a Dialogs dir with notes or source files + // perhaps only move already existing .asd files? + // need to look carefully into this, to move only the contents that could conflict + if (Directory.Exists(DialogScript.DIALOGUES_DIR)) + { + string backupRootDir = Utilities.MakeUniqueDirectory(_agsEditor.CurrentGame.DirectoryPath, DialogScript.DIALOGUES_DIR, "Backup-"); + + Utilities.SafeMoveDirectoryFiles(DialogScript.DIALOGUES_DIR, backupRootDir); + } + + // I think I can just check here if the directory already exists + // then the only way this can fail is some weird permission setting from file system + try + { + if (!Directory.Exists(DialogScript.DIALOGUES_DIR)) + { + Directory.CreateDirectory(DialogScript.DIALOGUES_DIR); + } + } + catch (Exception e) + { + errors.Add(new CompileError($"Failed to create directory '{DialogScript.DIALOGUES_DIR}'.", e)); + // this probably needs a return but I don't know what a failure here means, is the project left in an intermediary state? + } + + // now we need to do the actual conversion, but how do I handle partial conversion? + // I guess this could only happen through power failure? perhaps cycle through all files again and do the save to disk later? + // I also think this issue means I still want a way to load the old stuff in Dialog in ags types... + + int totalCount = game.Dialogs.Count; + int convertedCount = 0; + + // ACTUALLY FOR THIS TO WORK I GUESS I NEED TO MOVE STUFF OUT OF THE CURRENT DIALOG LOADING? + // I HAVE NO IDEA HOW TO DO THIS + foreach (Dialog dialog in game.Dialogs) + { + try + { + string fileName = DialogScript.GetFileName(dialog.Name); + DialogScript script = new DialogScript(fileName, dialog.Script); + script.SaveToDisk(true); + convertedCount++; + } + catch (Exception e) + { + errors.Add(new CompileError($"Failed to convert dialog '{dialog.Name}'", e)); + } + } + + // I dont know if I should report these things at all or just a "Done!" message or nothing. + if (convertedCount != totalCount) + { + errors.Add(new CompileWarning($"Converted {convertedCount} of {totalCount} dialogs. Failed to convert {totalCount - convertedCount}.")); + } + + } } } diff --git a/Editor/AGS.Editor/Entities/UpgradeGame/UpgradeGameDialogsToIndividualFiles.cs b/Editor/AGS.Editor/Entities/UpgradeGame/UpgradeGameDialogsToIndividualFiles.cs new file mode 100644 index 00000000000..daadc4dc80e --- /dev/null +++ b/Editor/AGS.Editor/Entities/UpgradeGame/UpgradeGameDialogsToIndividualFiles.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using AGS.Types; + +namespace AGS.Editor +{ + public class UpgradeGameDialogsToIndividualFiles : IUpgradeGameTask + { + // TODO: revise this later. + // The conversion process is run via a delegate here, because at the time + // I was not certain if it should remain in DialogComponent or not. + // It sort of makes sense, conceptually. + // Also, the conversion calls a number of private methods from DialogComponent, + // therefore this seemed to be the most trivial way to proceed. + public delegate void ConvertDialogs(Game game, IWorkProgress progress, CompileMessages errors); + private ConvertDialogs _convertDialogs; + + public UpgradeGameDialogsToIndividualFiles(ConvertDialogs convertDialogs) + { + _convertDialogs = convertDialogs; + Enabled = true; + } + + /// + /// A unique string identifier of this upgrade task. + /// + public string ID { get { return "UpgradeGameDialogsToIndividualFiles"; } } + /// + /// An arbitrary title, used to identify this task when + /// presenting to a user. + /// + public string Title { get { return "Dialogs as Individual Files Outside Project"; } } + /// + /// An arbitrary description, may contain any amount of text. + /// + public string Description + { + get + { + return + "Dialog scripts are now stored as separate .asd files in the Dialogs directory. " + + "Previously these dialog scripts were part of the game project file instead." + + Environment.NewLine + Environment.NewLine + + "During this upgrade step AGS your dialogues will be extracted to a new " + DialogScript.DIALOGUES_DIR + " directory in your project."; + } + } + /// + /// A game project version that introduced this upgrade task. + /// If a loaded game has a less project version, then this task + /// must be applied, otherwise it should not. + /// Returns null if should be applied regardless of the game version + /// (but the execution process may still have version checks inside). + /// + public System.Version GameVersion { get { return new System.Version(AGSEditor.FIRST_XML_VERSION_USING_INDEX); } } + /// + /// A game project version in form of a numeric index, for the projects + /// which used these. + /// + public int? GameVersionIndex { get { return AGSEditor.AGS_4_0_0_XML_VERSION_INDEX_DIALOG_FILES; } } + /// + /// Tells whether this upgrade task is to be executed unconditionally, + /// without warning user about it. + /// + public bool Implicit { get { return false; } } + /// + /// Tells whether this upgrade task may be disabled by user's choice. + /// + public bool Optional { get { return false; } } + /// + /// Tells whether the upgrade process is allowed to continue if this + /// task had errors. + /// + public bool AllowToSkipIfHadErrors { get { return false; } } + /// + /// Tells whether user should be asked for a confirmation in order to + /// continue the upgrade process in case this task had errors. + /// + public bool RequestConfirmationOnErrors { get { return false; } } + + /// + /// Whether this task is enabled, otherwise should be skipped. + /// + public bool Enabled { get; set; } + + /// + /// Provides WizardPage controls used to represent this upgrade task. + /// The page implementation may have this IUpgradeGameTask passed into + /// constructor in order to assign settings right into it. + /// + public UpgradeGameWizardPage[] CreateWizardPages(Game game) + { + return new UpgradeGameWizardPage[] { new UpdateGameGenericInfoPage(game, this) }; + } + /// + /// Apply task options reading them from the dictionary of key-values. + /// + public void ApplyOptions(Dictionary options) + { + // does not have any options + } + /// + /// Execute the upgrade task over the given Game project. + /// Fills any errors or warnings into the provided "errors" collection. + /// + public void Execute(Game game, IWorkProgress progress, CompileMessages errors) + { + _convertDialogs(game, progress, errors); + } + } +} diff --git a/Editor/AGS.Native/agsnative.cpp b/Editor/AGS.Native/agsnative.cpp index 7172ca64b6d..637b5c57ff2 100644 --- a/Editor/AGS.Native/agsnative.cpp +++ b/Editor/AGS.Native/agsnative.cpp @@ -2832,7 +2832,7 @@ Game^ import_compiled_game_dta(const AGSString &filename) for (int i = 0; i < thisgame.numdialog; i++) { - AGS::Types::Dialog ^newDialog = gcnew AGS::Types::Dialog(); + AGS::Types::Dialog ^newDialog = gcnew AGS::Types::Dialog(AGS::Types::DialogScript::GetFileName(TextHelper::ConvertASCII(thisgame.dialogScriptNames[i]))); newDialog->ID = i; for (int j = 0; j < dialog[i].GetOptionCount(); j++) { diff --git a/Editor/AGS.Types/AGS.Types.csproj b/Editor/AGS.Types/AGS.Types.csproj index d73fa588888..bb24f0a7990 100644 --- a/Editor/AGS.Types/AGS.Types.csproj +++ b/Editor/AGS.Types/AGS.Types.csproj @@ -123,6 +123,7 @@ + diff --git a/Editor/AGS.Types/Dialog.cs b/Editor/AGS.Types/Dialog.cs index 74c6dac24b6..50075b7b0c9 100644 --- a/Editor/AGS.Types/Dialog.cs +++ b/Editor/AGS.Types/Dialog.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.IO; using System.Text; using System.Xml; using AGS.Types.Interfaces; @@ -13,17 +14,16 @@ public class Dialog : IScript, IToXml, IComparable private int _id; private string _name; private bool _showTextParser; - private string _script; + private DialogScript _script; private bool _scriptChangedSinceLastCompile; private string _cachedConvertedScript; private List _options = new List(); private CustomProperties _properties = new CustomProperties(CustomPropertyAppliesTo.Dialogs); - public Dialog() + public Dialog(string name) { - _script = "// Dialog script file" + Environment.NewLine + - "@S // Dialog startup entry point" + Environment.NewLine + - "return" + Environment.NewLine; + _name = Utilities.ValidateScriptName(name); + _script = DialogScript.CreateDefault(_name); _cachedConvertedScript = null; _scriptChangedSinceLastCompile = true; } @@ -35,7 +35,10 @@ public Dialog() public int ID { get { return _id; } - set { _id = value; } + set + { + _id = value; + } } [Description("The script name of the dialog")] @@ -44,14 +47,17 @@ public int ID public string Name { get { return _name; } - set { _name = Utilities.ValidateScriptName(value); } + set { + _name = Utilities.ValidateScriptName(value); + _script.FileName = DialogScript.GetFileName(_name); + } } [Browsable(false)] - public string FileName { get { return "Dialog " + ID; } } + public string FileName { get { return _script.FileName; } } [Browsable(false)] - public string Text { get { return _script; } } + public string Text { get { return _script.Text; } } [Browsable(false)] public ScriptAutoCompleteData AutoCompleteData { get { return null; } } @@ -67,14 +73,14 @@ public bool ShowTextParser [Browsable(false)] public string Script { - get { return _script; } + get { return _script.Text; } set { - if (_script != value) + if (_script.Text != value) { _scriptChangedSinceLastCompile = true; } - _script = value; + _script.Text = value; } } @@ -133,7 +139,27 @@ public Dialog(XmlNode node) _showTextParser = Boolean.Parse(SerializeUtils.GetElementString(node, "ShowTextParser")); XmlNode scriptNode = node.SelectSingleNode("Script"); // Luckily the CDATA section is easy to read back - _script = scriptNode.InnerText; + // FIX-ME: we will need to figure how to look the .asd file and then if it fails look into the inner text? + // or the reverse? Need to think on this + String fileName = DialogScript.GetFileName(_name); + + if (File.Exists(fileName)) + { + // read from .asd file + _script = new DialogScript(fileName, ""); + _script.LoadFromDisk(); + } + else if (!string.IsNullOrEmpty(scriptNode.InnerText)) + { + // try the CData? + _script = new DialogScript(fileName, scriptNode.InnerText); + } + else + { + // I don't think we should be here??? + _script = DialogScript.CreateDefault(fileName); + } + _script.FileName = fileName; foreach (XmlNode child in SerializeUtils.GetChildNodes(node, "DialogOptions")) { @@ -143,12 +169,19 @@ public Dialog(XmlNode node) public void ToXml(XmlTextWriter writer) { + // lets save the .asd file + _script.SaveToDisk(true); + writer.WriteStartElement("Dialog"); writer.WriteElementString("ID", ID.ToString()); writer.WriteElementString("Name", _name); writer.WriteElementString("ShowTextParser", _showTextParser.ToString()); writer.WriteStartElement("Script"); - writer.WriteCData(_script); + // Actually I am commenting this + // FIX-ME: move this out because the writing will be in a file by DialogScript. + // For now we keep this so things still work. + // writer.WriteCData(_script.Text); + writer.WriteCData(string.Empty); writer.WriteEndElement(); writer.WriteStartElement("DialogOptions"); diff --git a/Editor/AGS.Types/DialogScript.cs b/Editor/AGS.Types/DialogScript.cs new file mode 100644 index 00000000000..7a73cdf0c00 --- /dev/null +++ b/Editor/AGS.Types/DialogScript.cs @@ -0,0 +1,167 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Text; + +namespace AGS.Types +{ + public class DialogScript + { + private string _fileName; + private string _text = string.Empty; + private bool _modified = false; + // FIX-ME: this is not used yet but will be used by the file listener + // remove this comment once file listener is implemented + private bool _isBeingSaved = false; + private DateTime _lastSavedAt = DateTime.MinValue; + private static readonly string DEFAULT_NEW_DIALOG_SCRIPT = + "// Dialog script file" + Environment.NewLine + + "@S // Dialog startup entry point" + Environment.NewLine + + "return" + Environment.NewLine; + + public const string DIALOGUES_DIR = "Dialogs"; // Directory + public const string DIALOGUES_EXT = ".asd"; // Extension + + public static Encoding TextEncoding + { + get { return Script.TextEncoding; } + } + + public static string GetFileName(string dialog_name) + { + return Path.Combine(DIALOGUES_DIR, dialog_name + DIALOGUES_EXT); + } + + public static string GetFileName(Dialog dialog) + { + return Path.Combine(DIALOGUES_DIR, dialog.Name + DIALOGUES_EXT); + } + + public static DialogScript CreateDefault(string filename) + { + return new DialogScript(filename, DEFAULT_NEW_DIALOG_SCRIPT); + } + + /// + /// Creates a new Dialog Script which is the main part of a Dialog that is stored in a Dialog. + /// + /// The dialog script filename. + /// an underscore. + /// The script itself. + public DialogScript(string fileName, string text) + { + _fileName = fileName; + _text = text ?? string.Empty; + } + + [Browsable(false)] + public string Text + { + get { return _text; } + set + { + if (_text != value) + { + _text = value ?? string.Empty; + _modified = true; + } + } + } + + [ReadOnly(true)] + [Category("Setup")] + [Description("File name that the dialog script is stored in")] + public string FileName + { + get { return _fileName; } + set + { + string newFileName = value; + if (_fileName == newFileName) + return; + + // handle the file being renamed + if(!string.IsNullOrEmpty(_fileName) && + !string.IsNullOrEmpty(newFileName) && + File.Exists(_fileName)) + { + // lets guarantee the Dialogs directory exists + string dir = Path.GetDirectoryName(newFileName); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + File.Move(_fileName, newFileName); + } + + _fileName = newFileName; + } + } + + [Browsable(false)] + public bool Modified + { + get { return _modified; } + // set { _modified = value; } + } + + [Browsable(false)] + public DateTime LastSavedAt + { + get { return _lastSavedAt; } + } + + public void SaveToDisk() + { + SaveToDisk(false); + } + + public void SaveToDisk(bool force) + { + if (_modified || force) + { + _isBeingSaved = true; + + if (!Directory.Exists(Path.GetDirectoryName(_fileName))) + { + Directory.CreateDirectory(Path.GetDirectoryName(_fileName)); + } + + try + { + + byte[] bytes = TextEncoding.GetBytes(_text); + using (BinaryWriter binWriter = new BinaryWriter(File.Open(_fileName, FileMode.Create))) + { + binWriter.Write(bytes); + _lastSavedAt = DateTime.Now; + } + } + finally + { + _isBeingSaved = false; + } + _modified = false; + } + } + + public void LoadFromDisk() + { + try + { + using (BinaryReader reader = new BinaryReader(File.Open(_fileName, FileMode.Open, FileAccess.Read))) + { + byte[] bytes = reader.ReadBytes((int)reader.BaseStream.Length); + _text = TextEncoding.GetString(bytes) ?? string.Empty; + } + } + catch (Exception) + { + // TODO: add warning? would require changes to report system + _text = string.Empty; + } + _modified = false; + } + } +}