global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Text;
using System.Globalization;
namespace ParadoxSaveParser.Lib;
///
/// Sequential parser that doesn't cache anything.
///
public class SaveParserEU4
{
private const int DefaultBufferSize = 64 * 1024;
private static ReadOnlySpan Header => "EU4txt"u8;
///
/// Windows-1252 is the default encoding of the save files
///
public static Encoding DefaultEncoding => ParadoxEncodings.Windows1252;
private readonly Tokenizer _tokens;
private readonly SearchExpressionCompilation? _query;
// step of the query the parser is currently at, compiled by Parse()
private ISearchExpression? _searchExprCurrent;
///
/// Encoding of the strings inside the save. Saves of different localizations use
/// different ones, so it can be changed; is what EU4 writes.
///
public Encoding Encoding { get; }
///
/// Uncompressed stream of gamestate file which can be extracted from save archive
///
///
/// Parsing whole save may take a few seconds on mid pc and takes 1GB of RAM,
/// so you should specify what exactly you want to get from save file
///
/// Encoding of the strings inside the save.
/// Size of the read buffer. Mostly useful for tests.
public SaveParserEU4(Stream savefile, SearchExpressionCompilation? query,
Encoding? encoding = null, int bufferSize = DefaultBufferSize)
{
Encoding = encoding ?? DefaultEncoding;
_query = query;
_tokens = new Tokenizer(savefile, bufferSize);
}
public Dictionary Parse()
{
// compiled here, so the keys of the query are always encoded like the save
_searchExprCurrent = _query?.Compile(Encoding);
_tokens.ReadHeader(Header, Encoding);
return ParseDict();
}
// doesn't move next
private object? ParseValue()
{
switch (_tokens.Type)
{
case TokenType.StringOrNumber:
return ParseScalar(_tokens.Text);
case TokenType.BracketOpen:
return ParseListOrDict();
case TokenType.BracketClose:
return null;
default:
throw new UnexpectedTokenException(_tokens, Encoding);
}
}
private object ParseScalar(ReadOnlySpan text)
{
// string values can be empty
if (text.Length == 0)
return string.Empty;
if (text.SequenceEqual("yes"u8))
return true;
if (text.SequenceEqual("no"u8))
return false;
byte first = text[0];
if (first != (byte)'-' && !char.IsAsciiDigit((char)first))
return DecodeString(text);
if (text.Contains((byte)'.')
&& double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double d))
return d;
if (long.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out long l))
return l;
return DecodeString(text);
}
private string DecodeString(ReadOnlySpan text)
{
// Skip control characters, which are invisible and causing frontend bugs.
// I dont know why there are so many of them in strings.
if (!text.ContainsAnyInRange((byte)0, (byte)0x1F))
return Encoding.GetString(text);
Span cleaned = text.Length <= 256 ? stackalloc byte[text.Length] : new byte[text.Length];
int length = 0;
foreach (byte b in text)
if (b >= 0x20)
cleaned[length++] = b;
return Encoding.GetString(cleaned[..length]);
}
// skips next value
/// true if skipped value, false if current token is closing bracket
private bool SkipValue()
{
switch (_tokens.Type)
{
case TokenType.BracketOpen:
_tokens.SkipBlock();
return true;
case TokenType.StringOrNumber:
return true;
case TokenType.BracketClose:
return false;
default:
throw new UnexpectedTokenException(_tokens, Encoding);
}
}
private static bool IsEmptyCollection(object value)
=> value is Dictionary { Count: 0 } or List