-
Notifications
You must be signed in to change notification settings - Fork 6
Serialization/conversion rework for round-trip stability and performance #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
bdbffae
feat: allow simplified readback of xnb asset type
Krzyhau bd33622
feat: improve performance of lzx decoder
Krzyhau 1f8b2b4
feat: change reflection serialization to sourcegen
Krzyhau e06a680
feat: clear up null reference conversion
Krzyhau d0a2935
feat: use full assembly qualifier naming in xnbs
Krzyhau faaad57
fix: ensure correct length in decompressed xnb
Krzyhau bf900d4
fix: simplify xnb decompress stream
Krzyhau 6586ed7
feat: ensure accurate texture coords in ao
Krzyhau 4a866d7
fix: ensure correct reader list in texture2d asset
Krzyhau 688144c
feat: add content type claim for XNB FQN list
Krzyhau ff63dd7
fix: guarantee same ordering in dictionaries
Krzyhau 9309c67
fix: fix potential issue with json emplacement
Krzyhau 1dd8d3f
fix: ensure conversion loop for level asset type
Krzyhau 74784ba
feat: ensure round-trip conversion for trilesets
Krzyhau 99c378c
feat: resolve soundeffect round-tripping issues
Krzyhau a4e30c5
fix: resolve issues with XNB content type names
Krzyhau 5ac617b
feat: use more accurate atlas sprite packing
Krzyhau 009a6c8
fix: fix sprite fonts content serialization
Krzyhau fc0b3dc
fix: convert nullable default char in sprite font
Krzyhau 3b64e81
fix: implement minor reader optimisations
Krzyhau e67635a
feat: rework dxt utility for compression support
Krzyhau 7354431
feat: include texture surface type in png metadata
Krzyhau 01c9b6f
feat: add json conversion contexts sourcegen
Krzyhau 9dbe6bc
feat: minimize list indexing in ordereddictionary
Krzyhau 16805a0
refactor: rework conversion flag handling
Krzyhau f8af1ad
feat: allow sheet export for animated textures
Krzyhau 6c75c58
refactor: get rid of xnbcompressor wrapper class
Krzyhau File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| using System.Text; | ||
|
|
||
| namespace FEZRepacker.Core.SourceGen | ||
| { | ||
| public class CodeStringBuilder | ||
| { | ||
| private readonly StringBuilder _stringBuilder; | ||
| private int _currentIndent; | ||
| private bool _shouldIndent = false; | ||
|
|
||
| public CodeStringBuilder() | ||
| { | ||
| _stringBuilder = new StringBuilder(); | ||
| _currentIndent = 0; | ||
| } | ||
|
|
||
| public void Append(string s) | ||
| { | ||
| if (_shouldIndent) | ||
| { | ||
| _stringBuilder.Append(new string(' ', _currentIndent * 4)); | ||
| _shouldIndent = false; | ||
| } | ||
| _stringBuilder.Append(s); | ||
| } | ||
|
|
||
| public void AppendLine() | ||
| { | ||
| _stringBuilder.AppendLine(); | ||
| _shouldIndent = true; | ||
| } | ||
|
|
||
| public void AppendLine(string s) | ||
| { | ||
| Append(s); | ||
| AppendLine(); | ||
| } | ||
|
|
||
| public void BeginCodeBlock(string openCharacter = "{") | ||
| { | ||
| AppendLine(openCharacter); | ||
| _currentIndent++; | ||
| } | ||
|
|
||
| public void EndCodeBlock(string closeCharacter = "}") | ||
| { | ||
| if(_currentIndent > 0) _currentIndent--; | ||
| AppendLine(closeCharacter); | ||
| } | ||
|
|
||
| public override string ToString() => _stringBuilder.ToString(); | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,337 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
|
|
||
| namespace FEZRepacker.Core.SourceGen; | ||
|
|
||
| [Generator] | ||
| public sealed class ContentSerializerGenerator : IIncrementalGenerator | ||
| { | ||
| private const string XnbReaderTypeAttributeName = "FEZRepacker.Core.Definitions.Game.XnbReaderTypeAttribute"; | ||
| private const string XnbPropertyAttributeName = "FEZRepacker.Core.Definitions.Game.XnbPropertyAttribute"; | ||
|
|
||
| private struct XnbTypeInfo | ||
| { | ||
| public string TypeName; | ||
| public string TypeFullName; | ||
| public string QualifierString; | ||
| public List<string> GenericParameters; | ||
| public List<XnbPropertyInfo> Properties; | ||
| } | ||
|
|
||
| private struct XnbPropertyInfo | ||
| { | ||
| public string Name; | ||
| public string TypeFullName; | ||
| public bool IsNullable; | ||
| public bool IsReferenceType; | ||
| public int Order; | ||
| public bool UseConverter; | ||
| public bool Optional; | ||
| public bool SkipIdentifier; | ||
| } | ||
|
|
||
| public void Initialize(IncrementalGeneratorInitializationContext context) | ||
| { | ||
| var xnbTypeInfos = context.SyntaxProvider | ||
| .CreateSyntaxProvider((node, _) => node is TypeDeclarationSyntax { AttributeLists.Count: > 0 }, GetXnbType) | ||
| .Where(m => m != null); | ||
|
|
||
| context.RegisterSourceOutput(xnbTypeInfos, (ctx, xnbTypeInfo) => | ||
| CreateSerializerSourceFile(ctx, xnbTypeInfo!.Value)); | ||
| } | ||
|
|
||
| private static XnbTypeInfo? GetXnbType(GeneratorSyntaxContext ctx, CancellationToken ct) | ||
| { | ||
| if (ctx.SemanticModel.GetDeclaredSymbol(ctx.Node, ct) is not INamedTypeSymbol typeSymbol) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var xnbReaderTypeAttribute = typeSymbol.GetAttributes() | ||
| .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == XnbReaderTypeAttributeName); | ||
|
|
||
| if (xnbReaderTypeAttribute is not {ConstructorArguments.Length: > 0}) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| bool isPrivate = xnbReaderTypeAttribute.NamedArguments | ||
| .FirstOrDefault(x => x.Key == "IsPrivate").Value.Value is true; | ||
|
|
||
| if (isPrivate) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var qualifierString = xnbReaderTypeAttribute.ConstructorArguments[0].Value as string ?? string.Empty; | ||
| var genericParameters = typeSymbol.TypeParameters.Select(tp => tp.Name).ToList(); | ||
| var properties = new List<XnbPropertyInfo>(); | ||
|
|
||
| foreach (var member in typeSymbol.GetMembers().OfType<IPropertySymbol>()) | ||
| { | ||
| ct.ThrowIfCancellationRequested(); | ||
|
|
||
| var xnbPropertyAttribute = member.GetAttributes() | ||
| .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == XnbPropertyAttributeName); | ||
|
|
||
| if (xnbPropertyAttribute == null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| int order = (int?)(xnbPropertyAttribute.ConstructorArguments.FirstOrDefault()).Value ?? 0; | ||
|
|
||
| bool useConverter = xnbPropertyAttribute.NamedArguments | ||
| .FirstOrDefault(x => x.Key == "UseConverter").Value.Value is true; | ||
| bool optional = xnbPropertyAttribute.NamedArguments | ||
| .FirstOrDefault(x => x.Key == "Optional").Value.Value is true; | ||
| bool skipIdentifier = xnbPropertyAttribute.NamedArguments | ||
| .FirstOrDefault(x => x.Key == "SkipIdentifier").Value.Value is true; | ||
|
|
||
| ITypeSymbol underlyingPropertyType = member.Type; | ||
| bool propertyNullable = false; | ||
|
|
||
| if (member.Type is INamedTypeSymbol {ConstructedFrom.SpecialType: SpecialType.System_Nullable_T} namedType) | ||
| { | ||
| underlyingPropertyType = namedType.TypeArguments[0]; | ||
| propertyNullable = true; | ||
| } | ||
|
|
||
| properties.Add(new XnbPropertyInfo | ||
| { | ||
| Name = member.Name, | ||
| TypeFullName = underlyingPropertyType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), | ||
| IsNullable = propertyNullable, | ||
| IsReferenceType = !propertyNullable && member.Type.IsReferenceType, | ||
| Order = order, | ||
| UseConverter = useConverter, | ||
| Optional = optional, | ||
| SkipIdentifier = skipIdentifier | ||
| }); | ||
| } | ||
|
|
||
| properties.Sort((a, b) => a.Order.CompareTo(b.Order)); | ||
|
|
||
| return new XnbTypeInfo | ||
| { | ||
| TypeName = typeSymbol.Name, | ||
| TypeFullName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), | ||
| QualifierString = qualifierString, | ||
| Properties = properties, | ||
| GenericParameters = genericParameters | ||
| }; | ||
| } | ||
|
|
||
| private static void CreateSerializerSourceFile(SourceProductionContext ctx, XnbTypeInfo model) | ||
| { | ||
| var cb = new CodeStringBuilder(); | ||
| EmitSerializer(cb, model); | ||
| ctx.AddSource($"{model.TypeName}ContentSerializer.g.cs", cb.ToString()); | ||
| } | ||
|
|
||
| private static void EmitSerializer(CodeStringBuilder cb, XnbTypeInfo xnbTypeInfo) | ||
| { | ||
| cb.AppendLine("// <auto-generated/>"); | ||
| cb.AppendLine("// FEZRepacker.Core.SourceGen output"); | ||
| cb.AppendLine("#nullable enable"); | ||
| cb.AppendLine(); | ||
| cb.AppendLine("using FEZRepacker.Core;"); | ||
| cb.AppendLine("using FEZRepacker.Core.Helpers;"); | ||
| cb.AppendLine("using FEZRepacker.Core.XNB;"); | ||
| cb.AppendLine(); | ||
| cb.AppendLine("namespace FEZRepacker.Core.XNB.ContentSerialization;"); | ||
| cb.AppendLine(); | ||
| cb.Append($"internal sealed class {ConstructSerializerName(xnbTypeInfo)}"); | ||
| cb.AppendLine($" : XnbContentSerializer<{xnbTypeInfo.TypeFullName}>"); | ||
| cb.BeginCodeBlock(); | ||
| { | ||
| EmitConstructor(cb, xnbTypeInfo); | ||
| cb.AppendLine(); | ||
| EmitDeserialize(cb, xnbTypeInfo); | ||
| cb.AppendLine(); | ||
| EmitSerialize(cb, xnbTypeInfo); | ||
| } | ||
| cb.EndCodeBlock(); | ||
| } | ||
|
|
||
| private static string ConstructSerializerName(XnbTypeInfo xnbTypeInfo) | ||
| { | ||
| var name = $"{xnbTypeInfo.TypeName}ContentSerializer"; | ||
| if (xnbTypeInfo.GenericParameters.Count > 0) | ||
| { | ||
| var genericParametersList = string.Join(", " , xnbTypeInfo.GenericParameters); | ||
| name += $"<{genericParametersList}>"; | ||
| } | ||
| return name; | ||
| } | ||
|
|
||
| private static void EmitConstructor(CodeStringBuilder cb, XnbTypeInfo xnbTypeInfo) | ||
| { | ||
| if (xnbTypeInfo.GenericParameters.Count == 0) | ||
| { | ||
| cb.AppendLine($"public override XnbAssemblyQualifier Name => \"{xnbTypeInfo.QualifierString}\";"); | ||
| return; | ||
| } | ||
|
|
||
| cb.AppendLine( "private readonly XnbAssemblyQualifier _name;"); | ||
| cb.AppendLine(); | ||
| cb.AppendLine("public override XnbAssemblyQualifier Name => _name;"); | ||
| cb.Append("public override Type[] UnderlyingContentTypes => ["); | ||
| cb.Append(string.Join(", ", xnbTypeInfo.GenericParameters.Select(type => $"typeof({type})"))); | ||
| cb.AppendLine("];"); | ||
| cb.AppendLine(); | ||
| cb.AppendLine($"public {xnbTypeInfo.TypeName}ContentSerializer() : base ()"); | ||
| cb.BeginCodeBlock(); | ||
| { | ||
| cb.AppendLine($"var name = XnbAssemblyQualifier.TryGetFromXnbReaderType(typeof({xnbTypeInfo.TypeFullName}));"); | ||
| cb.AppendLine("if (name.HasValue) _name = name.Value;"); | ||
| } | ||
| cb.EndCodeBlock(); | ||
| } | ||
|
|
||
| private static void EmitDeserialize(CodeStringBuilder cb, XnbTypeInfo model) | ||
| { | ||
| cb.AppendLine("public override object Deserialize(XnbContentReader reader)"); | ||
| cb.BeginCodeBlock(); | ||
| { | ||
| cb.AppendLine($"var content = new {model.TypeFullName}();"); | ||
|
|
||
| foreach (var prop in model.Properties) | ||
| { | ||
| EmitPropertyDeserialize(cb, prop); | ||
| } | ||
|
|
||
| cb.AppendLine("return content;"); | ||
| } | ||
| cb.EndCodeBlock(); | ||
| } | ||
|
|
||
| private static void EmitPropertyDeserialize(CodeStringBuilder cb, XnbPropertyInfo prop) | ||
| { | ||
| if (prop.Optional) | ||
| { | ||
| cb.AppendLine($"if (reader.ReadBoolean())"); | ||
| cb.BeginCodeBlock(); | ||
| } | ||
|
|
||
| cb.Append($"content.{prop.Name} = "); | ||
|
|
||
| if (prop.UseConverter) | ||
| { | ||
| var castType = $"{prop.TypeFullName}{(prop.IsNullable ? "?" : "")}"; | ||
| cb.Append($"({castType})reader.ReadContent(typeof({prop.TypeFullName}), "); | ||
| cb.Append($"{(prop.SkipIdentifier ? "true" : "false")}){(prop.IsNullable ? "" : "!")}"); | ||
| } | ||
| else | ||
| { | ||
| cb.Append(prop.TypeFullName switch | ||
| { | ||
| "bool" => "reader.ReadBoolean()", | ||
| "int" => "reader.ReadInt32()", | ||
| "byte" => "reader.ReadByte()", | ||
| "short" => "reader.ReadInt16()", | ||
| "float" => "reader.ReadSingle()", | ||
| "char" => "reader.ReadChar()", | ||
| "string" => "reader.ReadString()", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Vector2" => "reader.ReadVector2()", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Vector3" => "reader.ReadVector3()", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Quaternion" => "reader.ReadQuaternion()", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Color" => "reader.ReadColor()", | ||
| "global::System.TimeSpan" => "new global::System.TimeSpan(reader.ReadInt64())", | ||
| _ => $"default! /* unsupported type: {prop.TypeFullName} */" | ||
| }); | ||
| } | ||
|
|
||
| cb.AppendLine(";"); | ||
|
|
||
| if (prop.Optional) | ||
| { | ||
| cb.EndCodeBlock(); | ||
| } | ||
| } | ||
|
|
||
| private static void EmitSerialize(CodeStringBuilder cb, XnbTypeInfo model) | ||
| { | ||
| cb.AppendLine("public override void Serialize(object data, XnbContentWriter writer)"); | ||
| cb.BeginCodeBlock(); | ||
| { | ||
| cb.AppendLine($"var content = ({model.TypeFullName})data;"); | ||
|
|
||
| foreach (var prop in model.Properties) | ||
| { | ||
| EmitPropertySerialize(cb, prop); | ||
| } | ||
| } | ||
| cb.EndCodeBlock(); | ||
| } | ||
|
|
||
| private static void EmitPropertySerialize(CodeStringBuilder cb, XnbPropertyInfo prop) | ||
| { | ||
| var valueExpression = $"content.{prop.Name}"; | ||
| var propertyType = $"typeof({prop.TypeFullName})"; | ||
|
|
||
| if (prop.Optional) | ||
| { | ||
| if (prop.IsNullable) | ||
| { | ||
| cb.AppendLine($"if ({valueExpression}.HasValue)"); | ||
| valueExpression += ".Value"; | ||
| } | ||
| else if (prop.IsReferenceType) | ||
| { | ||
| cb.AppendLine($"if ({valueExpression} != null)"); | ||
| } | ||
| else | ||
| { | ||
| cb.AppendLine($"// {prop.Name}"); | ||
| } | ||
|
|
||
| cb.BeginCodeBlock(); | ||
| cb.AppendLine("writer.Write(true);"); | ||
| } | ||
|
|
||
| if (prop.UseConverter) | ||
| { | ||
| cb.Append($"writer.WriteContent({propertyType}, {valueExpression}, "); | ||
| cb.Append($"{(prop.SkipIdentifier ? "true" : "false")})"); | ||
| } | ||
| else | ||
| { | ||
| cb.Append(prop.TypeFullName switch | ||
| { | ||
| "bool" => $"writer.Write({valueExpression})", | ||
| "int" => $"writer.Write({valueExpression})", | ||
| "byte" => $"writer.Write({valueExpression})", | ||
| "short" => $"writer.Write({valueExpression})", | ||
| "float" => $"writer.Write({valueExpression})", | ||
| "char" => $"writer.Write({valueExpression})", | ||
| "string" => $"writer.Write({valueExpression})", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Vector2" => $"writer.Write({valueExpression})", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Vector3" => $"writer.Write({valueExpression})", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Quaternion" => $"writer.Write({valueExpression})", | ||
| "global::FEZRepacker.Core.Definitions.Game.XNA.Color" => $"writer.Write({valueExpression})", | ||
| "global::System.TimeSpan" => $"writer.Write({valueExpression}.Ticks)", | ||
| _ => $"_ = {valueExpression} /* unsupported type: {prop.TypeFullName} */" | ||
| }); | ||
| } | ||
| cb.AppendLine(";"); | ||
|
|
||
| if (prop.Optional) | ||
| { | ||
| cb.EndCodeBlock(); | ||
|
|
||
| if (prop.IsNullable || prop.IsReferenceType) | ||
| { | ||
| cb.AppendLine("else"); | ||
| cb.BeginCodeBlock(); | ||
| cb.AppendLine("writer.Write(false);"); | ||
| cb.EndCodeBlock(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this
ContentSerializerGeneratorclass and where is it used?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's part of the Core.SourceGen project, referenced by Core project. It is a source generator that creates Serializer classes for every XNB type in the FEZRepacker.Core.Definition namespace. This generation is done in the background during code analysis and build process. It serves as a replacement for GenericContentSerializer, which used reflections at runtime, which was expensive.