Skip to content

Instantly share code, notes, and snippets.

@pjmagee
Created April 14, 2025 22:58
Show Gist options
  • Save pjmagee/4c6d8c4b71e125853f879e179ac354b3 to your computer and use it in GitHub Desktop.
Save pjmagee/4c6d8c4b71e125853f879e179ac354b3 to your computer and use it in GitHub Desktop.
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