Skip to content
Merged
Show file tree
Hide file tree
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 May 6, 2026
bd33622
feat: improve performance of lzx decoder
Krzyhau May 7, 2026
1f8b2b4
feat: change reflection serialization to sourcegen
Krzyhau May 7, 2026
e06a680
feat: clear up null reference conversion
Krzyhau May 7, 2026
d0a2935
feat: use full assembly qualifier naming in xnbs
Krzyhau May 7, 2026
faaad57
fix: ensure correct length in decompressed xnb
Krzyhau May 8, 2026
bf900d4
fix: simplify xnb decompress stream
Krzyhau May 8, 2026
6586ed7
feat: ensure accurate texture coords in ao
Krzyhau May 8, 2026
4a866d7
fix: ensure correct reader list in texture2d asset
Krzyhau May 8, 2026
688144c
feat: add content type claim for XNB FQN list
Krzyhau May 8, 2026
ff63dd7
fix: guarantee same ordering in dictionaries
Krzyhau May 9, 2026
9309c67
fix: fix potential issue with json emplacement
Krzyhau May 9, 2026
1dd8d3f
fix: ensure conversion loop for level asset type
Krzyhau May 9, 2026
74784ba
feat: ensure round-trip conversion for trilesets
Krzyhau May 9, 2026
99c378c
feat: resolve soundeffect round-tripping issues
Krzyhau May 9, 2026
a4e30c5
fix: resolve issues with XNB content type names
Krzyhau May 9, 2026
5ac617b
feat: use more accurate atlas sprite packing
Krzyhau May 10, 2026
009a6c8
fix: fix sprite fonts content serialization
Krzyhau May 10, 2026
fc0b3dc
fix: convert nullable default char in sprite font
Krzyhau May 11, 2026
3b64e81
fix: implement minor reader optimisations
Krzyhau May 11, 2026
e67635a
feat: rework dxt utility for compression support
Krzyhau May 11, 2026
7354431
feat: include texture surface type in png metadata
Krzyhau May 12, 2026
01c9b6f
feat: add json conversion contexts sourcegen
Krzyhau May 13, 2026
9dbe6bc
feat: minimize list indexing in ordereddictionary
Krzyhau May 13, 2026
16805a0
refactor: rework conversion flag handling
Krzyhau May 13, 2026
f8af1ad
feat: allow sheet export for animated textures
Krzyhau May 13, 2026
6c75c58
refactor: get rid of xnbcompressor wrapper class
Krzyhau May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions Core.SourceGen/CodeStringBuilder.cs
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();
}
}
337 changes: 337 additions & 0 deletions Core.SourceGen/ContentSerializerGenerator.cs
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this ContentSerializerGenerator class and where is it used?

Copy link
Copy Markdown
Member Author

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.

{
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();
}
}
}
}

Loading
Loading