Created
April 14, 2025 22:58
-
-
Save pjmagee/4c6d8c4b71e125853f879e179ac354b3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
void Main() | |
{ | |
// Read the file and get the root PdxObject. | |
PdxObject root = MageeSoft.PDX.CE.PdxSaveReader | |
.Read(File.ReadAllText(@"D:\paradox-clausewitz-sav\SRC\MageeSoft.PDX.CE.Models\gamestate.csf").AsSpan()); | |
root.Dump(); | |
// Process the PdxObject structure into a schema. | |
var schema = Schema.From(root, "GameState"); | |
schema.Dump(); | |
} | |
public static class Extensions | |
{ | |
public static string ToTitleCase(this string value) | |
{ | |
return string.Join(string.Empty, value | |
.Split('_', StringSplitOptions.RemoveEmptyEntries) | |
.Select(x => CultureInfo.InvariantCulture.TextInfo.ToTitleCase(x))); | |
} | |
public static bool IsPdxDictionary(this PdxObject pdxObject) | |
{ | |
return pdxObject.Properties.All(x => x.Key is PdxInt or PdxLong); | |
} | |
public static bool IsPdxIntDictionary(this PdxObject pdxObject) | |
{ | |
return pdxObject.Properties.All(x => x.Key is PdxInt); | |
} | |
public static bool IsPdxLongDictionary(this PdxObject pdxObject) | |
{ | |
return pdxObject.Properties.Any(x => x.Key is PdxLong); | |
} | |
} | |
public class Schema : IEquatable<Schema> | |
{ | |
public required string ClassName { get; set; } | |
public HashSet<SchemaProperty> Properties { get; set; } = new(); | |
public HashSet<Schema> Schemas { get; set; } = new(); | |
/// <summary> | |
/// Public entry point – creates a "Class" root schema from the root PdxObject. | |
/// </summary> | |
public static Schema From(PdxObject root, string className) | |
{ | |
return FromIterative(root, className); | |
} | |
/// <summary> | |
/// Iteratively processes the PdxObject structure starting with the given className. | |
/// </summary> | |
private static Schema FromIterative(PdxObject root, string key) | |
{ | |
var schema = new Schema { ClassName = key }; | |
var workStack = new Stack<(Schema schema, IPdxElement pdxElement)>(); | |
workStack.Push((schema, root)); | |
while (workStack.Count > 0) | |
{ | |
var (currentSchema, currentPdx) = workStack.Pop(); | |
if (currentPdx is PdxObject pdxObject) | |
{ | |
/* key={ 1={} 2={} 3={} } */ | |
if (pdxObject.Properties.Length > 0 && pdxObject.IsPdxDictionary()) | |
{ | |
// if we end up with a PdxString this could be a PdxString=None | |
if (pdxObject.Properties.Any(p => p.Value.Type == PdxType.Object) && | |
pdxObject.Properties.Any(p => p.Value.Type == PdxType.String && ((PdxString)p.Value).Value == "none") && | |
pdxObject.Properties.All(p => p.Value.Type == PdxType.String || p.Value.Type == PdxType.Object)) | |
{ | |
var childSchemas = new List<Schema>(); | |
foreach (var kvp in pdxObject.Properties) | |
{ | |
if (kvp.Value is PdxString childStr) | |
{ | |
if (childStr.Value == "none") continue; | |
else throw new Exception("Not expected"); | |
} | |
else if (kvp.Value is PdxObject childObj) | |
{ | |
var childSchema = FromIterative(childObj, currentSchema.ClassName + "_Child"); | |
childSchemas.Add(childSchema); | |
} | |
} | |
// Merge properties from all processed dictionary child schemas. | |
var mergeSchema = new Schema { ClassName = currentSchema.ClassName + "_Merged" }; | |
foreach (var childSchema in childSchemas) | |
{ | |
foreach (var prop in childSchema.Properties) | |
{ | |
mergeSchema.Properties.Add(prop); | |
} | |
foreach (var sub in childSchema.Schemas) | |
{ | |
mergeSchema.Schemas.Add(sub); | |
} | |
} | |
currentSchema.Properties.Add(new SchemaProperty(currentSchema.ClassName + "_Dictionary", $"Dictionary<int, {mergeSchema.ClassName}>")); | |
currentSchema.Schemas.Add(mergeSchema); | |
} | |
// Once the dictionary branch is processed, skip other property processing. | |
continue; | |
} | |
else | |
{ | |
// Process regular properties that use string keys. | |
foreach (var typeGroup in pdxObject.Properties.GroupBy(x => x.Key.Type)) | |
{ | |
if (typeGroup.Key == PdxType.String) | |
{ | |
var stringKeys = typeGroup.GroupBy(x => ((PdxString)x.Key).Value); | |
foreach (var stringKey in stringKeys) | |
{ | |
if (stringKey.Count() == 1) | |
{ | |
var kvp = stringKey.Single(); | |
// unique_key= | |
if (kvp.Key is PdxString pdxStr) | |
{ | |
string propName = pdxStr.Value.ToTitleCase(); | |
var value = kvp.Value; | |
switch (value) | |
{ | |
// key="my_quoted_value" | |
case PdxString pdxString and { WasQuoted: true }: | |
currentSchema.Properties.Add(new SchemaProperty(propName, "string")); | |
break; | |
// key=my_unquoted_value | |
case PdxString pdxString and { WasQuoted: false }: | |
currentSchema.Properties.Add(new SchemaProperty(propName, "string")); | |
break; | |
// key=1 | |
case PdxInt pdxInt: | |
currentSchema.Properties.Add(new SchemaProperty(propName, "int")); | |
break; | |
// key=1.0 | |
case PdxFloat pdxFloat: | |
currentSchema.Properties.Add(new SchemaProperty(propName, "double")); | |
break; | |
case PdxLong pdxLong: | |
currentSchema.Properties.Add(new SchemaProperty(propName, "long")); | |
break; | |
// key=yes or key=no | |
case PdxBool pdxBool: | |
currentSchema.Properties.Add(new SchemaProperty(propName, "bool")); | |
break; | |
// key="2000.01.01" | |
case PdxDate pdxDate: | |
currentSchema.Properties.Add(new SchemaProperty(propName, nameof(DateOnly))); | |
break; | |
// key={ item1 item2 item 3 } | |
case PdxArray pdxArray: | |
{ | |
// key={ 1 2 3 } | |
if (pdxArray.Items.All(x => x is PdxInt)) | |
{ | |
currentSchema.Properties.Add(new SchemaProperty(propName, "List<int>")); | |
} | |
// key={ 9999999999999 9999999999999 9999999999999 } | |
else if (pdxArray.Items.All(x => x is PdxLong)) | |
{ | |
currentSchema.Properties.Add(new SchemaProperty(propName, "List<long>")); | |
} | |
// key={ a b c } | |
else if (pdxArray.Items.All(x => x is PdxString)) | |
{ | |
currentSchema.Properties.Add(new SchemaProperty(propName, "List<string>")); | |
} | |
// key={ 1.0 2.0 3.0} | |
else if (pdxArray.Items.All(x => x is PdxFloat)) | |
{ | |
currentSchema.Properties.Add(new SchemaProperty(propName, "List<double>")); | |
} | |
// key={ yes no yes no } | |
else if (pdxArray.Items.All(x => x is PdxBool)) | |
{ | |
currentSchema.Properties.Add(new SchemaProperty(propName, "List<bool>")); | |
} | |
// key={ {}{}{} } | |
else if (pdxArray.Items.All(x => x is PdxObject)) | |
{ | |
var childSchemas = new List<Schema>(); | |
foreach (var item in pdxArray.Items) | |
{ | |
if (item is PdxObject childObj) | |
{ | |
var childSchema = FromIterative(childObj, currentSchema.ClassName + "_" + propName + "_Child"); | |
childSchemas.Add(childSchema); | |
} | |
} | |
// Merge properties from all processed dictionary child schemas. | |
var mergeSchema = new Schema { ClassName = currentSchema.ClassName + "_" + propName + "_Merged" }; | |
foreach (var childSchema in childSchemas) | |
{ | |
foreach (var prop in childSchema.Properties) | |
{ | |
mergeSchema.Properties.Add(prop); | |
} | |
foreach (var sub in childSchema.Schemas) | |
{ | |
mergeSchema.Schemas.Add(sub); | |
} | |
} | |
var finalPropName = (currentSchema.ClassName + "_" + propName + "_Items").ToTitleCase(); | |
currentSchema.Properties.Add(new SchemaProperty(finalPropName, $"List<{mergeSchema.ClassName}>")); | |
currentSchema.Schemas.Add(mergeSchema); | |
} | |
} | |
break; | |
case PdxObject childObject: | |
{ | |
var childSchema = new Schema { ClassName = propName }; | |
if (childObject.IsPdxIntDictionary()) | |
{ | |
currentSchema.Properties.Add(new SchemaProperty(propName, $"Dictionary<int,{childSchema.ClassName}>")); | |
} | |
currentSchema.Schemas.Add(childSchema); | |
workStack.Push((childSchema, childObject)); | |
} | |
break; | |
} | |
} | |
} | |
else | |
{ | |
// { asteroid_postfix={ "1" "2" "3" } asteroid_postfix={ "1" "2" "3" } asteroid_postfix={ "1" "2" "3" } } | |
if (stringKey.All(kvp => kvp.Value is PdxArray arr && arr.Items.All(i => i is PdxString))) | |
{ | |
string propName = stringKey.First().Key.Value(); | |
currentSchema.Properties.Add(new SchemaProperty(propName, $"List<List<string>>")); | |
} | |
// { key={ 1 2 3 } key={ 1 2 3 } key={ 1 2 3 } | |
else if (stringKey.All(kvp => kvp.Value is PdxArray arr && arr.Items.All(i => i is PdxInt))) | |
{ | |
string propName = stringKey.First().Key.Value(); | |
currentSchema.Properties.Add(new SchemaProperty(propName, $"List<List<int>>")); | |
} | |
else if (stringKey.All(kvp => kvp.Value is PdxArray arr && arr.Items.All(i => i is PdxBool))) | |
{ | |
string propName = stringKey.First().Key.Value(); | |
currentSchema.Properties.Add(new SchemaProperty(propName, $"List<List<bool>>")); | |
} | |
else if (stringKey.All(kvp => kvp.Value is PdxArray arr && arr.Items.All(i => i is PdxFloat))) | |
{ | |
string propName = stringKey.First().Key.Value(); | |
currentSchema.Properties.Add(new SchemaProperty(propName, $"List<List<double>>")); | |
} | |
else if (stringKey.All(kvp => kvp.Value is PdxArray arr && arr.Items.All(i => i is PdxObject))) | |
{ | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
else if (currentPdx is PdxArray pdxArray) | |
{ | |
var childSchemas = new List<Schema>(); | |
foreach (var item in pdxArray.Items) | |
{ | |
if (item is PdxObject childObj) | |
{ | |
var childSchema = FromIterative(childObj, currentSchema.ClassName + "_" + key + "_Child"); | |
childSchemas.Add(childSchema); | |
} | |
} | |
// Merge properties from all processed dictionary child schemas. | |
var mergeSchema = new Schema { ClassName = currentSchema.ClassName + "_" + key + "_Merged" }; | |
foreach (var childSchema in childSchemas) | |
{ | |
foreach (var prop in childSchema.Properties) | |
{ | |
mergeSchema.Properties.Add(prop); | |
} | |
foreach (var sub in childSchema.Schemas) | |
{ | |
mergeSchema.Schemas.Add(sub); | |
} | |
} | |
var finalPropName = (currentSchema.ClassName + "_" + key + "_Items").ToTitleCase(); | |
currentSchema.Properties.Add(new SchemaProperty(finalPropName, $"List<{mergeSchema.ClassName}>")); | |
currentSchema.Schemas.Add(mergeSchema); | |
} | |
} | |
return schema; | |
} | |
public bool Equals(Schema? other) | |
{ | |
return other is not null && ClassName == other.ClassName && Properties.SetEquals(other.Properties); | |
} | |
public override int GetHashCode() | |
{ | |
return HashCode.Combine(ClassName.GetHashCode(), Properties.Select(p => p.GetHashCode()).Aggregate(0, HashCode.Combine)); | |
} | |
} | |
public class SchemaProperty : IEquatable<SchemaProperty> | |
{ | |
public string Name { get; set; } | |
public string PropertyType { get; set; } | |
public SchemaProperty(string name, string propertyType) | |
{ | |
Name = name; | |
PropertyType = propertyType; | |
} | |
public bool Equals(SchemaProperty? other) | |
{ | |
return Name.Equals(other!.Name) && other.PropertyType.Equals(other.PropertyType); | |
} | |
public override int GetHashCode() | |
{ | |
return HashCode.Combine(Name.GetHashCode(), PropertyType.GetHashCode()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment