public static void HandleAttribute(Yarn.Markup.MarkupAttribute attribute, CustomDialogueUI ui) { switch (attribute.Name) { case Character: SetDialogueGivenLine(attribute, ui); break; case Wave: float speed = attribute.Properties.ContainsKey("s") ? attribute.Properties["s"].FloatValue : 0.5f; ui.currentDialogue.tweenText.Wave( attribute.Position, attribute.Position + attribute.Length, speed); break; case Shake: float strength = attribute.Properties.ContainsKey("s") ? attribute.Properties["s"].IntegerValue : 4; ui.currentDialogue.tweenText.Shake( attribute.Position, attribute.Position + attribute.Length, strength); break; case Screenshake: float amplitude = attribute.Properties.ContainsKey("a") ? attribute.Properties["a"].FloatValue : 1; MainSingleton.Instance.ImpulseSource.m_ImpulseDefinition.m_AmplitudeGain = amplitude; MainSingleton.Instance.ImpulseSource.GenerateImpulse(); break; } }
/// <summary> /// Gets the first attribute with the specified name, if present. /// </summary> /// <param name="name">The name of the attribute to get.</param> /// <param name="attribute">When this method returns, contains the /// attribute with the specified name, if the attribute is found; /// otherwise, the default <see cref="MarkupAttribute"/>. This /// parameter is passed uninitialized.</param> /// <returns><see langword="true"/> if the <see /// cref="MarkupParseResult"/> contains an attribute with the /// specified name; otherwise, <see langword="false"/>.</returns> public bool TryGetAttributeWithName(string name, out MarkupAttribute attribute) { foreach (var a in this.Attributes) { if (a.Name == name) { attribute = a; return(true); } } attribute = default; return(false); }
/// <summary> /// Returns the substring of <see cref="Text"/> covered by /// <paramref name="attribute"/> Position and Length properties. /// </summary> /// <remarks> /// If the attribute's <see cref="MarkupAttribute.Length"/> /// property is zero, this method returns the empty string. /// /// This method does not check to see if <paramref /// name="attribute"/> is an attribute belonging to this /// MarkupParseResult. As a result, if you pass an attribute that /// doesn't belong, it may describe a range of text that does not /// appear in <see cref="Text"/>. If this occurs, an <see /// cref="System.IndexOutOfRangeException"/> will be thrown. /// </remarks> /// <param name="attribute">The attribute to get the text /// for.</param> /// <returns>The text contained within the attribute.</returns> /// <throws cref="System.IndexOutOfRangeException">Thrown when /// attribute's <see cref="MarkupAttribute.Position"/> and <see /// cref="MarkupAttribute.Length"/> properties describe a range of /// text outside the maximum range of <see cref="Text"/>.</throws> public string TextForAttribute(MarkupAttribute attribute) { if (attribute.Length == 0) { return(string.Empty); } if (this.Text.Length < attribute.Position + attribute.Length) { throw new System.IndexOutOfRangeException($"Attribute represents a range not representable by this text. Does this {nameof(MarkupAttribute)} belong to this {nameof(MarkupParseResult)}?"); } return(this.Text.Substring(attribute.Position, attribute.Length)); }
/// <summary> /// Set current DialogueGroup based on line attributes: character name /// </summary> /// <param name="dialogueLine"></param> private static void SetDialogueGivenLine(Yarn.Markup.MarkupAttribute attribute, CustomDialogueUI ui) { string name = attribute.Properties["name"].StringValue; DialogueType type; // Edit below to add more types switch (name) { case "Player": type = DialogueType.Player; break; case "Think": type = DialogueType.Think; break; default: type = DialogueType.NPC; break; } ui.SetCurrentDialogue(type, name); }
/// <summary> /// Deletes an attribute from this markup. /// </summary> /// <remarks> /// This method deletes the range of text covered by <paramref /// name="attributeToDelete"/>, and updates the other attributes in /// this markup as follows: /// /// - Attributes that start and end before the deleted attribute /// are unmodified. /// /// - Attributes that start before the deleted attribute and end /// inside it are truncated to remove the part overlapping the /// deleted attribute. /// /// - Attributes that have the same position and length as the /// deleted attribute are deleted, if they apply to any text. /// /// - Attributes that start and end within the deleted attribute /// are deleted. /// /// - Attributes that start within the deleted attribute, and end /// outside it, have their start truncated to remove the part /// overlapping the deleted attribute. /// /// - Attributes that start after the deleted attribute have their /// start point adjusted to account for the deleted text. /// /// This method does not modify the current object. A new /// MarkupParseResult is returned. /// /// If <paramref name="attributeToDelete"/> is not an attribute of /// this <see cref="MarkupParseResult"/>, the behaviour is /// undefined. /// </remarks> /// <param name="attributeToDelete">The attribute to /// remove.</param> /// <returns>A new MarkupParseResult, with the plain text modified /// and an updated collection of attributes.</returns> public MarkupParseResult DeleteRange(MarkupAttribute attributeToDelete) { var newAttributes = new List <MarkupAttribute>(); // Address the trivial case: if the attribute has a zero // length, just create a new markup that doesn't include it. // The plain text is left unmodified, because this attribute // didn't apply to any text. if (attributeToDelete.Length == 0) { foreach (var a in this.Attributes) { if (!a.Equals(attributeToDelete)) { newAttributes.Add(a); } } return(new MarkupParseResult(this.Text, newAttributes)); } var deletionStart = attributeToDelete.Position; var deletionEnd = attributeToDelete.Position + attributeToDelete.Length; var editedSubstring = this.Text.Remove(attributeToDelete.Position, attributeToDelete.Length); foreach (var existingAttribute in this.Attributes) { var start = existingAttribute.Position; var end = existingAttribute.Position + existingAttribute.Length; if (existingAttribute.Equals(attributeToDelete)) { // This is the attribute we're deleting. Don't include // it. continue; } var editedAttribute = existingAttribute; if (start <= deletionStart) { // The attribute starts before start point of the item // we're deleting. if (end <= deletionStart) { // This attribute is entirely before the item we're // deleting, and will be unmodified. } else if (end <= deletionEnd) { // This attribute starts before the item we're // deleting, and ends inside it. The Position // doesn't need to change, but its Length is // trimmed so that it ends where the deleted // attribute begins. editedAttribute.Length = deletionStart - start; if (existingAttribute.Length > 0 && editedAttribute.Length <= 0) { // The attribute's length has been reduced to // zero. All of the contents it previous had // have been removed, so we will remove the // attribute itself. continue; } } else { // This attribute starts before the item we're // deleting, and ends after it. Its length is // edited to remove the length of the item we're // deleting. editedAttribute.Length -= attributeToDelete.Length; } } else if (start >= deletionEnd) { // The item begins after the item we're deleting. Its // length isn't changing. We just need to offset its // start position. editedAttribute.Position = start - attributeToDelete.Length; } else if (start >= deletionStart && end <= deletionEnd) { // The item is entirely within the item we're deleting. // It will be deleted too - we'll skip including it in // the updated attributes list. continue; } else if (start >= deletionStart && end > deletionEnd) { // The item starts within the item we're deleting, and // ends outside it. We'll adjust the start point so // that it begins at the point where this item and the // item we're deleting stop overlapping. var overlapLength = deletionEnd - start; var newStart = deletionStart; var newLength = existingAttribute.Length - overlapLength; editedAttribute.Position = newStart; editedAttribute.Length = newLength; } newAttributes.Add(editedAttribute); } return(new MarkupParseResult(editedSubstring, newAttributes)); }
/// <summary> /// Creates a list of <see cref="MarkupAttribute"/>s from loose /// <see cref="MarkupAttributeMarker"/>s. /// </summary> /// <param name="markers">The collection of markers.</param> /// <returns>The list of attributes.</returns> /// <throws cref="MarkupParseException">Thrown when a close marker /// is encountered, but no corresponding open marker for it /// exists.</throws> private List <MarkupAttribute> BuildAttributesFromMarkers(List <MarkupAttributeMarker> markers) { // Using a linked list here because we want to append to the // front and be able to walk through it easily var unclosedMarkerList = new LinkedList <MarkupAttributeMarker>(); var attributes = new List <MarkupAttribute>(markers.Count); foreach (var marker in markers) { switch (marker.Type) { case TagType.Open: // A new marker! Add it to the unclosed list at the // start (because there's a high chance that it // will be closed soon). unclosedMarkerList.AddFirst(marker); break; case TagType.Close: { // A close marker! Walk back through the // unclosed stack to find the most recent // marker of the same type to find its pair. MarkupAttributeMarker matchedOpenMarker = default; foreach (var openMarker in unclosedMarkerList) { if (openMarker.Name == marker.Name) { // Found a corresponding open! matchedOpenMarker = openMarker; break; } } if (matchedOpenMarker.Name == null) { throw new MarkupParseException($"Unexpected close marker {marker.Name} at position {marker.Position} in line {this.input}"); } // This attribute is now closed, so we can // remove the marker from the unmatched list unclosedMarkerList.Remove(matchedOpenMarker); // We can now construct the attribute! var length = marker.Position - matchedOpenMarker.Position; var attribute = new MarkupAttribute(matchedOpenMarker, length); attributes.Add(attribute); } break; case TagType.SelfClosing: { // Self-closing markers create a zero-length // attribute where they appear var attribute = new MarkupAttribute(marker, 0); attributes.Add(attribute); } break; case TagType.CloseAll: { // Close all currently open markers // For each marker that we currently have open, // this marker has closed it, so create an // attribute for it foreach (var openMarker in unclosedMarkerList) { var length = marker.Position - openMarker.Position; var attribute = new MarkupAttribute(openMarker, length); attributes.Add(attribute); } // We've now closed all markers, so we can // clear the unclosed list now unclosedMarkerList.Clear(); } break; } } attributes.Sort(AttributePositionComparison); return(attributes); }
/// <summary>Parses a line of text, and produces a /// <see cref="MarkupParseResult"/> containing the processed /// text.</summary> /// <param name="input">The text to parse.</param> /// <returns>The resulting markup information.</returns> internal MarkupParseResult ParseMarkup(string input) { if (string.IsNullOrEmpty(input)) { // We got a null input; return an empty markup parse result return(new MarkupParseResult { Text = string.Empty, Attributes = new List <MarkupAttribute>(), }); } this.input = input.Normalize(); this.stringReader = new StringReader(this.input); var stringBuilder = new StringBuilder(); var markers = new List <MarkupAttributeMarker>(); int nextCharacter; char lastCharacter = char.MinValue; // Read the entirety of the line while ((nextCharacter = this.stringReader.Read()) != -1) { char c = (char)nextCharacter; if (c == '\\') { // This may be the start of an escaped bracket ("\[" or // "\]"). Peek ahead to see if it is. var nextC = (char)this.stringReader.Peek(); if (nextC == '[' || nextC == ']') { // It is! We'll discard this '\', and read the next // character as plain text. c = (char)this.stringReader.Read(); stringBuilder.Append(c); this.sourcePosition += 1; continue; } else { // It wasn't an escaped bracket. Continue on, and // parse the '\' as a normal character. } } if (c == '[') { // How long is our current string, in text elements // (i.e. visible glyphs)? this.position = new System.Globalization.StringInfo(stringBuilder.ToString()).LengthInTextElements; // The start of a marker! MarkupAttributeMarker marker = this.ParseAttributeMarker(); markers.Add(marker); var hadPrecedingWhitespaceOrLineStart = this.position == 0 || char.IsWhiteSpace(lastCharacter); bool wasReplacementMarker = false; // Is this a replacement marker? if (marker.Name != null && this.markerProcessors.ContainsKey(marker.Name)) { wasReplacementMarker = true; // Process it and get the replacement text! var replacementText = this.ProcessReplacementMarker(marker); // Insert it into our final string and update our // position accordingly stringBuilder.Append(replacementText); } bool trimWhitespaceIfAble = false; if (hadPrecedingWhitespaceOrLineStart) { // By default, self-closing markers will trim a // single trailing whitespace after it if there was // preceding whitespace. This doesn't happen if the // marker was a replacement marker, or it has a // property "trimwhitespace" (which must be // boolean) set to false. All markers can opt-in to // trailing whitespace trimming by having a // 'trimwhitespace' property set to true. if (marker.Type == TagType.SelfClosing) { trimWhitespaceIfAble = !wasReplacementMarker; } if (marker.TryGetProperty(TrimWhitespaceProperty, out var prop)) { if (prop.Type != MarkupValueType.Bool) { throw new MarkupParseException($"Error parsing line {this.input}: attribute {marker.Name} at position {this.position} has a {prop.Type.ToString().ToLower()} property \"{TrimWhitespaceProperty}\" - this property is required to be a boolean value."); } trimWhitespaceIfAble = prop.BoolValue; } } if (trimWhitespaceIfAble) { // If there's trailing whitespace, and we want to // remove it, do so if (this.PeekWhitespace()) { // Consume the single trailing whitespace // character (and don't update position) this.stringReader.Read(); this.sourcePosition += 1; } } } else { // plain text! add it to the resulting string and // advance the parser's plain-text position stringBuilder.Append(c); this.sourcePosition += 1; } lastCharacter = c; } var attributes = this.BuildAttributesFromMarkers(markers); var characterAttributeIsPresent = false; foreach (var attribute in attributes) { if (attribute.Name == CharacterAttribute) { characterAttributeIsPresent = true; } } if (characterAttributeIsPresent == false) { // Attempt to generate a character attribute from the start // of the string to the first colon var match = EndOfCharacterMarker.Match(this.input); if (match.Success) { var endRange = match.Index + match.Length; var characterName = this.input.Substring(0, match.Index); MarkupValue nameValue = new MarkupValue { Type = MarkupValueType.String, StringValue = characterName, }; MarkupProperty nameProperty = new MarkupProperty(CharacterAttributeNameProperty, nameValue); var characterAttribute = new MarkupAttribute(0, 0, endRange, CharacterAttribute, new[] { nameProperty }); attributes.Add(characterAttribute); } } return(new MarkupParseResult { Text = stringBuilder.ToString(), Attributes = attributes, }); }