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 Parser(Stream savefile)
|
||||
private List<Token> _tokens = new();
|
||||
private int _tokenIndex;
|
||||
|
||||
public Parser(Stream savefile)
|
||||
{
|
||||
_saveFile = savefile;
|
||||
}
|
||||
|
||||
|
||||
protected enum TokenType
|
||||
{
|
||||
Invalid, String, Equals, BracketOpen, BracketClose,
|
||||
Invalid,
|
||||
String,
|
||||
Equals,
|
||||
BracketOpen,
|
||||
BracketClose,
|
||||
}
|
||||
|
||||
|
||||
protected struct Token
|
||||
{
|
||||
public TokenType type;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class UnexpectedTokenException : Exception
|
||||
{
|
||||
public UnexpectedTokenException(Token token, int tokenIndex) :
|
||||
base($"Unexpected token at index {tokenIndex}: {token}")
|
||||
{}
|
||||
}
|
||||
|
||||
public abstract SaveData Parse();
|
||||
|
||||
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 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 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");
|
||||
}
|
||||
@ -5,7 +5,9 @@ global using System.Threading.Tasks;
|
||||
global using DTLib.Demystifier;
|
||||
global using ParadoxSaveParser.Lib;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -39,30 +41,12 @@ public class Program
|
||||
|
||||
_app.UseHttpsRedirection();
|
||||
_app.MapGet("/getSaveStatus", GetSaveStatusHandler);
|
||||
_app.MapPost("/uploadSave/eu4", UploadSaveHandler);
|
||||
_app.MapPost("/parseSave/eu4", ParseSaveEU4Handler);
|
||||
_app.Run();
|
||||
}
|
||||
|
||||
private static async Task GetSaveStatusHandler(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)
|
||||
private static async Task UploadSaveHandler(HttpContext httpContext)
|
||||
{
|
||||
var remoteFile = httpContext.Request.Form.Files.FirstOrDefault();
|
||||
if (remoteFile is null || !remoteFile.FileName.EndsWith(".eu4"))
|
||||
@ -84,22 +68,75 @@ public class Program
|
||||
{
|
||||
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
|
||||
{
|
||||
meta.status = SaveFileProcessingStatus.Uploading;
|
||||
string saveFilePath = PathHelper.GetEU4SaveFilePath(meta.id);
|
||||
await using var saveFile = File.Open(saveFilePath, FileMode.CreateNew, FileAccess.ReadWrite);
|
||||
await using (var remoteStream = remoteFile.OpenReadStream())
|
||||
{
|
||||
await Task.Delay(50000);
|
||||
await remoteStream.CopyToAsync(saveFile);
|
||||
}
|
||||
|
||||
if(meta.status != SaveFileProcessingStatus.Uploaded)
|
||||
throw new Exception($"Invalid save processing status: {meta.status}");
|
||||
|
||||
using var zipArchive = ZipFile.Open(PathHelper.GetSaveFilePath(meta.id), ZipArchiveMode.Read);
|
||||
var zipEntry = zipArchive.Entries.FirstOrDefault(e => e.Name == "gamestate");
|
||||
if(zipEntry is null)
|
||||
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;
|
||||
saveFile.Seek(0, SeekOrigin.Begin);
|
||||
var parser = new ParserEU4(saveFile);
|
||||
string expectedHeader = "EU4txt";
|
||||
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();
|
||||
|
||||
meta.status = SaveFileProcessingStatus.SavingResults;
|
||||
string resultFilePath = PathHelper.GetParsedSaveFilePath(meta.id);
|
||||
await using var resultFile = File.Open(resultFilePath, FileMode.CreateNew, FileAccess.Write);
|
||||
@ -112,7 +149,8 @@ public class Program
|
||||
meta.status = SaveFileProcessingStatus.Error;
|
||||
string errorMesage = ex.ToStringDemystified();
|
||||
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);
|
||||
|
||||
@ -6,7 +6,7 @@ namespace ParadoxSaveParser.WebAPI;
|
||||
|
||||
public enum SaveFileProcessingStatus
|
||||
{
|
||||
Initialized, Uploading, Parsing, SavingResults, Done, Error
|
||||
Initialized, Uploading, Uploaded, Parsing, SavingResults, Done, Error
|
||||
}
|
||||
|
||||
public enum Game
|
||||
@ -23,9 +23,7 @@ public class SaveFileMetadata
|
||||
|
||||
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||
public required SaveFileProcessingStatus status { get; set; }
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public string? errorMesage { get; set; }
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user