Combine Duplicate Attributions - Screenplay

Announcements, support questions, and discussion for the Dialogue System.
Post Reply
VoodooDetective
Posts: 222
Joined: Wed Jan 22, 2020 10:48 pm

Combine Duplicate Attributions - Screenplay

Post by VoodooDetective »

I was just wondering if you'd be willing to add an option to collapse duplicate attributions in the screenplay exporter.

So this:
Voodoo Detective: What time is it?
Voodoo Detective: Time for mystery I guess.
Mary Fontule: You got that right.
Would become this:
Voodoo Detective: What time is it? Time for mystery I guess.
Mary Fontule: You got that right.
I wrote a crappy little implementation:

Code: Select all

// Copyright (c) Pixel Crushers. All rights reserved.

using UnityEngine;
using System.IO;
using System.Text;
using System.Collections.Generic;

namespace PixelCrushers.DialogueSystem
{

    /// <summary>
    /// This part of the Dialogue Editor window contains the screenplay export code.
    /// </summary>
    public static class ScreenplayExporter
    {

        /// <summary>
        /// The main export method. Exports screenplay text files for each language.
        /// </summary>
        /// <param name="database">Source database.</param>
        /// <param name="filename">Target filename.</param>
        /// <param name="exportActors">If set to <c>true</c> export actors.</param>
        public static void Export(DialogueDatabase database, string filename, EncodingType encodingType)
        {
            if (database == null || string.IsNullOrEmpty(filename)) return;
            var otherLanguages = FindOtherLanguages(database);
            ExportFile(database, string.Empty, filename, encodingType);
            foreach (var language in otherLanguages)
            {
                ExportFile(database, language, filename, encodingType);
            }
        }

        private static List<string> FindOtherLanguages(DialogueDatabase database)
        {
            var otherLanguages = new List<string>();
            foreach (var conversation in database.conversations)
            {
                foreach (var entry in conversation.dialogueEntries)
                {
                    foreach (var field in entry.fields)
                    {
                        if ((field.type == FieldType.Localization) &&
                            !otherLanguages.Contains(field.title) &&
                            !field.title.Contains(" "))
                        {
                            otherLanguages.Add(field.title);
                        }
                    }
                }
            }
            return otherLanguages;
        }

        private static void ExportFile(DialogueDatabase database, string language, string baseFilename, EncodingType encodingType)
        {
            var filename = string.IsNullOrEmpty(language) ? baseFilename
                : Path.GetDirectoryName(baseFilename) + "/" + Path.GetFileNameWithoutExtension(baseFilename) + "_" + language + ".csv";
            using (StreamWriter file = new StreamWriter(filename, false, EncodingTypeTools.GetEncoding(encodingType)))
            {
                ExportConversations(database, language, file);
            }
        }

        private static void ExportConversations(DialogueDatabase database, string language, StreamWriter file)
        {
            // Cache actor names:
            Dictionary<int, string> actorNames = new Dictionary<int, string>();

            // We need to know how many links go into each entry to know whether to show [id]:
            Dictionary<int, int> numLinksToEntry = new Dictionary<int, int>();

            // Track which entries we've already done:
            List<DialogueEntry> visited = new List<DialogueEntry>();

            // Setup Writer
            ScreenplayWriter screenplayWriter = new ScreenplayWriter(file);

            // Export all conversations:
            foreach (var conversation in database.conversations)
            {
                file.WriteLine(string.Format("[{0}]\t{1}", conversation.id, conversation.Title.ToUpper()));
                file.WriteLine(string.Empty);
                if (!string.IsNullOrEmpty(conversation.Description))
                {
                    file.WriteLine(string.Format("\t{0}", conversation.Description));
                    file.WriteLine(string.Empty);
                }

                // Count num links going into each entry:
                foreach (var entry in conversation.dialogueEntries)
                {
                    numLinksToEntry[entry.id] = 0;
                }
                foreach (var entry in conversation.dialogueEntries)
                {
                    foreach (var link in entry.outgoingLinks)
                    {
                        if (link.destinationConversationID == entry.conversationID)
                        {
                            numLinksToEntry[link.destinationDialogueID]++;
                        }
                    }
                }

                // Export depth first, starting from <START> node:
                ExportSubtree(database, language, actorNames, numLinksToEntry, visited, conversation.GetFirstDialogueEntry(), 0, screenplayWriter);

                // Add orphan nodes at the end:
                foreach (var e in conversation.dialogueEntries)
                {
                    if (!visited.Contains(e))
                    {
                        ExportSubtree(database, language, actorNames, numLinksToEntry, visited, e, -1, screenplayWriter);
                    }
                }

                file.WriteLine(string.Empty);
                file.WriteLine("---page---");
                file.WriteLine(string.Empty);
            }
        }

