Page 1 of 1

.fountain support

Posted: Wed Jul 12, 2023 6:21 pm
by MattDan
Hi there,

I'm wondering if there's any way to import a .fountain file into a dialogue system conversation? Or is there no way to make this happen aside from copying and pasting dialogue into the dialogue editor?

Thanks,
Matt

Re: .fountain support

Posted: Wed Jul 12, 2023 8:20 pm
by Tony Li
Hi Matt,

If you're comfortable with a bit of scripting, make a custom importer. Duplicate the starter script ConverterWindowTemplate.cs and fill in code where the comments indicate to import your .fountain file.

Re: .fountain support

Posted: Sun Jul 23, 2023 7:38 pm
by MattDan
Hi Tony,

I Started working on a .fountain importer using CSVConverterWindow.cs as reference. My goal is to get the basic actor/conversant entries into a new conversation in order. I'll add things like sequences, variables, branching dialogue etc. in the dialogue editor after.

Here is the current state of my custom CopySourceToDialogueDatabase method. I keep getting non-specific "null reference exception" when trying to run this:

Code: Select all


//I decided to try making this work by adding a "#" in front of each actor's name in the screenplay software 
//(and exported fountain file). Then it removes the # from the actorName, and reads the next line down as 
//the dialogue text and assigns those to the new entry and adds each one to the new conversation.

protected override void CopySourceToDialogueDatabase(DialogueDatabase database)
        {
            LoadSourceFile();
            
            Conversation newConversation = new Conversation();
            
            for(int i = 0; i < sourceLines.Count; i++ )
            {
                if(sourceLines[i].StartsWith("#"))//# triggers the following code
                {
                    string actorHashtagString = sourceLines[i];
                    string actorName = actorHashtagString.Remove(0,1);//Define actorName and remove hashtag
                    string dialogueText = sourceLines[i + 1];//Define dialogue text - always on the next line in fountain

                    DialogueEntry entry = new DialogueEntry(); //create new dialogue entry
                    entry.ActorID = Tools.StringToInt(actorName);//assign actorID to entry
                    entry.DialogueText = dialogueText;//assign dialogue text to entry
                    
                    newConversation.dialogueEntries.Add(entry);//add entry to conversation
                }
            }

            database.AddConversation(newConversation); //add newConversation to database
}
It reads the lines from the fountain file just fine, and I've been able to successfully parse out all the desired lines in testing. When trying to create dialogue entries and conversations I'm running into vague null reference exceptions.

In the CSV example, I noticed there are a ton of safeguards and that every possible variable is defined for entries, links, actors, etc etc. How granular do I need to be to create dialogue entries from code? Do I need to be as thorough as the CSV example or is there a minimum amount of information entries, conversations, and databases need to "pass muster"? Or is there a more simple mistake that I'm not seeing?

---------

Also I was wondering about adding the conversation to my main dialogue database instead of generating whole new database each time. Could I just use "DialogueManager.masterDatabase.AddConversation()"?

Re: .fountain support

Posted: Sun Jul 23, 2023 8:31 pm
by Tony Li
Hi,

Use the template property to create conversations and dialogue entries:

Code: Select all

protected override void CopySourceToDialogueDatabase(DialogueDatabase database)
{
    LoadSourceFile();

    int conversationID = template.GetNextConversationID(database);
    Conversation newConversation = template.CreateConversation(conversationID, "Your Conversation Title");

    // Create the <START> entry:
    DialogueEntry startEntry = template.CreateDialogueEntry(0, conversationID, "START");
    newConversation.dialogueEntries.Add(startEntry);

    DialogueEntry prevEntry = startEntry;

    for (int i = 0; i < sourceLines.Count; i++)
    {
        if (sourceLines[i].StartsWith("#"))//# triggers the following code
        {
            string actorHashtagString = sourceLines[i];
            string actorName = actorHashtagString.Remove(0, 1);//Define actorName and remove hashtag
            string dialogueText = sourceLines[i + 1];//Define dialogue text - always on the next line in fountain

            int entryID = template.GetNextDialogueEntryID(newConversation);
            DialogueEntry entry = template.CreateDialogueEntry(entryID, conversationID, "");
            entry.ActorID = Tools.StringToInt(actorName);//assign actorID to entry
            entry.DialogueText = dialogueText;//assign dialogue text to entry

            newConversation.dialogueEntries.Add(entry);//add entry to conversation

            // Link previous entry to this entry:
            prevEntry.outgoingLinks.Add(new Link(conversationID, prevEntry.id, conversationID, entry.id));

            prevEntry = entry;
        }
    }

    database.AddConversation(newConversation); //add newConversation to database
}
You can find similar example code here.
MattDan wrote: Sun Jul 23, 2023 7:38 pmAlso I was wondering about adding the conversation to my main dialogue database instead of generating whole new database each time. Could I just use "DialogueManager.masterDatabase.AddConversation()"?
Yes. A quick and dirty way is to add this line to the beginning of your CopySourceToDialogueDatabase() method:

