Compare commits

..

3 Commits

Author SHA1 Message Date
c1d1361d50 new interactive mode 2025-04-06 01:43:27 +05:00
b0c8841250 created project ParadoxSaveParser.CLI 2025-04-06 00:38:13 +05:00
a8c4361512 do dot add empty collections into list 2025-04-06 00:37:12 +05:00
10 changed files with 302 additions and 19 deletions

View File

@ -0,0 +1,156 @@
using System.Text.Json;
using ParadoxSaveParser.Lib;
namespace ParadoxSaveParser.CLI;
internal enum Mode
{
Unset, Search, Interactive
}
internal static class Modes
{
internal static void Search(string searchQuery, IOPath inputPath, IOPath? outputPath)
{
using var inputStream = File.OpenRead(inputPath);
using var outputStream = outputPath is null
? Console.OpenStandardOutput()
: File.OpenWrite(outputPath.Value);
var searchExpression = SearchExpressionCompiler.Compile(searchQuery);
var parser = new SaveParserEU4(inputStream, searchExpression);
var parsedValue = parser.Parse();
JsonSerializer.Serialize(outputStream, parsedValue, ParsedValueJsonContext.Default.DictionaryStringListObject);
}
internal static void Interactive()
{
Console.Clear();
Console.ResetColor();
ColoredConsole.WriteTitle("interactive mode", fg: ConsoleColor.Cyan);
ColoredConsole.WriteLine($"working directory: '{Environment.CurrentDirectory}'", ConsoleColor.Gray);
IOPath? inputPath = null;
IOPath? outputPath = null;
while (true)
{
try
{
ColoredConsole.Write("> ", ConsoleColor.Blue);
Console.ForegroundColor = ConsoleColor.Gray;
string? input = Console.ReadLine();
if (string.IsNullOrEmpty(input))
continue;
const string helpMessage =
"""
Commands dont have arguments. Just write command name ant it will ask you for more information.
Avaliable commands:
h, help - show this message
q, quit, exit - close the program
pwd - show working directory
cd - change working directory
ls - show list of files in working directory
i, input - set input file path
o, output - set output file path
s, search - perform search in input file using expression
""";
if (input.Contains(' '))
{
ColoredConsole.WriteLine("Commands dont have arguments."
+ " Just write command name ant it will ask you for more information.",
ConsoleColor.Red);
continue;
}
switch (input)
{
default:
ColoredConsole.WriteLine("Unknown command, use 'help' for a list of commands.",
ConsoleColor.Red);
continue;
case "q":
case "quit":
case "exit":
return;
case "h":
case "help":
ColoredConsole.WriteLine(helpMessage, fg: ConsoleColor.White);
break;
case "i":
case "input":
input = ColoredConsole.ReadLine("Input file path",
ConsoleColor.Blue);
if (string.IsNullOrEmpty(input))
throw new NullReferenceException();
inputPath = input;
break;
case "o":
case "output":
input = ColoredConsole.ReadLine("Output file path [default=stdout]",
ConsoleColor.Blue);
if (string.IsNullOrEmpty(input))
throw new ArgumentException("Input file path is required");
inputPath = input;
break;
case "s":
case "search":
if (inputPath is null)
throw new ArgumentException("Input file path is required");
var searchQuery = ColoredConsole.ReadLine("search expression",
ConsoleColor.Blue);
if (string.IsNullOrEmpty(searchQuery))
throw new ArgumentException("Search expression is required");
ColoredConsole.WriteHLine('-', ConsoleColor.Cyan);
Console.ResetColor();
Search(searchQuery, inputPath.Value, outputPath);
ColoredConsole.WriteHLine('-', ConsoleColor.Green);
break;
case "pwd":
ColoredConsole.WriteLine(Environment.CurrentDirectory, ConsoleColor.White);
break;
case "cd":
input = ColoredConsole.ReadLine("Change working directory to",
ConsoleColor.Blue);
if (!string.IsNullOrEmpty(input))
{
Environment.CurrentDirectory = new IOPath(input).Str;
ColoredConsole.WriteLine(Environment.CurrentDirectory, ConsoleColor.Green);
}
break;
case "ls":
IOPath curdir = Directory.GetCurrent();
foreach (var dir in Directory.GetDirectories(curdir))
{
ColoredConsole.WriteLine(dir.RemoveBase(curdir).Str + Path.Sep, ConsoleColor.Cyan);
}
foreach (var file in Directory.GetFiles(curdir))
{
ColoredConsole.WriteLine(file.RemoveBase(curdir).Str, ConsoleColor.White);
}
break;
}
}
catch (Exception ex)
{
ColoredConsole.WriteLine(ex.ToStringDemystified(), ConsoleColor.Red);
}
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ParadoxSaveParser.Lib\ParadoxSaveParser.Lib.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DTLib" Version="1.6.5"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,66 @@
global using System;
global using DTLib.Console;
global using DTLib.Demystifier;
global using DTLib.Filesystem;
global using Directory = DTLib.Filesystem.Directory;
global using File = DTLib.Filesystem.File;
using ParadoxSaveParser.CLI;
try
{
Mode mode = Mode.Unset;
IOPath? inputPath = null;
IOPath? outputPath = null;
string? searchQuery = null;
new LaunchArgumentParser(
new LaunchArgument(["-i", "--input"],
"Set input file path",
s => inputPath = s,
"gamestate or zip file"),
new LaunchArgument(["-o", "--output"],
"Set output file path",
s => outputPath = s,
"json file [default=stdout]"),
new LaunchArgument(["-s", "--search"],
"Search in input file",
s =>
{
searchQuery = s;
mode = Mode.Search;
},
"search expression")
)
.WithNoExit()
.ParseAndHandle(args);
if (args.Length == 0)
mode = Mode.Interactive;
switch (mode)
{
default:
throw new ArgumentOutOfRangeException(nameof(mode));
case Mode.Unset:
throw new Exception("No action specified");
case Mode.Search:
if (string.IsNullOrEmpty(searchQuery))
throw new ArgumentException("Search expression is required");
if (inputPath is null)
throw new ArgumentException("Input file path is required");
Modes.Search(searchQuery, inputPath.Value, outputPath);
break;
case Mode.Interactive:
Modes.Interactive();
break;
}
}
catch (Exception ex)
{
ColoredConsole.WriteLine(ex.ToStringDemystified(), ConsoleColor.Red);
Console.ResetColor();
Environment.Exit(1);
}
Console.ResetColor();

View File

@ -1,21 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0"/> <ProjectReference Include="..\ParadoxSaveParser.Lib\ParadoxSaveParser.Lib.csproj"/>
<PackageReference Include="DTLib" Version="1.6.5"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
<PackageReference Include="NUnit" Version="3.14.0"/>
<PackageReference Include="NUnit.Analyzers" Version="3.9.0"/>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -23,7 +19,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ParadoxSaveParser.Lib\ParadoxSaveParser.Lib.csproj"/> <PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="DTLib" Version="1.6.5"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.7.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,3 +1,4 @@
using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using DTLib.Extensions; using DTLib.Extensions;

View File

@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.3" /> <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace ParadoxSaveParser.Lib;
[JsonSourceGenerationOptions(MaxDepth = 1024, WriteIndented = true)]
[JsonSerializable(typeof(Dictionary<string, List<object>>))]
[JsonSerializable(typeof(List<object>))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(long))]
[JsonSerializable(typeof(double))]
public partial class ParsedValueJsonContext : JsonSerializerContext
{
}

View File

@ -215,6 +215,9 @@ public class SaveParserEU4
} }
} }
private static bool IsEmptyCollection(object value)
=> value is Dictionary<string, List<object>> { Count: 0 } or List<object> { Count: 0 };
// doesn't move next // doesn't move next
private object ParseListOrDict() private object ParseListOrDict()
{ {
@ -250,6 +253,10 @@ public class SaveParserEU4
if (value is null) if (value is null)
break; break;
// do dot add empty collections into list
if (IsEmptyCollection(value))
continue;
list.Add(value); list.Add(value);
} }
@ -326,6 +333,10 @@ public class SaveParserEU4
dict.Add(keyStr, list); dict.Add(keyStr, list);
} }
// do dot add empty collections into list
if (IsEmptyCollection(value))
continue;
list.Add(value); list.Add(value);
} }

View File

@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.Lib.Tests", "ParadoxSaveParser.Lib.Tests\ParadoxSaveParser.Lib.Tests.csproj", "{23F4BE1B-3043-4821-9F65-74FF5F57FA59}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.Lib.Tests", "ParadoxSaveParser.Lib.Tests\ParadoxSaveParser.Lib.Tests.csproj", "{23F4BE1B-3043-4821-9F65-74FF5F57FA59}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.CLI", "ParadoxSaveParser.CLI\ParadoxSaveParser.CLI.csproj", "{2D4448A6-390D-47F3-9BB7-6266669719DE}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -30,5 +32,9 @@ Global
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU {23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU {23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.Build.0 = Release|Any CPU {23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.Build.0 = Release|Any CPU
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,11 +1,14 @@
Parser.CLI:
Temp files management system
Main: Main:
Add temporary files deletion Temp files management system
Database???
Add query to get parsed data
ParseSaveHandler: ParseSaveHandler:
Make this method run as background task instead of POST query Make this method run as background task instead of POST query
Add debug log Add debug log
Save parsed in protobuf Save parsed data in protobuf
Re-parse if saved data was parsed with another query Re-parse if saved data was parsed with another query
Parser:
Optimize it (5 sec per query isn't good)