        private static void ExportSubtree(DialogueDatabase database, string language, Dictionary<int, string> actorNames, Dictionary<int, int> numLinksToEntry, List<DialogueEntry> visited, DialogueEntry entry, int siblingIndex, ScreenplayWriter file)
        {
            if (entry == null) return;
            visited.Add(entry);
            if (entry.id > 0)
            {
                // Write this entry (the root of the subtree).

                // Write entry ID if necessary:
                if (siblingIndex == -1)
                {
                    file.Flush();
                    file.WriteLine(string.Format("\tUnconnected entry [{0}]:", entry.id));
                    file.WriteLine(string.Empty);
                }
                else if ((siblingIndex == 0 && !string.IsNullOrEmpty(entry.conditionsString)) ||
                    (siblingIndex > 0) ||
                    (numLinksToEntry.ContainsKey(entry.id) && numLinksToEntry[entry.id] > 1))
                {
                    file.Flush();
                    if (string.IsNullOrEmpty(entry.conditionsString))
                    {
                        file.WriteLine(string.Format("\tEntry [{0}]:", entry.id));
                    }
                    else
                    {
                        file.WriteLine(string.Format("\tEntry [{0}]: ({1})", entry.id, entry.conditionsString));
                    }
                    file.WriteLine(string.Empty);
                }

                // Cache Actor Name
                if (!actorNames.ContainsKey(entry.ActorID))
                {
                    Actor actor = database.GetActor(entry.ActorID);
                    actorNames.Add(entry.ActorID, (actor != null) ? actor.Name.ToUpper() : "ACTOR");
                }

                // Write Actor Name
                file.WriteActor(string.Format("\t\t\t\t{0}", actorNames[entry.ActorID]));
                // var description = Field.LookupValue(entry.fields, "Description");
                // if (!string.IsNullOrEmpty(description))
                // {
                //     file.WriteLine(string.Format("\t\t\t({0})", description));
                // }

                // Write Line
                var lineText = string.IsNullOrEmpty(language) ? entry.subtitleText : Field.LookupValue(entry.fields, language);
                if (entry.isGroup)
                {
                    // Group entries use Title:
                    // lineText = Field.LookupValue(entry.fields, "Title");
                    // lineText = !string.IsNullOrEmpty(lineText) ? ("(" + lineText + ")") : "(Group entry; no dialogue)";
                    lineText = "(Group entry; no dialogue)";
                }
                // file.WriteSubtitle(string.Format("\t\t{0}", lineText));
                file.WriteSubtitle(lineText);
                // file.WriteLine(string.Empty);
            }

            // Handle link summary:
            if (entry.outgoingLinks.Count == 0)
            {
                file.Flush();
                file.WriteLine("\t\t\t\t[END]");
                file.WriteLine(string.Empty);
            }
            else if (entry.outgoingLinks.Count > 1)
            {
                var s = "\tResponses: ";
                var first = true;
                for (int i = 0; i < entry.outgoingLinks.Count; i++)
                {
                    if (!first) s += ", ";
                    first = false;
                    var link = entry.outgoingLinks[i];
                    if (link.destinationConversationID == entry.conversationID)
                    {
                        s += "[" + link.destinationDialogueID + "]";
                    }
                    else
                    {
                        var destConversation = database.GetConversation(link.destinationConversationID);
                        if (destConversation != null)
                        {
                            s += "[" + destConversation.Title.ToUpper() + ":" + link.destinationDialogueID + "]";
                        }
                        else
                        {
                            s += "[Other Conversation]";
                        }
                    }
                }
                file.Flush();
                file.WriteLine(s);
                file.WriteLine(string.Empty);
            }

            // Follow each outgoing link as a subtree:
            for (int i = 0; i < entry.outgoingLinks.Count; i++)
            {
                var child = database.GetDialogueEntry(entry.outgoingLinks[i]);
                if (!visited.Contains(child))
                {
                    ExportSubtree(database, language, actorNames, numLinksToEntry, visited, child, i, file);
                }
            }
        }

        public class ScreenplayWriter
        {
            // Properties
            private StreamWriter file;
            private StringBuilder run;
            private string lastActor;

            // Constructor
            public ScreenplayWriter(StreamWriter file)
            {
                this.file = file;
                this.run = new StringBuilder();
            }

            public void WriteLine(string s) => file.WriteLine(s);
            public void WriteActor(string s)
            {
                if (lastActor != s)
                {

                    // Flush
                    Flush();
                    // New Run
                    lastActor = s;
                    run.Append(s + "\n");
                }
            }

            public void WriteSubtitle(string s)
            {
                if (run.Length == (lastActor.Length + 1)) run.Append("\t\t");
                run.Append(s + " ");
            }

            public void Flush()
            {
                // Add Newline
                if (run.Length > 0)
                {
                    run.Append("\n\n");
                    file.Write(run.ToString());
                    run.Clear();
                }
                lastActor = string.Empty;
            }
        }
    }
}
User avatar
Tony Li
Posts: 22051
Joined: Thu Jul 18, 2013 1:27 pm

Re: Combine Duplicate Attributions - Screenplay

Post by Tony Li »

I'll try to get that suggestion into a patch soon and also include it in the next release of course.
VoodooDetective
Posts: 222
Joined: Wed Jan 22, 2020 10:48 pm

Re: Combine Duplicate Attributions - Screenplay

Post by VoodooDetective »

Thanks so much!!
Post Reply