implemented SearchExpression (buggy)
This commit is contained in:
@@ -5,15 +5,24 @@ global using System.Text;
|
||||
|
||||
namespace ParadoxSaveParser.Lib;
|
||||
|
||||
public class Parser
|
||||
/// <summary>
|
||||
/// Sequential parser that doesn't cache anything.
|
||||
/// </summary>
|
||||
public class SaveParserEU4
|
||||
{
|
||||
protected Stream _saveFile;
|
||||
private BufferedEnumerator<Token> _tokens;
|
||||
|
||||
public Parser(Stream savefile)
|
||||
private SearchExpression _query;
|
||||
private int _currentDepth;
|
||||
|
||||
/// <param name="savefile">Uncompressed stream of <c>gamestate</c> file which can be extracted from save archive</param>
|
||||
/// <param name="query">Parsing whole save takes 10 seconds on mid pc and takes 1GB of RAM,
|
||||
/// so you should specify what exactly you want to get from save file</param>
|
||||
public SaveParserEU4(Stream savefile, SearchExpression query)
|
||||
{
|
||||
_tokens = new BufferedEnumerator<Token>(Lex(), 5);
|
||||
_tokens = new BufferedEnumerator<Token>(LexTextSave(), 5);
|
||||
_saveFile = savefile;
|
||||
_query = query;
|
||||
}
|
||||
|
||||
protected enum TokenType : byte
|
||||
@@ -60,14 +69,21 @@ public class Parser
|
||||
}
|
||||
}
|
||||
|
||||
protected IEnumerator<Token> Lex()
|
||||
protected class UnexpectedTokenException : Exception
|
||||
{
|
||||
public UnexpectedTokenException(Token token) :
|
||||
base($"Unexpected token: {token}")
|
||||
{}
|
||||
}
|
||||
|
||||
protected IEnumerator<Token> LexTextSave()
|
||||
{
|
||||
string expectedHeader = "EU4txt";
|
||||
byte[] headBytes = new byte[expectedHeader.Length];
|
||||
_saveFile.ReadExactly(headBytes);
|
||||
string headStr = Encoding.UTF8.GetString(headBytes);
|
||||
if (headStr != expectedHeader)
|
||||
throw new Exception($"Invalid gamestate header: '{headStr}'");
|
||||
throw new Exception($"Invalid gamestate header. Expected '{expectedHeader}', got '{headStr}'.");
|
||||
|
||||
StringBuilder str = new();
|
||||
int line = 2;
|
||||
@@ -85,7 +101,8 @@ public class Parser
|
||||
{
|
||||
if (isQuoteOpen)
|
||||
return false;
|
||||
// strings in quotes can be empty
|
||||
|
||||
// strings in quotes may be empty
|
||||
if (!isStrInQuotes && (str.Length <= 0 || str[0] == '#'))
|
||||
return false;
|
||||
|
||||
@@ -163,13 +180,6 @@ public class Parser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class UnexpectedTokenException : Exception
|
||||
{
|
||||
public UnexpectedTokenException(Token token) :
|
||||
base($"Unexpected token: {token}")
|
||||
{}
|
||||
}
|
||||
|
||||
|
||||
// doesn't move next
|
||||
@@ -189,13 +199,44 @@ public class Parser
|
||||
return l;
|
||||
return tok.value;
|
||||
case TokenType.BracketOpen:
|
||||
return ParseListOrDict();
|
||||
_currentDepth++;
|
||||
var obj = ParseListOrDict();
|
||||
_currentDepth--;
|
||||
return obj;
|
||||
case TokenType.BracketClose:
|
||||
return null;
|
||||
default:
|
||||
throw new UnexpectedTokenException(tok);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// skips next value
|
||||
/// <returns>true if skipped value, false if current token is closing bracket</returns>
|
||||
private bool SkipValue()
|
||||
{
|
||||
Token tok = _tokens.Current.Value;
|
||||
if (tok.type == TokenType.BracketOpen)
|
||||
{
|
||||
SkipObject();
|
||||
return true;
|
||||
}
|
||||
|
||||
return tok.type != TokenType.BracketClose;
|
||||
}
|
||||
|
||||
// skips all tokens inside curly braces block
|
||||
private void SkipObject(int bracketBalance = 1)
|
||||
{
|
||||
while (bracketBalance != 0 && _tokens.MoveNext())
|
||||
{
|
||||
Token tok = _tokens.Current.Value;
|
||||
if (tok.type == TokenType.BracketOpen)
|
||||
bracketBalance++;
|
||||
else if (tok.type == TokenType.BracketClose)
|
||||
bracketBalance--;
|
||||
}
|
||||
}
|
||||
|
||||
// doesn't move next
|
||||
private object ParseListOrDict()
|
||||
@@ -223,14 +264,14 @@ public class Parser
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
// moves next
|
||||
private Dictionary<string, List<object>> ParseDict()
|
||||
{
|
||||
Dictionary<string, List<object>> dict = new();
|
||||
|
||||
// root is a dict without closing bracket, so this method must check _tokenIndex < _tokens.Count
|
||||
while (_tokens.MoveNext())
|
||||
for (int localIndex = 0; _tokens.MoveNext(); localIndex++)
|
||||
{
|
||||
Token tok = _tokens.Current.Value;
|
||||
// end of dictionary
|
||||
@@ -243,16 +284,7 @@ public class Parser
|
||||
// { } { } { }`
|
||||
if (tok.type == TokenType.BracketOpen)
|
||||
{
|
||||
int bracketBalance = 1;
|
||||
while (bracketBalance != 0 && _tokens.MoveNext())
|
||||
{
|
||||
tok = _tokens.Current.Value;
|
||||
if (tok.type == TokenType.BracketOpen)
|
||||
bracketBalance++;
|
||||
else if (tok.type == TokenType.BracketClose)
|
||||
bracketBalance--;
|
||||
}
|
||||
|
||||
SkipObject();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -276,6 +308,12 @@ public class Parser
|
||||
else if (tok.type != TokenType.BracketOpen)
|
||||
throw new UnexpectedTokenException(tok);
|
||||
|
||||
if (!_query.DoesMatch(new SearchArgs(key, _currentDepth, localIndex)))
|
||||
{
|
||||
SkipValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
object? value = ParseValue();
|
||||
if (value is null)
|
||||
throw new UnexpectedTokenException(_tokens.Current.Value);
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace ParadoxSaveParser.Lib;
|
||||
|
||||
public record SearchArgs(string key, int currentDepth, int localIndex);
|
||||
|
||||
public interface ISearchExpression
|
||||
{
|
||||
bool DoesMatch(SearchArgs args);
|
||||
}
|
||||
|
||||
public class SearchExpression : ISearchExpression
|
||||
{
|
||||
private List<ISearchExpression> _compiledExpression;
|
||||
private int _expressionDepth;
|
||||
|
||||
private SearchExpression(List<ISearchExpression> compiledExpression, int expressionDepth)
|
||||
{
|
||||
_compiledExpression = compiledExpression;
|
||||
_expressionDepth = expressionDepth;
|
||||
}
|
||||
|
||||
|
||||
public bool DoesMatch(SearchArgs args)
|
||||
{
|
||||
if (args.key is "c" or "d" or "e")
|
||||
Console.WriteLine("UwU");
|
||||
int index = args.currentDepth - _expressionDepth;
|
||||
if (index < 0 || index >= _compiledExpression.Count)
|
||||
return true;
|
||||
|
||||
return _compiledExpression[index].DoesMatch(args);
|
||||
}
|
||||
|
||||
|
||||
private static bool CharEqualsAndNotEscaped(char c, ReadOnlySpan<char> chars, int i) =>
|
||||
chars[i] == c && (i < 1 || chars[i - 1] != '\\') && (i < 2 || chars[i - 2] != '\\');
|
||||
|
||||
public static SearchExpression Parse(string query) => ParseInternal(query, 0);
|
||||
|
||||
private static SearchExpression ParseInternal(ReadOnlySpan<char> query, int expressionDepth)
|
||||
{
|
||||
var compiledExpression = new List<ISearchExpression>();
|
||||
ISearchExpression exprPart;
|
||||
int partBegin = 0;
|
||||
int bracketBalance = 0;
|
||||
int expressionDepthIncrement = 0;
|
||||
|
||||
for (int i = 0; i < query.Length; i++)
|
||||
{
|
||||
if (CharEqualsAndNotEscaped('(', query, i))
|
||||
bracketBalance++;
|
||||
else if (CharEqualsAndNotEscaped(')', query, i))
|
||||
bracketBalance--;
|
||||
else if (bracketBalance == 0 && CharEqualsAndNotEscaped('.', query, i))
|
||||
{
|
||||
var part = query.Slice(partBegin, i - partBegin);
|
||||
expressionDepthIncrement++;
|
||||
exprPart = ParsePart(part, query, partBegin,
|
||||
expressionDepth + expressionDepthIncrement);
|
||||
compiledExpression.Add(exprPart);
|
||||
partBegin = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
exprPart = ParsePart(query.Slice(partBegin), query, partBegin,
|
||||
expressionDepth + expressionDepthIncrement);
|
||||
compiledExpression.Add(exprPart);
|
||||
|
||||
return new SearchExpression(compiledExpression, expressionDepth);
|
||||
}
|
||||
|
||||
private static ISearchExpression ParsePart(ReadOnlySpan<char> part,
|
||||
ReadOnlySpan<char> query, int partBegin, int expressionDepth)
|
||||
{
|
||||
if (part is "*")
|
||||
{
|
||||
return new AnyMatchExpression();
|
||||
}
|
||||
|
||||
if (CharEqualsAndNotEscaped('[', query, partBegin))
|
||||
{
|
||||
part = part.Slice(1, part.Length - 2);
|
||||
return new IndexMatchExpression(int.Parse(part));
|
||||
}
|
||||
|
||||
if(part[0] is '(')
|
||||
{
|
||||
var subExprs = new List<ISearchExpression>();
|
||||
ISearchExpression subExpr;
|
||||
part = part.Slice(1, part.Length - 2);
|
||||
int supExprBegin = 0;
|
||||
for (int j = 0; j < part.Length; j++)
|
||||
{
|
||||
if (CharEqualsAndNotEscaped('|', part, j))
|
||||
{
|
||||
subExpr = ParseInternal(part.Slice(supExprBegin, j - supExprBegin),
|
||||
expressionDepth);
|
||||
subExprs.Add(subExpr);
|
||||
supExprBegin = j + 1;
|
||||
}
|
||||
}
|
||||
|
||||
subExpr = ParseInternal(part.Slice(supExprBegin), expressionDepth);
|
||||
subExprs.Add(subExpr);
|
||||
return new MultipleMatchExpression(subExprs);
|
||||
}
|
||||
|
||||
return new ExactMatchExpression(part.ToString());
|
||||
}
|
||||
|
||||
private record AnyMatchExpression : ISearchExpression
|
||||
{
|
||||
public bool DoesMatch(SearchArgs args) => true;
|
||||
}
|
||||
|
||||
private record MultipleMatchExpression(List<ISearchExpression> subExprs) : ISearchExpression
|
||||
{
|
||||
public bool DoesMatch(SearchArgs args)
|
||||
{
|
||||
foreach (var e in subExprs)
|
||||
{
|
||||
if(e.DoesMatch(args))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record IndexMatchExpression(int index) : ISearchExpression
|
||||
{
|
||||
public bool DoesMatch(SearchArgs args) => args.localIndex == index;
|
||||
}
|
||||
|
||||
private record ExactMatchExpression(string key) : ISearchExpression
|
||||
{
|
||||
public bool DoesMatch(SearchArgs args) => args.key == key;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user