using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; namespace Barotrauma { class NPCConversation { const int MaxPreviousConversations = 20; private static List list = new List(); public readonly string Line; public readonly List AllowedJobs; public readonly List Flags; //The line can only be selected when eventmanager intensity is between these values //null = no restriction public float? maxIntensity, minIntensity; public readonly List Responses; private readonly int speakerIndex; private readonly List allowedSpeakerTags; public static void LoadAll(IEnumerable filePaths) { //language, identifier, filepath List> contentPackageFiles = new List>(); foreach (string filePath in filePaths) { if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported XDocument doc = XMLExtensions.TryLoadXml(filePath); if (doc == null || doc.Root == null) continue; string language = doc.Root.GetAttributeString("Language", "English"); string identifier = doc.Root.GetAttributeString("Identifier", "unknown"); contentPackageFiles.Add(new Tuple(language, identifier, filePath)); } List> translationFiles = new List>(); foreach (string filePath in Directory.GetFiles(Path.Combine("Content", "NPCConversations"))) { if (Path.GetExtension(filePath) == ".csv") continue; // .csv files are not supported XDocument doc = XMLExtensions.TryLoadXml(filePath); if (doc == null || doc.Root == null) continue; string language = doc.Root.GetAttributeString("Language", "English"); string identifier = doc.Root.GetAttributeString("Identifier", "unknown"); translationFiles.Add(new Tuple(language, identifier, filePath)); } //get the languages and identifiers of the files for (int i = 0; i < contentPackageFiles.Count; i++) { var contentPackageFile = contentPackageFiles[i]; //correct language, all good if (contentPackageFile.Item1 == TextManager.Language) continue; //attempt to find a translation file with the correct language and a matching identifier //if it fails, we'll just use the original file with the incorrect language var translation = translationFiles.Find(t => t.Item1 == TextManager.Language && t.Item2 == contentPackageFile.Item2); if (translation != null) contentPackageFiles[i] = translation; //replace with the translation file } foreach (var file in contentPackageFiles) { Load(file.Item3); } } private static void Load(string file) { XDocument doc = XMLExtensions.TryLoadXml(file); if (doc == null || doc.Root == null) return; string language = doc.Root.GetAttributeString("Language", "English"); if (language != TextManager.Language) return; foreach (XElement subElement in doc.Root.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "conversation": list.Add(new NPCConversation(subElement)); break; case "personalitytrait": new NPCPersonalityTrait(subElement); break; } } } public NPCConversation(XElement element) { Line = element.GetAttributeString("line", ""); speakerIndex = element.GetAttributeInt("speaker", 0); AllowedJobs = new List(); string allowedJobsStr = element.GetAttributeString("allowedjobs", ""); foreach (string allowedJobIdentifier in allowedJobsStr.Split(',')) { var jobPrefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == allowedJobIdentifier.ToLowerInvariant()); if (jobPrefab != null) AllowedJobs.Add(jobPrefab); } Flags = new List(element.GetAttributeStringArray("flags", new string[0])); allowedSpeakerTags = new List(); string allowedSpeakerTagsStr = element.GetAttributeString("speakertags", ""); foreach (string tag in allowedSpeakerTagsStr.Split(',')) { if (string.IsNullOrEmpty(tag)) continue; allowedSpeakerTags.Add(tag.Trim().ToLowerInvariant()); } if (element.Attribute("minintensity") != null) minIntensity = element.GetAttributeFloat("minintensity", 0.0f); if (element.Attribute("maxintensity") != null) maxIntensity = element.GetAttributeFloat("maxintensity", 1.0f); Responses = new List(); foreach (XElement subElement in element.Elements()) { Responses.Add(new NPCConversation(subElement)); } } private static List GetCurrentFlags(Character speaker) { var currentFlags = new List(); if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) currentFlags.Add("SubmarineDeep"); if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) currentFlags.Add("Initial"); if (speaker != null) { if (speaker.AnimController.InWater) currentFlags.Add("Underwater"); currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside"); if (Character.Controlled != null) { if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null) { currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf"); } } var afflictions = speaker.CharacterHealth.GetAllAfflictions(); foreach (Affliction affliction in afflictions) { var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength); if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag)) { currentFlags.Add(currentEffect.DialogFlag); } } } return currentFlags; } private static List previousConversations = new List(); public static List> CreateRandom(List availableSpeakers) { Dictionary assignedSpeakers = new Dictionary(); List> lines = new List>(); CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: list); return lines; } public static List> CreateRandom(List availableSpeakers, List requiredFlags) { Dictionary assignedSpeakers = new Dictionary(); List> lines = new List>(); var availableConversations = list.FindAll(conversation => requiredFlags.All(f => conversation.Flags.Contains(f))); if (availableConversations.Count > 0) { CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true); } return lines; } private static void CreateConversation( List availableSpeakers, Dictionary assignedSpeakers, NPCConversation baseConversation, List> lineList, List availableConversations, bool ignoreFlags = false) { List conversations = baseConversation == null ? availableConversations : baseConversation.Responses; if (conversations.Count == 0) return; int conversationIndex = Rand.Int(conversations.Count); NPCConversation selectedConversation = conversations[conversationIndex]; if (string.IsNullOrEmpty(selectedConversation.Line)) return; Character speaker = null; //speaker already assigned for this line if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex)) { //check if the character has all required flags to say the line var characterFlags = GetCurrentFlags(assignedSpeakers[selectedConversation.speakerIndex]); if (selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) { speaker = assignedSpeakers[selectedConversation.speakerIndex]; } } if (speaker == null) { var allowedSpeakers = new List(); List potentialLines = new List(conversations); //remove lines that are not appropriate for the intensity of the current situation if (GameMain.GameSession?.EventManager != null) { potentialLines.RemoveAll(l => (l.minIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity < l.minIntensity) || (l.maxIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity > l.maxIntensity)); } while (potentialLines.Count > 0) { //select a random line and attempt to find a speaker for it // and if no valid speaker is found, choose another random line selectedConversation = GetRandomConversation(potentialLines, baseConversation == null); if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) return; //speaker already assigned for this line if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex)) { speaker = assignedSpeakers[selectedConversation.speakerIndex]; break; } foreach (Character potentialSpeaker in availableSpeakers) { //check if the character has an appropriate job to say the line if ((potentialSpeaker.Info?.Job != null && potentialSpeaker.Info.Job.Prefab.OnlyJobSpecificDialog) || selectedConversation.AllowedJobs.Count > 0) { if (!selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue; } //check if the character has all required flags to say the line if (!ignoreFlags) { var characterFlags = GetCurrentFlags(potentialSpeaker); if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) continue; } //check if the character has an appropriate personality if (selectedConversation.allowedSpeakerTags.Count > 0) { if (potentialSpeaker.Info?.PersonalityTrait == null) continue; if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) continue; } else { if (potentialSpeaker.Info?.PersonalityTrait != null && !potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none")) { continue; } } allowedSpeakers.Add(potentialSpeaker); } if (allowedSpeakers.Count == 0) { potentialLines.Remove(selectedConversation); } else { break; } } if (allowedSpeakers.Count == 0) return; speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)]; availableSpeakers.Remove(speaker); assignedSpeakers.Add(selectedConversation.speakerIndex, speaker); } if (baseConversation == null) { previousConversations.Insert(0, selectedConversation); if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations); } lineList.Add(new Pair(speaker, selectedConversation.Line)); CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations); } private static NPCConversation GetRandomConversation(List conversations, bool avoidPreviouslyUsed) { if (!avoidPreviouslyUsed) { return conversations.Count == 0 ? null : conversations[Rand.Int(conversations.Count)]; } List probabilities = new List(); foreach (NPCConversation conversation in conversations) { probabilities.Add(GetConversationProbability(conversation)); } return ToolBox.SelectWeightedRandom(conversations, probabilities, Rand.RandSync.Unsynced); } private static float GetConversationProbability(NPCConversation conversation) { int index = previousConversations.IndexOf(conversation); if (index < 0) return 10.0f; return 1.0f - 1.0f / (index + 1); } #if DEBUG public static void WriteToCSV() { System.Text.StringBuilder sb = new System.Text.StringBuilder(); for (int i = 0; i < list.Count; i++) { NPCConversation current = list[i]; WriteConversation(sb, current, 0); WriteSubConversations(sb, current.Responses, 1); WriteEmptyRow(sb); } StreamWriter file = new StreamWriter(@"NPCConversations.csv"); file.WriteLine(sb.ToString()); file.Close(); } private static void WriteConversation(System.Text.StringBuilder sb, NPCConversation conv, int depthIndex) { sb.Append(conv.speakerIndex); // Speaker index sb.Append('*'); sb.Append(depthIndex); // Depth index sb.Append('*'); sb.Append(conv.Line); // Original sb.Append('*'); // Translated sb.Append('*'); sb.Append(string.Join(",", conv.Flags)); // Flags sb.Append('*'); for (int i = 0; i < conv.AllowedJobs.Count; i++) // Jobs { sb.Append(conv.AllowedJobs[i].Identifier); if (i < conv.AllowedJobs.Count - 1) { sb.Append(","); } } sb.Append('*'); sb.Append(string.Join(",", conv.allowedSpeakerTags)); // Traits sb.Append('*'); sb.Append(conv.minIntensity); // Minimum intensity sb.Append('*'); sb.Append(conv.maxIntensity); // Maximum intensity sb.Append('*'); // Comments sb.AppendLine(); } private static void WriteSubConversations(System.Text.StringBuilder sb, List responses, int depthIndex) { for (int i = 0; i < responses.Count; i++) { WriteConversation(sb, responses[i], depthIndex); if (responses[i].Responses != null && responses[i].Responses.Count > 0) { WriteSubConversations(sb, responses[i].Responses, depthIndex + 1); } } } private static void WriteEmptyRow(System.Text.StringBuilder sb) { for (int i = 0; i < 8; i++) { sb.Append('*'); } sb.AppendLine(); } #endif } }