diff --git a/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj b/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj
index 79e7bca..b167bad 100644
--- a/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj
+++ b/ParadoxSaveParser.Lib.Tests/ParadoxSaveParser.Lib.Tests.csproj
@@ -11,7 +11,7 @@
-
+
@@ -23,7 +23,7 @@
-
+
diff --git a/ParadoxSaveParser.Lib.Tests/SearchExpressionTests.cs b/ParadoxSaveParser.Lib.Tests/SearchExpressionTests.cs
index a2c0180..12453b3 100644
--- a/ParadoxSaveParser.Lib.Tests/SearchExpressionTests.cs
+++ b/ParadoxSaveParser.Lib.Tests/SearchExpressionTests.cs
@@ -8,24 +8,24 @@ namespace ParadoxSaveParser.Lib.Tests;
[TestOf(typeof(ISearchExpression))]
public class SearchExpressionTests
{
- byte[] _smallSaveData;
-
[SetUp]
public void Setup()
{
_smallSaveData = "EU4txt a={ b={ c=0 d=1 e=2 } f=3 }".ToBytes();
}
-
- private static JsonSerializerOptions _smallSaveSerializerOptions = new()
+ private byte[] _smallSaveData;
+
+
+ private static readonly JsonSerializerOptions _smallSaveSerializerOptions = new()
{
WriteIndented = false,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- MaxDepth = 1024,
+ MaxDepth = 1024
};
- internal static string JsonToPdx(string json) =>
- json.Substring(1, json.Length - 2)
+ internal static string JsonToPdx(string json)
+ => json.Substring(1, json.Length - 2)
.Replace(",", " ").Replace("{", "{ ").Replace("}", " }")
.Replace("\"", "").Replace("[", "").Replace("]", "").Replace(":", "=");
diff --git a/ParadoxSaveParser.Lib/BufferedEnumerator.cs b/ParadoxSaveParser.Lib/BufferedEnumerator.cs
index 7d244aa..2bca57b 100644
--- a/ParadoxSaveParser.Lib/BufferedEnumerator.cs
+++ b/ParadoxSaveParser.Lib/BufferedEnumerator.cs
@@ -39,21 +39,21 @@ namespace ParadoxSaveParser.Lib;
///
public class BufferedEnumerator : IEnumerator>
{
- private IEnumerator _enumerator;
- private int _bufferSize;
- LinkedList _llist = new();
+ private readonly int _bufferSize;
private LinkedListNode? _currentNode;
private int _currentNodeIndex = -1;
-
+ private readonly IEnumerator _enumerator;
+ private readonly LinkedList _llist = new();
+
public BufferedEnumerator(IEnumerator enumerator, int bufferSize)
{
_enumerator = enumerator;
_bufferSize = bufferSize;
}
-
+
public bool MoveNext()
{
- if(_currentNodeIndex >= _bufferSize / 2)
+ if (_currentNodeIndex >= _bufferSize / 2)
_llist.RemoveFirst();
while (_llist.Count < _bufferSize && _enumerator.MoveNext())
@@ -62,7 +62,7 @@ public class BufferedEnumerator : IEnumerator>
}
if (_llist.Count == 0)
return false;
-
+
_currentNodeIndex++;
_currentNode = _currentNode is null ? _llist.First : _currentNode.Next;
return _currentNode is not null;
diff --git a/ParadoxSaveParser.Lib/SaveParserEU4.cs b/ParadoxSaveParser.Lib/SaveParserEU4.cs
index e1e0c13..ed82fc3 100644
--- a/ParadoxSaveParser.Lib/SaveParserEU4.cs
+++ b/ParadoxSaveParser.Lib/SaveParserEU4.cs
@@ -6,17 +6,21 @@ global using System.Text;
namespace ParadoxSaveParser.Lib;
///
-/// Sequential parser that doesn't cache anything.
+/// Sequential parser that doesn't cache anything.
///
public class SaveParserEU4
{
protected Stream _saveFile;
- private BufferedEnumerator _tokens;
private ISearchExpression? _searchExprCurrent;
-
- /// Uncompressed stream of gamestate file which can be extracted from save archive
- /// 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
+ private readonly BufferedEnumerator _tokens;
+
+ ///
+ /// Uncompressed stream of gamestate file which can be extracted from save archive
+ ///
+ ///
+ /// 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
+ ///
public SaveParserEU4(Stream savefile, ISearchExpression? query)
{
_tokens = new BufferedEnumerator(LexTextSave(), 5);
@@ -24,13 +28,279 @@ public class SaveParserEU4
_searchExprCurrent = query;
}
+ protected IEnumerator 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. Expected '{expectedHeader}', got '{headStr}'.");
+
+ StringBuilder str = new();
+ int line = 2;
+ int column = 0;
+ bool isQuoteOpen = false;
+ bool isStrInQuotes = false;
+ Token strToken = new()
+ {
+ type = TokenType.Invalid,
+ column = -1,
+ line = -1
+ };
+
+ bool TryCompleteStringToken()
+ {
+ if (isQuoteOpen)
+ return false;
+
+ // strings in quotes may be empty
+ if (!isStrInQuotes && (str.Length <= 0 || str[0] == '#'))
+ return false;
+
+ strToken = new Token
+ {
+ type = TokenType.StringOrNumber,
+ column = (short)(column - str.Length),
+ line = line,
+ value = str.ToString()
+ };
+ str.Clear();
+ isStrInQuotes = false;
+ return true;
+ }
+
+ while (_saveFile.CanRead)
+ {
+ int c = _saveFile.ReadByte();
+ column++;
+ switch (c)
+ {
+ case -1:
+ if (TryCompleteStringToken())
+ yield return strToken;
+ yield break;
+ case '\"':
+ isQuoteOpen = !isQuoteOpen;
+ isStrInQuotes = true;
+ break;
+ case ' ':
+ case '\t':
+ case '\r':
+ if (TryCompleteStringToken())
+ yield return strToken;
+ break;
+ case '\n':
+ if (TryCompleteStringToken())
+ yield return strToken;
+ line++;
+ column = 0;
+ break;
+ case '=':
+ if (TryCompleteStringToken())
+ yield return strToken;
+ yield return new Token
+ {
+ type = TokenType.Equals,
+ line = line, column = (short)column
+ };
+ break;
+ case '{':
+ if (TryCompleteStringToken())
+ yield return strToken;
+ yield return new Token
+ {
+ type = TokenType.BracketOpen,
+ line = line, column = (short)column
+ };
+ break;
+ case '}':
+ if (TryCompleteStringToken())
+ yield return strToken;
+ yield return new Token
+ {
+ type = TokenType.BracketClose,
+ line = line, column = (short)column
+ };
+ break;
+ default:
+ // Skip control characters, which are invisible and causing frontend bugs.
+ // I dont know why there are so many of them in strings.
+ if (c >= 0x20)
+ str.Append((char)c);
+ break;
+ }
+ }
+ }
+
+
+ // doesn't move next
+ private object? ParseValue()
+ {
+ var tok = _tokens.Current.Value;
+ switch (tok.type)
+ {
+ case TokenType.StringOrNumber:
+ if (string.IsNullOrEmpty(tok.value))
+ return string.Empty;
+ if (tok.value[0] != '-' && !char.IsDigit(tok.value[0]))
+ return tok.value;
+ if (tok.value.Contains('.') && double.TryParse(tok.value, out double d))
+ return d;
+ if (long.TryParse(tok.value, out long l))
+ return l;
+ return tok.value;
+ case TokenType.BracketOpen:
+ object obj = ParseListOrDict();
+ return obj;
+ case TokenType.BracketClose:
+ return null;
+ default:
+ throw new UnexpectedTokenException(tok);
+ }
+ }
+
+
+ // skips next value
+ /// true if skipped value, false if current token is closing bracket
+ private bool SkipValue()
+ {
+ var 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())
+ {
+ var tok = _tokens.Current.Value;
+ if (tok.type == TokenType.BracketOpen)
+ bracketBalance++;
+ else if (tok.type == TokenType.BracketClose)
+ bracketBalance--;
+ }
+ }
+
+ // doesn't move next
+ private object ParseListOrDict()
+ {
+ var first = _tokens.Current.Next;
+ var second = _tokens.Current.Next?.Next;
+ if (first?.Value.type == TokenType.StringOrNumber && second?.Value.type == TokenType.Equals)
+ return ParseDict();
+
+ return ParseList();
+ }
+
+ // moves next
+ private List