CapersProject/Assets/Plugins/Pixel Crushers/Dialogue System/Scripts/MVC/Sequencer/SequenceParser.cs

356 lines
12 KiB
C#
Raw Normal View History

// Copyright (c) Pixel Crushers. All rights reserved.
using UnityEngine;
using System.Collections.Generic;
using System.Text;
using System.IO;
using PixelCrushers.DialogueSystem.SequencerCommands;
namespace PixelCrushers.DialogueSystem
{
/// <summary>
/// Custom exception for parser errors.
/// </summary>
public class ParserException : System.Exception
{
public ParserException(string message) : base(message) { }
}
/// <summary>
/// Basic recursive descent parser for sequences.
/// </summary>
public class SequenceParser
{
private const int MaxSafeguard = 9999; // Limits loops to prevent infinite loop bugs.
private int column; // Helps pinpoint location of syntax errors.
private int row;
private bool isNextCharEscaped = false; // Next character was preceded by \
/// <summary>
/// Parses a sequence string into a list of sequencer command records.
/// </summary>
/// <param name="sequence">Sequence to parse.</param>
/// <returns>A list of command records.</returns>
public List<QueuedSequencerCommand> Parse(string sequence, bool throwExceptions = false)
{
var list = new List<QueuedSequencerCommand>();
try
{
var reader = new StringReader(sequence);
row = 1;
column = 1;
int safeguard = 0;
while (reader.Peek() != -1 && safeguard < MaxSafeguard)
{
safeguard++;
var command = ParseCommand(reader);
if (command != null) list.Add(command);
}
}
catch (ParserException e)
{
if (DialogueDebug.logWarnings) Debug.LogWarning(DialogueDebug.Prefix + ": Syntax error '" + e.Message + "' at column " + column + " row " + row + " parsing: " + sequence);
list.Clear();
if (throwExceptions) throw e;
}
return list;
}
private QueuedSequencerCommand ParseCommand(StringReader reader)
{
ParseOptionalWhitespace(reader, true);
CheckParseComment(reader);
var required = false;
var s = ParseWord(reader);
if (string.Equals(s, SequencerKeywords.Required, System.StringComparison.OrdinalIgnoreCase) || string.Equals(s, SequencerKeywords.Require, System.StringComparison.OrdinalIgnoreCase))
{
required = true;
ParseOptionalWhitespace(reader);
s = ParseWord(reader);
}
var command = s;
ParseOptionalWhitespace(reader);
if (reader.Peek() == -1) return null;
ParseOpenParen(reader);
ParseOptionalWhitespace(reader);
var parameters = ParseParameters(reader);
ParseCloseParen(reader);
ParseOptionalWhitespace(reader);
float atTime;
string atMessage;
string sendMessage;
ParsePostParameters(reader, out atTime, out atMessage, out sendMessage);
ParseOptionalWhitespace(reader);
if (!CheckParseComment(reader))
{
ParseSemicolonOrEnd(reader);
}
ParseOptionalWhitespace(reader);
CheckParseComment(reader);
return new QueuedSequencerCommand(command, parameters, atTime, atMessage, sendMessage, required);
}
private string ParseWord(StringReader reader, bool allowWhiteSpace = false)
{
var sb = new StringBuilder();
int safeguard = 0;
while (HasNextChar(reader) && safeguard < MaxSafeguard)
{
safeguard++;
var c = (char)reader.Peek();
if ((char.IsWhiteSpace(c) && !allowWhiteSpace) || c == '(' || c == ')' || c == ';' || c == '-')
{
break;
}
else
{
sb.Append(ReadNextChar(reader));
}
}
return sb.ToString();
}
private void ParseOptionalWhitespace(StringReader reader, bool includingSemicolons = false)
{
int safeguard = 0;
while ((IsNextCharWhiteSpace(reader) || (includingSemicolons && IsNextChar(reader, ';'))) && safeguard < MaxSafeguard)
{
safeguard++;
ReadNextChar(reader);
}
}
private bool HasNextChar(StringReader reader)
{
return reader != null && reader.Peek() != -1;
}
private char PeekNextChar(StringReader reader)
{
if (reader == null) return '\0';
if (reader.Peek() == '\\')
{
isNextCharEscaped = true;
reader.Read(); // Read past escape char.
column++;
return (char)reader.Peek();
}
else
{
return (char)reader.Peek();
}
}
private bool IsNextCharWhiteSpace(StringReader reader)
{
return HasNextChar(reader) && char.IsWhiteSpace(PeekNextChar(reader));
}
private bool IsNextChar(StringReader reader, char requiredChar)
{
return HasNextChar(reader) && PeekNextChar(reader) == requiredChar;
}
private bool IsNextCharNot(StringReader reader, char requiredChar)
{
return HasNextChar(reader) && PeekNextChar(reader) != requiredChar;
}
private char ReadNextChar(StringReader reader)
{
var c = (char)reader.Read();
if (c == '\n')
{
row++;
column = 1;
}
else
{
column++;
}
isNextCharEscaped = false;
return c;
}
private void ParseChar(StringReader reader, char requiredChar)
{
if (IsNextChar(reader, requiredChar))
{
ReadNextChar(reader);
}
else
{
throw new ParserException("Expected '" + requiredChar + "'");
}
}
private void ParseOpenParen(StringReader reader)
{
ParseChar(reader, '(');
}
private void ParseCloseParen(StringReader reader)
{
ParseChar(reader, ')');
}
private string[] ParseParameters(StringReader reader)
{
var parameters = new List<string>();
int safeguard = 0;
while (IsNextCharNot(reader, ')') && safeguard < MaxSafeguard)
{
safeguard++;
ParseOptionalWhitespace(reader);
parameters.Add(ParseParameter(reader));
if (IsNextChar(reader, ',')) ReadNextChar(reader);
}
return parameters.ToArray();
}
private string ParseParameter(StringReader reader)
{
var sb = new StringBuilder();
int parenDepth = 0;
int safeguard = 0;
while (HasNextChar(reader) && safeguard < MaxSafeguard)
{
safeguard++;
var c = PeekNextChar(reader);
if (parenDepth <= 0 && (c == ',' || c == ')') && !isNextCharEscaped)
{
break;
}
else
{
var paramChar = ReadNextChar(reader);
sb.Append(paramChar);
if (paramChar == '(')
{
parenDepth++;
}
else if (paramChar == ')')
{
parenDepth--;
}
}
}
var s = sb.ToString().Trim();
return s;
}
private void ParsePostParameters(StringReader reader, out float atTime, out string atMessage, out string sendMessage)
{
atTime = 0;
atMessage = string.Empty;
sendMessage = string.Empty;
ParseOptionalWhitespace(reader);
if (IsNextChar(reader, '@'))
{
ParseAtSignModifier(reader, out atTime, out atMessage);
}
ParseOptionalWhitespace(reader);
if (IsNextChar(reader, '-'))
{
ParseArrowModifier(reader, out sendMessage);
}
}
private void ParseAtSignModifier(StringReader reader, out float atTime, out string atMessage)
{
atTime = 0;
atMessage = string.Empty;
if (IsNextChar(reader, '@'))
{
ReadNextChar(reader);
ParseOptionalWhitespace(reader);
var s = ParseWord(reader);
if (string.Equals(s, "message", System.StringComparison.OrdinalIgnoreCase))
{
ParseOptionalWhitespace(reader);
ParseChar(reader, '(');
ParseOptionalWhitespace(reader);
s = ParseWord(reader, true);
ParseChar(reader, ')');
atMessage = s;
atTime = string.IsNullOrEmpty(atMessage) ? 0 : 365f * 86400f; // One year -- essentially forever, since we're really waiting for the message.
}
else
{
float value;
if (float.TryParse(s, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out value))
{
atTime = value;
}
else
{
throw new ParserException("Can't convert " + s + " to a number");
}
}
}
}
private void ParseArrowModifier(StringReader reader, out string sendMessage)
{
sendMessage = string.Empty;
if (IsNextChar(reader, '-'))
{
ReadNextChar(reader);
if (!IsNextChar(reader, '>'))
{
throw new ParserException("Invalid modifier after command; expected @time, @Message(x), ->Message(x) or nothing");
}
else
{
ReadNextChar(reader);
ParseOptionalWhitespace(reader);
var s = ParseWord(reader);
if (string.Equals(s, SequencerKeywords.Message, System.StringComparison.OrdinalIgnoreCase))
{
ParseOptionalWhitespace(reader);
ParseChar(reader, '(');
ParseOptionalWhitespace(reader);
s = ParseWord(reader, true);
sendMessage = s.Trim();
ParseChar(reader, ')');
}
}
}
}
private void ParseSemicolonOrEnd(StringReader reader)
{
if (!HasNextChar(reader) || (char)reader.Peek() == ';')
{
ReadNextChar(reader);
}
else
{
throw new ParserException("Expected semicolon or end of sequence");
}
}
private bool CheckParseComment(StringReader reader)
{
if (!HasNextChar(reader) || (char)reader.Peek() == '/')
{
reader.Read();
if (!HasNextChar(reader) || (char)reader.Peek() == '/')
{
reader.ReadLine();
return true;
}
}
return false;
}
}
}