implemented parser
This commit is contained in:
parent
0e122adcff
commit
05972fa40f
@ -1,29 +1,190 @@
|
|||||||
namespace ParadoxSaveParser.Lib;
|
using System.Text;
|
||||||
|
|
||||||
public abstract class Parser
|
namespace ParadoxSaveParser.Lib;
|
||||||
|
|
||||||
|
public class Parser
|
||||||
{
|
{
|
||||||
protected Stream _saveFile;
|
protected Stream _saveFile;
|
||||||
|
private List<Token> _tokens = new();
|
||||||
|
private int _tokenIndex;
|
||||||
|
|
||||||
protected Parser(Stream savefile)
|
public Parser(Stream savefile)
|
||||||
{
|
{
|
||||||
_saveFile = savefile;
|
_saveFile = savefile;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected enum TokenType
|
protected enum TokenType
|
||||||
{
|
{
|
||||||
Invalid, String, Equals, BracketOpen, BracketClose,
|
Invalid,
|
||||||
|
String,
|
||||||
|
Equals,
|
||||||
|
BracketOpen,
|
||||||
|
BracketClose,
|
||||||
}
|
}
|
||||||
|
|
||||||
protected struct Token
|
protected struct Token
|
||||||
{
|
{
|
||||||
public TokenType type;
|
public TokenType type;
|
||||||
public string? value;
|
public string? value;
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case TokenType.Invalid:
|
||||||
|
return "INVALID_TOKEN";
|
||||||
|
case TokenType.String:
|
||||||
|
return value ?? "NULL";
|
||||||
|
case TokenType.Equals:
|
||||||
|
return "=";
|
||||||
|
case TokenType.BracketOpen:
|
||||||
|
return "{";
|
||||||
|
case TokenType.BracketClose:
|
||||||
|
return "}";
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(type.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BuildAST()
|
protected void Lex()
|
||||||
{
|
{
|
||||||
|
_tokens.Clear();
|
||||||
|
StringBuilder str = new();
|
||||||
|
|
||||||
|
void CompleteStringToken()
|
||||||
|
{
|
||||||
|
if (str.Length > 0 && str[0] != '#')
|
||||||
|
{
|
||||||
|
_tokens.Add(new Token { type = TokenType.String, value = str.ToString() });
|
||||||
|
str.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_saveFile.CanRead)
|
||||||
|
{
|
||||||
|
int c = _saveFile.ReadByte();
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case -1:
|
||||||
|
CompleteStringToken();
|
||||||
|
return;
|
||||||
|
case ' ':
|
||||||
|
case '\t':
|
||||||
|
case '\n':
|
||||||
|
case '\r':
|
||||||
|
CompleteStringToken();
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
CompleteStringToken();
|
||||||
|
_tokens.Add(new Token { type = TokenType.Equals });
|
||||||
|
break;
|
||||||
|
case '{':
|
||||||
|
CompleteStringToken();
|
||||||
|
_tokens.Add(new Token { type = TokenType.BracketOpen });
|
||||||
|
break;
|
||||||
|
case '}':
|
||||||
|
CompleteStringToken();
|
||||||
|
_tokens.Add(new Token { type = TokenType.BracketClose });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
str.Append((char)c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract SaveData Parse();
|
protected class UnexpectedTokenException : Exception
|
||||||
|
{
|
||||||
|
public UnexpectedTokenException(Token token, int tokenIndex) :
|
||||||
|
base($"Unexpected token at index {tokenIndex}: {token}")
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private object? ParseValue()
|
||||||
|
{
|
||||||
|
Token tok = _tokens[_tokenIndex++];
|
||||||
|
switch (tok.type)
|
||||||
|
{
|
||||||
|
case TokenType.String:
|
||||||
|
return tok.value!;
|
||||||
|
case TokenType.BracketOpen:
|
||||||
|
return ParseListOrDict();
|
||||||
|
case TokenType.BracketClose:
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
throw new UnexpectedTokenException(tok, _tokenIndex - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object ParseListOrDict()
|
||||||
|
{
|
||||||
|
Token first = _tokens[_tokenIndex];
|
||||||
|
Token second = _tokens[_tokenIndex + 1];
|
||||||
|
if (first.type == TokenType.String && second.type == TokenType.Equals)
|
||||||
|
return ParseDict();
|
||||||
|
|
||||||
|
return ParseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<object> ParseList()
|
||||||
|
{
|
||||||
|
List<object> list = new();
|
||||||
|
Token tok = _tokens[_tokenIndex];
|
||||||
|
while (tok.type != TokenType.BracketClose)
|
||||||
|
{
|
||||||
|
object? value = ParseValue();
|
||||||
|
if (value == null)
|
||||||
|
break;
|
||||||
|
list.Add(value);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 (_tokenIndex < _tokens.Count)
|
||||||
|
{
|
||||||
|
Token tok = _tokens[_tokenIndex++];
|
||||||
|
if (tok.type == TokenType.BracketClose)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if(tok.type != TokenType.String)
|
||||||
|
throw new UnexpectedTokenException(tok, _tokenIndex - 1);
|
||||||
|
|
||||||
|
string key = tok.value!;
|
||||||
|
|
||||||
|
tok = _tokens[_tokenIndex++];
|
||||||
|
if(tok.type != TokenType.Equals)
|
||||||
|
throw new UnexpectedTokenException(tok, _tokenIndex - 1);
|
||||||
|
|
||||||
|
|
||||||
|
object? value = ParseValue();
|
||||||
|
if (value == null)
|
||||||
|
throw new UnexpectedTokenException(_tokens[_tokenIndex - 1], _tokenIndex - 1);
|
||||||
|
if(!dict.TryGetValue(key, out List<object>? list))
|
||||||
|
{
|
||||||
|
list = new List<object>();
|
||||||
|
dict.Add(key, list);
|
||||||
|
}
|
||||||
|
list.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, List<object>> Parse()
|
||||||
|
{
|
||||||
|
Lex();
|
||||||
|
if (_tokens.Count == 0)
|
||||||
|
throw new Exception("Save file is empty");
|
||||||
|
|
||||||
|
_tokenIndex = 0;
|
||||||
|
var root = ParseDict();
|
||||||
|
return root;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
namespace ParadoxSaveParser.Lib;
|
|
||||||
|
|
||||||
public class ParserEU4 : Parser
|
|
||||||
{
|
|
||||||
public ParserEU4(Stream savefile) : base(savefile)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override SaveData Parse()
|
|
||||||
{
|
|
||||||
var saveData = new SaveData();
|
|
||||||
|
|
||||||
return saveData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
namespace ParadoxSaveParser.Lib;
|
|
||||||
|
|
||||||
public class SaveData
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -7,6 +7,6 @@ public static class PathHelper
|
|||||||
public const string DATA_DIR = "data";
|
public const string DATA_DIR = "data";
|
||||||
public static string SAVES_DIR = Path.Join(DATA_DIR, "saves");
|
public static string SAVES_DIR = Path.Join(DATA_DIR, "saves");
|
||||||
public static string GetMetaFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".meta.json");
|
public static string GetMetaFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".meta.json");
|
||||||
public static string GetEU4SaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".eu4");
|
public static string GetSaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".eu4");
|
||||||
public static string GetParsedSaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".parsed.json");
|
public static string GetParsedSaveFilePath(string save_id) => Path.Join(SAVES_DIR, save_id + ".parsed.json");
|
||||||
}
|
}
|
||||||
@ -5,7 +5,9 @@ global using System.Threading.Tasks;
|
|||||||
global using DTLib.Demystifier;
|
global using DTLib.Demystifier;
|
||||||
global using ParadoxSaveParser.Lib;
|
global using ParadoxSaveParser.Lib;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -39,30 +41,12 @@ public class Program
|
|||||||
|
|
||||||
_app.UseHttpsRedirection();
|
_app.UseHttpsRedirection();
|
||||||
_app.MapGet("/getSaveStatus", GetSaveStatusHandler);
|
_app.MapGet("/getSaveStatus", GetSaveStatusHandler);
|
||||||
|
_app.MapPost("/uploadSave/eu4", UploadSaveHandler);
|
||||||
_app.MapPost("/parseSave/eu4", ParseSaveEU4Handler);
|
_app.MapPost("/parseSave/eu4", ParseSaveEU4Handler);
|
||||||
_app.Run();
|
_app.Run();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task GetSaveStatusHandler(HttpContext httpContext)
|
private static async Task UploadSaveHandler(HttpContext httpContext)
|
||||||
{
|
|
||||||
httpContext.Request.Query.TryGetValue("id", out var ids);
|
|
||||||
string? id = ids.FirstOrDefault();
|
|
||||||
if (string.IsNullOrEmpty(id))
|
|
||||||
{
|
|
||||||
throw new BadHttpRequestException("No id provided",
|
|
||||||
StatusCodes.Status400BadRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_saveMetadataStorage.TryGetValue(id, out var meta))
|
|
||||||
{
|
|
||||||
throw new BadHttpRequestException($"Save with {id} not found",
|
|
||||||
StatusCodes.Status400BadRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
await httpContext.Response.WriteAsJsonAsync(meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task ParseSaveEU4Handler(HttpContext httpContext)
|
|
||||||
{
|
{
|
||||||
var remoteFile = httpContext.Request.Form.Files.FirstOrDefault();
|
var remoteFile = httpContext.Request.Form.Files.FirstOrDefault();
|
||||||
if (remoteFile is null || !remoteFile.FileName.EndsWith(".eu4"))
|
if (remoteFile is null || !remoteFile.FileName.EndsWith(".eu4"))
|
||||||
@ -85,21 +69,74 @@ public class Program
|
|||||||
throw new BadHttpRequestException($"Guid collision! Can't create metadata with id {saveId}", StatusCodes.Status500InternalServerError);
|
throw new BadHttpRequestException($"Guid collision! Can't create metadata with id {saveId}", StatusCodes.Status500InternalServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
meta.status = SaveFileProcessingStatus.Uploading;
|
||||||
|
string saveFilePath = PathHelper.GetSaveFilePath(meta.id);
|
||||||
|
await using var saveFile = File.Open(saveFilePath, FileMode.CreateNew, FileAccess.ReadWrite);
|
||||||
|
await using var remoteStream = remoteFile.OpenReadStream();
|
||||||
|
await remoteStream.CopyToAsync(saveFile);
|
||||||
|
meta.status = SaveFileProcessingStatus.Uploaded;
|
||||||
|
|
||||||
|
await httpContext.Response.WriteAsJsonAsync(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SaveFileMetadata GetMetaFromRequestId(HttpContext httpContext, string requestParamName)
|
||||||
|
{
|
||||||
|
httpContext.Request.Query.TryGetValue(requestParamName, out var ids);
|
||||||
|
string? id = ids.FirstOrDefault();
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
throw new BadHttpRequestException($"No request parameter '{requestParamName}' provided",
|
||||||
|
StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_saveMetadataStorage.TryGetValue(id, out var meta))
|
||||||
|
{
|
||||||
|
throw new BadHttpRequestException($"Save with {id} not found",
|
||||||
|
StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task GetSaveStatusHandler(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var meta = GetMetaFromRequestId(httpContext, "id");
|
||||||
|
await httpContext.Response.WriteAsJsonAsync(meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ParseSaveEU4Handler(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var meta = GetMetaFromRequestId(httpContext, "id");
|
||||||
|
if (meta.status == SaveFileProcessingStatus.Error)
|
||||||
|
{
|
||||||
|
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
await httpContext.Response.WriteAsJsonAsync(meta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
meta.status = SaveFileProcessingStatus.Uploading;
|
if(meta.status != SaveFileProcessingStatus.Uploaded)
|
||||||
string saveFilePath = PathHelper.GetEU4SaveFilePath(meta.id);
|
throw new Exception($"Invalid save processing status: {meta.status}");
|
||||||
await using var saveFile = File.Open(saveFilePath, FileMode.CreateNew, FileAccess.ReadWrite);
|
|
||||||
await using (var remoteStream = remoteFile.OpenReadStream())
|
using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta.id), ZipArchiveMode.Read);
|
||||||
{
|
var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate");
|
||||||
await Task.Delay(50000);
|
if(zipEntry is null)
|
||||||
await remoteStream.CopyToAsync(saveFile);
|
throw new Exception("Invalid save format: no gamestate file found");
|
||||||
}
|
string extractedGamestatePath = PathHelper.GetSaveFilePath(meta.id) + ".gamestate";
|
||||||
|
zipEntry.ExtractToFile(extractedGamestatePath);
|
||||||
|
var gamestateStream = File.Open(extractedGamestatePath, FileMode.Open, FileAccess.Read);
|
||||||
|
|
||||||
meta.status = SaveFileProcessingStatus.Parsing;
|
meta.status = SaveFileProcessingStatus.Parsing;
|
||||||
saveFile.Seek(0, SeekOrigin.Begin);
|
string expectedHeader = "EU4txt";
|
||||||
var parser = new ParserEU4(saveFile);
|
byte[] headBytes = new byte[expectedHeader.Length];
|
||||||
|
gamestateStream.ReadExactly(headBytes);
|
||||||
|
string headStr = Encoding.UTF8.GetString(headBytes);
|
||||||
|
if(headStr != expectedHeader)
|
||||||
|
throw new Exception($"Invalid gamestate header: '{headStr}'");
|
||||||
|
var parser = new Parser(gamestateStream);
|
||||||
var result = parser.Parse();
|
var result = parser.Parse();
|
||||||
|
|
||||||
meta.status = SaveFileProcessingStatus.SavingResults;
|
meta.status = SaveFileProcessingStatus.SavingResults;
|
||||||
string resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id);
|
string resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id);
|
||||||
await using var resultFile = File.Open(resultFilePath, FileMode.CreateNew, FileAccess.Write);
|
await using var resultFile = File.Open(resultFilePath, FileMode.CreateNew, FileAccess.Write);
|
||||||
@ -112,7 +149,8 @@ public class Program
|
|||||||
meta.status = SaveFileProcessingStatus.Error;
|
meta.status = SaveFileProcessingStatus.Error;
|
||||||
string errorMesage = ex.ToStringDemystified();
|
string errorMesage = ex.ToStringDemystified();
|
||||||
meta.errorMesage = errorMesage;
|
meta.errorMesage = errorMesage;
|
||||||
_app.Logger.Log(LogLevel.Error, "EU4SaveParse Error: {errorMesage}", errorMesage);
|
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
_app.Logger.Log(LogLevel.Error, "ParseSaveEU4 Error: {errorMesage}", errorMesage);
|
||||||
}
|
}
|
||||||
|
|
||||||
await httpContext.Response.WriteAsJsonAsync(meta);
|
await httpContext.Response.WriteAsJsonAsync(meta);
|
||||||
|
|||||||
@ -6,7 +6,7 @@ namespace ParadoxSaveParser.WebAPI;
|
|||||||
|
|
||||||
public enum SaveFileProcessingStatus
|
public enum SaveFileProcessingStatus
|
||||||
{
|
{
|
||||||
Initialized, Uploading, Parsing, SavingResults, Done, Error
|
Initialized, Uploading, Uploaded, Parsing, SavingResults, Done, Error
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Game
|
public enum Game
|
||||||
@ -24,8 +24,6 @@ public class SaveFileMetadata
|
|||||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
public required SaveFileProcessingStatus status { get; set; }
|
public required SaveFileProcessingStatus status { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public string? errorMesage { get; set; }
|
public string? errorMesage { get; set; }
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user