Code: Select all

database = EditorTools.FindInitialDatabase();
This will set the database variable to point to the dialogue database that's assigned to the open scene's Dialogue Manager. If the open scene doesn't have a Dialogue Manager, or if no database is assigned to the Dialogue Manager, this line will fail.

A more robust approach would be to add a field to your custom editor where you can assign your database.

If you add the conversation to your main database, you may want to check if the conversation is already present in the database. If so, you'll want to handle that somehow, such as deleting the existing version before creating the new version of the conversation.

If you don't want to deal with this, you can import into a new database. Then open the main database in the Dialogue Editor. In the Database section, use the Merge Database feature to merge the new database's contents into your main database.

Re: .fountain support

Posted: Wed Jul 26, 2023 2:18 pm
by MattDan
Hi Tony,

Awesome, thanks for this. The template methods worked like a dream! This will save me hours of time.

For database merging, I found clicking on overwrite, then merge produced a desirable result. I'd rather have redundancy than accidentally overwrite something I didn't mean to, since I plan on going back and forth and testing a lot.

Here's what I ultimately cooked up in case someone else out there is trying to do something similar to me.
Basically at the start of the doc I added a [TITLE] [ACTOR] and [CONVERSANT] tag to define those things at the start.
Like this
[TITLE]Captain crunch's dilemma
[ACTOR]Captain Crunch
[CONVERSANT]Parrot
Placing @ at the beginning of a line indicates it's an actor, the next line gets turned into dialogue text.
If the actor isn't already in the database, it creates a new actor on the spot.

Thanks again!

Code: Select all

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using UnityEngine;
using UnityEditor;

namespace PixelCrushers.DialogueSystem
{
    //*** Rename TemplateConverterPrefs here and in the ConverterWindowTemplate class definition below.
    [Serializable]
    public class FountainConverterPrefs : AbstractConverterWindowPrefs
    {
        //*** Add any settings your window needs to remember here.
    }

    //*** Rename ConverterWindowTemplate to the name of your converter class:
    public class FountainConverterWindowTemplate : AbstractConverterWindow<FountainConverterPrefs>
    {

        //*** Set the source file extension here:
        //7-21-23 : changed "txt" to "fountain"
        public override string sourceFileExtension { get { return "fountain"; } }

        //*** Set the EditorPrefs key to save preferences under:
        //7-21-23 : changed to "PixelCrushers.DialogueSystem.FountainConverterSettings"
        public override string prefsKey { get { return "PixelCrushers.DialogueSystem.FountainConverterSettings"; } }

        //*** Customize this menu item:
        [MenuItem("Tools/Pixel Crushers/Dialogue System/Import/Fountain", false, 1)]
        public static void Init()
        {
            EditorWindow.GetWindow(typeof(FountainConverterWindowTemplate), false, "Fountain Import");
        }

        //*** Basic preferences are stored in a variable named 'prefs' of type Prefs. You can
        //*** create a subclass of Prefs if you need to store additional data. If you do this,
        //*** also override the ClearPrefs(), LoadPrefs(), and SavePrefs() methods.

        //*** This is the main conversion routine.
        //*** Read prefs.SourceFile (or whatever source data you need, if you've overridden
        //*** the prefs object) and copy the data into the dialogue database object.
        protected override void CopySourceToDialogueDatabase(DialogueDatabase database)
        {
            //Load the source file
            LoadSourceFile();
            Debug.Log("FountainConverterWindowTemplate : LoadSourceFile() ran!");

            string thisTitle = "Untitled Conversation";//the title of the conversation (default, assigned below)
            string s_thisActor = "Player";
            string s_thisConversant = "NPC";

            //---------- Find and add [TITLE]
            for (int i = 0; i < sourceLines.Count; i++)//iterate through source
            {
                if (sourceLines[i].StartsWith("[TITLE]"))
                {
                    string titleMarkedString = sourceLines[i];
                    string foundTitle = titleMarkedString.Remove(0, 7);//define foundTitle, remove [TITLE] markup
                    thisTitle = foundTitle;
                    Debug.Log("FountainConverterWindowTemplate : Title = " + thisTitle);
                    break;
                }
            }
            
            //---------- Find and add [ACTOR]
            for (int i = 0; i < sourceLines.Count; i++)//iterate
            {
                if (sourceLines[i].StartsWith("[ACTOR]"))
                {
                    //check if it exists, if not - make a new one.
                    string actorMarkedString = sourceLines[i];
                    string foundActor = actorMarkedString.Remove(0, 7);
                    s_thisActor = foundActor;

                    if (database.GetActor(s_thisActor) == null)//Check if the actor already exists. if not, add a new one.
                    {
                        int actorID = template.GetNextActorID(database);
                        bool isThisPlayer = false;
                        //if (s_thisActor == "JOHNBOY") { isThisPlayer = true; }//check if it's player
                        Actor newActor = template.CreateActor(actorID, s_thisActor, isThisPlayer);//create new actor
                        database.actors.Add(newActor);//Add to Dialogue System Database
                        Debug.Log("FountainConverterWindowTemplate : New Actor created : " + s_thisActor + " | Actor ID = " + newActor.id);
                    }

                    break;
                }
            }
            
            //---------- Find and add [CONVERSANT]
            for (int i = 0; i < sourceLines.Count; i++)//iterate
            {
                if (sourceLines[i].StartsWith("[CONVERSANT]"))
                {
                    string conversantMarkedString = sourceLines[i];
                    string foundConversant = conversantMarkedString.Remove(0, 12);
                    s_thisConversant = foundConversant;

                    if (database.GetActor(s_thisConversant) == null)
                    {
                        int actorID = template.GetNextActorID(database);
                        bool isThisPlayer = false;
                        //if (s_thisActor == "JOHNBOY") { isThisPlayer = true; }//check if it's player
                        Actor newActor = template.CreateActor(actorID, s_thisConversant, isThisPlayer);//create new actor
                        database.actors.Add(newActor);
                        Debug.Log("FountainConverterWindowTemplate : New Actor created : " + s_thisConversant + " | Actor ID = " + newActor.id);
                    }

                    break;
                }
            }

            //Define conversationID and new conversation. Add title, actor, and conversant.
            int conversationID = template.GetNextConversationID(database);
            Conversation newConversation = template.CreateConversation(conversationID, thisTitle);
            newConversation.ActorID = Tools.StringToInt(s_thisActor);
            newConversation.ConversantID = Tools.StringToInt(s_thisConversant);
            Debug.Log("FountainConverterWindowTemplate : New Conversation Created!");

            //Create the <START> entry:
            DialogueEntry startEntry = template.CreateDialogueEntry(0, conversationID, "START");
            newConversation.dialogueEntries.Add(startEntry);//Add startEntry to newConversation

            DialogueEntry prevEntry = startEntry;//Placeholder for prevEntry

            string currentActor = s_thisActor;
            string currentConversant = s_thisConversant;
            string previousActor = s_thisConversant;
            string previousConversant = s_thisActor;

            Debug.Log("FountainConverterWindowTemplate : for Loop Started, " + sourceLines.Count + " source lines in fountan file!");
            
            //Iterate through source file, every time an @ is detected, 
            for (int i = 0; i < sourceLines.Count; i++)
            {
                if (sourceLines[i].StartsWith("@"))
                {
                    string actorMarkedString = sourceLines[i];
                    string actorName = actorMarkedString.Remove(0, 1);//Define actorName, remove @ markup

                    //If the actor is different than last time, change the actor/conversant. If it's a new actor, add them.
                    if (currentActor != actorName)//if the actor is different
                    {
                        //check if current actor is not previous speaker, and if currentActor is already in database
                        if(actorName != previousConversant && database.GetActor(actorName) != null)
                        {
                            //assign new current actor. Previous conversant.
                            previousActor = currentActor;
                            previousConversant = currentConversant;

                            currentActor = actorName;
                            currentConversant = previousActor; //(is this enough?)

                            Debug.Log("FountainConverterWindowTemplate : Current Actor = " + currentActor + " | Current Conversant = " + currentConversant);
                            Debug.Log("FountainConverterWindowTemplate : Previous Actor = " + previousActor + " | Previous Conversant = " + previousConversant);
                        }
                        
                        else if(actorName != previousConversant && database.GetActor(actorName) == null)//if currentActor is NOT in database
                        {
                            //Add the new actor, than assign new current actor. Previous conversant.
                            int actorID = template.GetNextActorID(database);
                            Actor newActor = template.CreateActor(actorID, actorName, false);
                            database.actors.Add(newActor);
                            
                            previousActor = currentActor;
                            previousConversant = currentConversant;

                            currentActor = actorName;
                            currentConversant = previousActor;

                            Debug.Log("FountainConverterWindowTemplate : Current Actor = " + currentActor + " | Current Conversant = " + currentConversant);
                            Debug.Log("FountainConverterWindowTemplate : Previous Actor = " + previousActor + " | Previous Conversant = " + previousConversant);
                        }
                        else
                        {
                            //swap previous/current actor/conversant (2 way convo)
                            previousActor = currentActor;
                            previousConversant = currentConversant;

                            currentActor = actorName;
                            currentConversant = previousActor;
                            
                            Debug.Log("FountainConverterWindowTemplate : Current Actor = " + currentActor + " | Current Conversant = " + currentConversant);
                            Debug.Log("FountainConverterWindowTemplate : Previous Actor = " + previousActor + " | Previous Conversant = " + previousConversant);
                        }

                    }

                    string dialogueText = sourceLines[i + 1];//Define dialogue text (the next line down)

                    int entryID = template.GetNextDialogueEntryID(newConversation);//Define entryID from newConversation
                    DialogueEntry entry = template.CreateDialogueEntry(entryID, conversationID, ""); //create new dialogue entry

                    entry.ActorID = database.GetActor(currentActor).id;//Assign actorID and conversantID
                    entry.ConversantID = database.GetActor(currentConversant).id;

                    entry.DialogueText = dialogueText;//assign dialogue text to entry

                    newConversation.dialogueEntries.Add(entry);//add entry to conversation
                    Debug.Log("FountainConverterWindowTemplate : " + actorName + " : " + dialogueText);
                    //Debug.Log("FountainConverterWindowTemplate : entry actor/conversant check: actor = " + entry.ActorID + " conversant = " + entry.ConversantID);

                    //Link previous entry to this entry:
                    prevEntry.outgoingLinks.Add(new Link(conversationID, prevEntry.id, conversationID, entry.id));
                    prevEntry = entry;//shift prev entry to this entry
                }
            }

            //add newConversation to database
            database.AddConversation(newConversation);
            Debug.Log("FountainConverterWindowTemplate : Conversation added to Database!");
        }

        //*** Uncomment this method and change it if you want to change the way the converter
        //*** touches up the database after copying the source data. The base version of this
        //*** method edits the START nodes of all conversations and sets their Sequence fields
        //*** to None(). For example, if you know where the actors' portrait textures are,
        //*** You can also call FindPortraitTextures(database, portraitFolder), which will 
        //*** assign the actors' portrait images based on their Textures fields.
        //protected override void TouchUpDialogueDatabase(DialogueDatabase database) {
        //	base.TouchUpDialogueDatabase(database);
        //}

        //*** This is a subclass of AbstractConverterWindow. All methods in AbstractConverterWindow
        //*** are overrideable, so you can really customize it however you want by overriding
        //*** specific methods.

    }

}


Re: .fountain support

Posted: Wed Jul 26, 2023 2:20 pm
by Tony Li
Thanks for sharing your solution!