diff --git a/Core.SourceGen/CodeStringBuilder.cs b/Core.SourceGen/CodeStringBuilder.cs new file mode 100644 index 0000000..f7c1f45 --- /dev/null +++ b/Core.SourceGen/CodeStringBuilder.cs @@ -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(); + } +} diff --git a/Core.SourceGen/ContentSerializerGenerator.cs b/Core.SourceGen/ContentSerializerGenerator.cs new file mode 100644 index 0000000..a63b434 --- /dev/null +++ b/Core.SourceGen/ContentSerializerGenerator.cs @@ -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 GenericParameters; + public List 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(); + + foreach (var member in typeSymbol.GetMembers().OfType()) + { + 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("// "); + 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(); + } + } + } +} + diff --git a/Core.SourceGen/FEZRepacker.Core.SourceGen.csproj b/Core.SourceGen/FEZRepacker.Core.SourceGen.csproj new file mode 100644 index 0000000..27719ec --- /dev/null +++ b/Core.SourceGen/FEZRepacker.Core.SourceGen.csproj @@ -0,0 +1,16 @@ + + + netstandard2.0 + preview + enable + FEZRepacker.Core.SourceGen + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + \ No newline at end of file diff --git a/Core/Conversion/FormatConverter.cs b/Core/Conversion/FormatConverter.cs index 3ccf94d..7378409 100644 --- a/Core/Conversion/FormatConverter.cs +++ b/Core/Conversion/FormatConverter.cs @@ -11,7 +11,7 @@ namespace FEZRepacker.Core.Conversion /// public abstract class FormatConverter { - public abstract string FileFormat { get; } + public abstract string[] FileFormats { get; } public abstract Type FormatType { get; } public abstract FileBundle Convert(object? data); diff --git a/Core/Conversion/FormatConverterSettings.cs b/Core/Conversion/FormatConverterSettings.cs index f5e6ce2..dafdafa 100644 --- a/Core/Conversion/FormatConverterSettings.cs +++ b/Core/Conversion/FormatConverterSettings.cs @@ -1,4 +1,5 @@ using FEZRepacker.Core.Definitions.Game.ArtObject; +using FEZRepacker.Core.Definitions.Game.Graphics; using FEZRepacker.Core.Definitions.Game.TrileSet; namespace FEZRepacker.Core.Conversion @@ -9,17 +10,17 @@ namespace FEZRepacker.Core.Conversion public struct FormatConverterSettings() { /// - /// By default, the glTF - /// all-in-one format is used for transmitting and editing properties. + /// By default, the glTF all-in-one + /// format is used for transmitting and editing and properties. /// If the flag is true, the converter will use a legacy bundle with separate files. /// - public bool UseLegacyArtObjectBundle = false; + public bool UseTrixelArtBundle = false; /// - /// By default, the glTF - /// all-in-one format is used for transmitting and editing properties. - /// If the flag is true, the converter will use a legacy bundle with separate files. + /// By default, is converted into GIF animation file. This is a lossy conversion, + /// dropping original atlas texture arrangement and leading to minor precision loss to color and frame duration. + /// If the flag is true, the converter will export animations into a bundle of atlas texture and JSON data. /// - public bool UseLegacyTrileSetBundle = false; + public bool UseAnimationSheet = false; } } \ No newline at end of file diff --git a/Core/Conversion/FormatConverters.cs b/Core/Conversion/FormatConverters.cs index a60ee6b..5d86b16 100644 --- a/Core/Conversion/FormatConverters.cs +++ b/Core/Conversion/FormatConverters.cs @@ -28,7 +28,7 @@ public static class FormatConverters public static FormatConverter? FindByExtension(string extension) { - return List.FirstOrDefault(x => x.FileFormat == extension); + return List.FirstOrDefault(x => x.FileFormats.Contains(extension)); } public static FormatConverter? FindForFileBundle(FileBundle bundle) diff --git a/Core/Conversion/Formats/AnimatedTextureConverter.cs b/Core/Conversion/Formats/AnimatedTextureConverter.cs index cd07084..bd11b5d 100644 --- a/Core/Conversion/Formats/AnimatedTextureConverter.cs +++ b/Core/Conversion/Formats/AnimatedTextureConverter.cs @@ -2,9 +2,11 @@ using FEZRepacker.Core.Definitions.Game.XNA; using FEZRepacker.Core.FileSystem; using FEZRepacker.Core.Helpers; +using FEZRepacker.Core.Helpers.Json; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -14,23 +16,53 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class AnimatedTextureConverter : FormatConverter { - const int FramePadding = 1; + private const string GifFileFormat = ".gif"; + private const string BundleFileFormat = ".fezanim"; - public override string FileFormat => ".gif"; + public override string[] FileFormats => [GifFileFormat, BundleFileFormat]; public override FileBundle ConvertTyped(AnimatedTexture txt) { + if (Settings.UseAnimationSheet) + { + var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, txt); + var atlasTexture = new Texture2D() + { + Format = SurfaceFormat.Color, + Width = txt.AtlasWidth, + Height = txt.AtlasHeight, + TextureData = txt.TextureData + }; + using var animationAtlas = TexturesUtil.ImageFromTexture2D(atlasTexture); + bundle.AddFile(animationAtlas.SaveAsMemoryStream(new PngEncoder()), ".png"); + + return bundle; + } + using var animation = AnimatedTextureToGif(txt); var outStream = animation.SaveAsMemoryStream( new GifEncoder { ColorTableMode = GifColorTableMode.Local } ); - return FileBundle.Single(outStream, FileFormat); + return FileBundle.Single(outStream, GifFileFormat); } public override AnimatedTexture DeconvertTyped(FileBundle bundle) { + if (bundle.MainExtension == BundleFileFormat) + { + var animatedTexture = ConfiguredJsonSerializer.DeserializeFromFileBundle(bundle); + + using var atlasImage = Image.Load(bundle.RequireData(".png")); + var atlasTexture = TexturesUtil.ImageToTexture2D(atlasImage); + + animatedTexture.AtlasWidth = atlasTexture.Width; + animatedTexture.AtlasHeight = atlasTexture.Height; + animatedTexture.TextureData = atlasTexture.TextureData; + return animatedTexture; + } + using var animation = Image.Load(bundle.RequireData("")); return AnimationImageToAnimatedTexture(animation); } @@ -83,45 +115,33 @@ private static Image AnimationTextureToAtlasImage(AnimatedTexture txt) private static AnimatedTexture AnimationImageToAnimatedTexture(Image animation) { - (var atlasWidth, var atlasHeight) = FindMinimumPowerOfTwoAtlasSize(animation); - var frames = ExtractFrameDataFromGif(animation, atlasWidth); - - using var atlasImage = new Image(atlasWidth, atlasHeight, Color.Transparent); - PopulateAtlasImageFromGif(atlasImage, animation, frames); - var atlas = TexturesUtil.ImageToTexture2D(atlasImage); - - return new AnimatedTexture() + var frames = ExtractFrameDataFromGif(animation); + var animatedTexture = new AnimatedTexture() { - AtlasWidth = atlasWidth, - AtlasHeight = atlasHeight, FrameWidth = animation.Width, FrameHeight = animation.Height, - Frames = frames, - TextureData = atlas.TextureData, }; + animatedTexture.PackFrames(1); + + using var atlasImage = new Image(animatedTexture.AtlasWidth, animatedTexture.AtlasHeight, Color.Transparent); + PopulateAtlasImageFromGif(atlasImage, animation, frames); + animatedTexture.TextureData = TexturesUtil.ImageToTexture2D(atlasImage).TextureData; + + return animatedTexture; } - private static List ExtractFrameDataFromGif(Image animation, int atlasWidth) + private static List ExtractFrameDataFromGif(Image animation) { var frames = new List(); - int framePosX = 0; - int framePosY = 0; foreach (ImageFrame frameImg in animation.Frames) { frames.Add(new() { Duration = TimeSpan.FromMilliseconds(frameImg.Metadata.GetGifMetadata().FrameDelay * 10), - Rectangle = new(framePosX, framePosY, frameImg.Width, frameImg.Height) + Rectangle = new(0, 0, frameImg.Width, frameImg.Height) }); - - framePosX += animation.Width + FramePadding; - if (framePosX > atlasWidth - animation.Width) - { - framePosX = 0; - framePosY += animation.Height + FramePadding; - } } return frames; @@ -142,39 +162,5 @@ private static void PopulateAtlasImageFromGif(Image atlasImage, Image newAtlasHeight && atlasWidth > 0 && atlasHeight > 0) break; - - int newArea = newAtlasWidth * newAtlasHeight; - if (newArea <= atlasArea) - { - atlasArea = newArea; - atlasWidth = newAtlasWidth; - atlasHeight = newAtlasHeight; - } - } - - return (atlasWidth, atlasHeight); - } } } diff --git a/Core/Conversion/Formats/ArtObjectConverter.cs b/Core/Conversion/Formats/ArtObjectConverter.cs index 169fb27..e642d9c 100644 --- a/Core/Conversion/Formats/ArtObjectConverter.cs +++ b/Core/Conversion/Formats/ArtObjectConverter.cs @@ -13,16 +13,18 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class ArtObjectConverter : FormatConverter { - public override string FileFormat => ".fezao"; + private const string BundleFileFormat = ".fezao"; + + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(ArtObject data) { - if (!Settings.UseLegacyArtObjectBundle) + if (!Settings.UseTrixelArtBundle) { - return FileBundle.Single(GetTransmissionFormatStream(data), FileFormat, ".glb"); + return FileBundle.Single(GetTransmissionFormatStream(data), BundleFileFormat, ".glb"); } - var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, data); + var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, data); bundle.AddFile(GetTextureStream(data, TexturesUtil.CubemapPart.Albedo), ".png"); bundle.AddFile(GetTextureStream(data, TexturesUtil.CubemapPart.Emission), ".apng"); @@ -83,7 +85,7 @@ private static ArtObject LoadFromTransmissionFormat(Stream modelStream) var entry = entries.First(); var artObject = ConfiguredJsonSerializer.DeserializeFromNode(entry.Extras) ?? new ArtObject(); artObject.Geometry = entry.Geometry.WithReversedWindingIndices(); - FezGeometryUtil.RecalculateCubemapTexCoords(artObject.Geometry, artObject.Size); + FezGeometryUtil.RecalculateCubemapTexCoords(artObject.Geometry, artObject.Size, true); (Stream? albedo, Stream? emission) = GltfUtil.ExtractCubemapStreams(modelRoot); LoadCubemap(ref artObject, albedo, emission); @@ -96,7 +98,7 @@ private static void AppendGeometryStream(ref ArtObject data, Stream geometryStre var geometries = WavefrontObjUtil.FromWavefrontObjStream(geometryStream); if (geometries.Count < 1) return; data.Geometry = geometries.First().Value.WithReversedWindingIndices(); - FezGeometryUtil.RecalculateCubemapTexCoords(data.Geometry, data.Size); + FezGeometryUtil.RecalculateCubemapTexCoords(data.Geometry, data.Size, true); } private static void LoadCubemap(ref ArtObject data, Stream? albedoStream, Stream? emissionStream) diff --git a/Core/Conversion/Formats/EffectConverter.cs b/Core/Conversion/Formats/EffectConverter.cs index 3c84586..08a1ce4 100644 --- a/Core/Conversion/Formats/EffectConverter.cs +++ b/Core/Conversion/Formats/EffectConverter.cs @@ -5,7 +5,8 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class EffectConverter : FormatConverter { - public override string FileFormat => ".fxb"; + private const string FileFormat = ".fxb"; + public override string[] FileFormats => [FileFormat]; public override FileBundle ConvertTyped(Effect data) { diff --git a/Core/Conversion/Formats/LevelConverter.cs b/Core/Conversion/Formats/LevelConverter.cs index 8555603..412f6a3 100644 --- a/Core/Conversion/Formats/LevelConverter.cs +++ b/Core/Conversion/Formats/LevelConverter.cs @@ -7,12 +7,13 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class LevelConverter : FormatConverter { - public override string FileFormat => ".fezlvl"; + private const string BundleFileFormat = ".fezlvl"; + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(Level data) { var levelModel = new LevelJsonModel(data); - return ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, levelModel); + return ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, levelModel); } public override Level DeconvertTyped(FileBundle bundle) diff --git a/Core/Conversion/Formats/MapTreeConverter.cs b/Core/Conversion/Formats/MapTreeConverter.cs index 6baa0f1..6c4c4a9 100644 --- a/Core/Conversion/Formats/MapTreeConverter.cs +++ b/Core/Conversion/Formats/MapTreeConverter.cs @@ -7,13 +7,14 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class MapTreeConverter : FormatConverter { - public override string FileFormat => ".fezmap"; + private const string BundleFileFormat = ".fezmap"; + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(MapTree data) { var mapModel = new MapTreeJsonModel(); mapModel.SerializeFrom(data); - return ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, mapModel); + return ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, mapModel); } public override MapTree DeconvertTyped(FileBundle bundle) diff --git a/Core/Conversion/Formats/NpcMetadataConverter.cs b/Core/Conversion/Formats/NpcMetadataConverter.cs index e03580c..dd0a0f1 100644 --- a/Core/Conversion/Formats/NpcMetadataConverter.cs +++ b/Core/Conversion/Formats/NpcMetadataConverter.cs @@ -6,11 +6,12 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class NpcMetadataConverter : FormatConverter { - public override string FileFormat => ".feznpc"; + private const string BundleFileFormat = ".feznpc"; + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(NpcMetadata data) { - return ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, data); + return ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, data); } public override NpcMetadata DeconvertTyped(FileBundle bundle) diff --git a/Core/Conversion/Formats/SkyConverter.cs b/Core/Conversion/Formats/SkyConverter.cs index bad088a..3e98941 100644 --- a/Core/Conversion/Formats/SkyConverter.cs +++ b/Core/Conversion/Formats/SkyConverter.cs @@ -6,11 +6,12 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class SkyConverter : FormatConverter { - public override string FileFormat => ".fezsky"; + private const string BundleFileFormat = ".fezsky"; + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(Sky data) { - return ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, data); + return ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, data); } public override Sky DeconvertTyped(FileBundle bundle) diff --git a/Core/Conversion/Formats/SoundEffectConverter.cs b/Core/Conversion/Formats/SoundEffectConverter.cs index 15557cb..8cec83e 100644 --- a/Core/Conversion/Formats/SoundEffectConverter.cs +++ b/Core/Conversion/Formats/SoundEffectConverter.cs @@ -2,12 +2,15 @@ using FEZRepacker.Core.Definitions.Game.XNA; using FEZRepacker.Core.FileSystem; +using FEZRepacker.Core.Helpers; namespace FEZRepacker.Core.Conversion.Formats { internal class SoundEffectConverter : FormatConverter { - public override string FileFormat => ".wav"; + private const string FileFormat = ".wav"; + + public override string[] FileFormats => [FileFormat]; public override FileBundle ConvertTyped(SoundEffect data) { @@ -15,7 +18,8 @@ public override FileBundle ConvertTyped(SoundEffect data) using var outWriter = new BinaryWriter(outStream, Encoding.UTF8, true); outWriter.Write("RIFF".ToCharArray()); - var fileSize = 4 + 8 + 18 + 8 + data.DataChunk.Length; + const int headerSize = 104; // RIFF + WAVE + fmt chunk + smpl chunk + data chunk header + var fileSize = headerSize + data.DataChunk.Length; outWriter.Write(fileSize); outWriter.Write("WAVE".ToCharArray()); outWriter.Write("fmt ".ToCharArray()); @@ -26,6 +30,12 @@ public override FileBundle ConvertTyped(SoundEffect data) outWriter.Write(data.BytesPerSecond); outWriter.Write(data.BlockAlignment); outWriter.Write(data.BitsPerSample); + outWriter.Write("smpl".ToCharArray()); + outWriter.WriteInts(52, 0, 0, 222675, 60, 0, 0, 0, 1, 0); + outWriter.WriteInts(0, 0); + outWriter.Write(data.LoopStart); + outWriter.Write(data.LoopStart + data.LoopLength - 1); + outWriter.WriteInts(0, 0); outWriter.Write("data".ToCharArray()); outWriter.Write(data.DataChunk.Length); outWriter.Write(data.DataChunk); @@ -64,6 +74,26 @@ public override SoundEffect DeconvertTyped(FileBundle bundle) soundEffect.ExtraParameter = 0; inReader.ReadBytes(chunkLength - 16); } + else if (chunkHeader == "smpl") + { + inReader.ReadBytes(28); + var sampleLoops = inReader.ReadInt32(); + var extraBytesCount = inReader.ReadInt32(); + for (int i = 0; i < sampleLoops; i++) + { + inReader.ReadBytes(8); + var loopStart = inReader.ReadInt32(); + var loopEnd = inReader.ReadInt32(); + inReader.ReadBytes(8); + + if (i == 0) + { + soundEffect.LoopStart = loopStart; + soundEffect.LoopLength = loopEnd - loopStart + 1; + } + } + inReader.ReadBytes(extraBytesCount); + } else { var chunkData = inReader.ReadBytes(chunkLength); @@ -82,10 +112,8 @@ public override SoundEffect DeconvertTyped(FileBundle bundle) { throw new InvalidDataException($"PCM WAV file bit resolution must be divisible by 16 ({soundEffect.BitsPerSample}-bit samples detected)"); } - - // not sure if loops are even used, but just to be safe, setting it to be the entire file - soundEffect.LoopStart = 0; - soundEffect.LoopLength = soundEffect.DataChunk.Length / Math.Max(1, (int)soundEffect.ChannelCount); + + soundEffect.DurationMs = (int)(((long)soundEffect.DataChunk.Length * 1000) / soundEffect.BytesPerSecond); return soundEffect; } diff --git a/Core/Conversion/Formats/SpriteFontConverter.cs b/Core/Conversion/Formats/SpriteFontConverter.cs index 1d3348b..a489ef8 100644 --- a/Core/Conversion/Formats/SpriteFontConverter.cs +++ b/Core/Conversion/Formats/SpriteFontConverter.cs @@ -12,13 +12,15 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class SpriteFontConverter : FormatConverter { - public override string FileFormat => ".fezfont"; + private const string BundleFileFormat = ".fezfont"; + + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(SpriteFont data) { var spriteFontModel = new SpriteFontPropertiesJsonModel(); spriteFontModel.SerializeFrom(data); - var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, spriteFontModel); + var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, spriteFontModel); using var fontAtlas = TexturesUtil.ImageFromTexture2D(data.Texture); bundle.AddFile(fontAtlas.SaveAsMemoryStream(new PngEncoder()), ".png"); @@ -32,7 +34,7 @@ public override SpriteFont DeconvertTyped(FileBundle bundle) var spriteFont = spriteFontModel.Deserialize(); using var importedImage = Image.Load(bundle.RequireData(".png")); - spriteFont.Texture = TexturesUtil.ImageToTexture2D(importedImage); + spriteFont.Texture = TexturesUtil.ImageToTexture2D(importedImage, SurfaceFormat.Dxt3); return spriteFont; } diff --git a/Core/Conversion/Formats/TextStorageConverter.cs b/Core/Conversion/Formats/TextStorageConverter.cs index ef672eb..18f1381 100644 --- a/Core/Conversion/Formats/TextStorageConverter.cs +++ b/Core/Conversion/Formats/TextStorageConverter.cs @@ -1,16 +1,19 @@ using FEZRepacker.Core.FileSystem; +using FEZRepacker.Core.Helpers; using FEZRepacker.Core.Helpers.Json; namespace FEZRepacker.Core.Conversion.Formats { - using TextStorage = Dictionary>; + using TextStorage = OrderedDictionary>; internal class TextStorageConverter : FormatConverter { - public override string FileFormat => ".feztxt"; + private const string BundleFileFormat = ".feztxt"; + + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(TextStorage data) { - return ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, data); + return ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, data); } public override TextStorage DeconvertTyped(FileBundle bundle) diff --git a/Core/Conversion/Formats/TextureConverter.cs b/Core/Conversion/Formats/TextureConverter.cs index 789818b..6581f85 100644 --- a/Core/Conversion/Formats/TextureConverter.cs +++ b/Core/Conversion/Formats/TextureConverter.cs @@ -10,11 +10,15 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class TextureConverter : FormatConverter { - public override string FileFormat => ".png"; + private const string FileFormat = ".png"; + private const string SurfaceTypeMetadataKey = "XNASurfaceType"; + + public override string[] FileFormats => [FileFormat]; public override FileBundle ConvertTyped(Texture2D texture) { using var image = TexturesUtil.ImageFromTexture2D(texture); + StoreSurfaceFormatInImage(image, texture.Format); var outStream = image.SaveAsMemoryStream(new PngEncoder()); return FileBundle.Single(outStream, FileFormat); } @@ -22,7 +26,26 @@ public override FileBundle ConvertTyped(Texture2D texture) public override Texture2D DeconvertTyped(FileBundle bundle) { using var importedImage = Image.Load(bundle.RequireData("")); - return TexturesUtil.ImageToTexture2D(importedImage); + var format = TryDetectDxtFormat(importedImage, SurfaceFormat.Color); + return TexturesUtil.ImageToTexture2D(importedImage, format); + } + + private static void StoreSurfaceFormatInImage(Image img, SurfaceFormat format) + { + PngMetadata pngMeta = img.Metadata.GetPngMetadata(); + pngMeta.TextData.Add(new PngTextData(SurfaceTypeMetadataKey, format.ToString(), "", "")); + } + + private static SurfaceFormat TryDetectDxtFormat(Image img, SurfaceFormat defaultFormat) + { + PngMetadata pngMeta = img.Metadata.GetPngMetadata(); + var surfaceTypeMetadata = pngMeta.TextData.FirstOrDefault(data => data.Keyword == SurfaceTypeMetadataKey); + if (surfaceTypeMetadata != null && Enum.TryParse(surfaceTypeMetadata.Value, out SurfaceFormat surfaceFormat)) + { + return surfaceFormat; + } + + return defaultFormat; } } } diff --git a/Core/Conversion/Formats/TrackedSongConverter.cs b/Core/Conversion/Formats/TrackedSongConverter.cs index 8bf275c..3362fce 100644 --- a/Core/Conversion/Formats/TrackedSongConverter.cs +++ b/Core/Conversion/Formats/TrackedSongConverter.cs @@ -6,11 +6,13 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class TrackedSongConverter : FormatConverter { - public override string FileFormat => ".fezsong"; + private const string BundleFileFormat = ".fezsong"; + + public override string[] FileFormats => [BundleFileFormat]; public override FileBundle ConvertTyped(TrackedSong data) { - return ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, data); + return ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, data); } public override TrackedSong DeconvertTyped(FileBundle bundle) diff --git a/Core/Conversion/Formats/TrileSetConverter.cs b/Core/Conversion/Formats/TrileSetConverter.cs index 296cd85..192b826 100644 --- a/Core/Conversion/Formats/TrileSetConverter.cs +++ b/Core/Conversion/Formats/TrileSetConverter.cs @@ -17,18 +17,20 @@ namespace FEZRepacker.Core.Conversion.Formats { internal class TrileSetConverter : FormatConverter { - public override string FileFormat => ".fezts"; - + private const string BundleFileFormat = ".fezts"; private const string TrileIdKey = "TrileId"; + + public override string[] FileFormats => [BundleFileFormat]; + public override FileBundle ConvertTyped(TrileSet data) { - if (!Settings.UseLegacyTrileSetBundle) + if (!Settings.UseTrixelArtBundle) { - return FileBundle.Single(GetTransmissionFormatStream(data), FileFormat, ".glb"); + return FileBundle.Single(GetTransmissionFormatStream(data), BundleFileFormat, ".glb"); } - var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(FileFormat, data); + var bundle = ConfiguredJsonSerializer.SerializeToFileBundle(BundleFileFormat, data); bundle.AddFile(GetTextureStream(data, TexturesUtil.CubemapPart.Albedo), ".png"); bundle.AddFile(GetTextureStream(data, TexturesUtil.CubemapPart.Emission), ".apng"); diff --git a/Core/Definitions/Game/ArtObject/ArtObject.cs b/Core/Definitions/Game/ArtObject/ArtObject.cs index 4ff7211..d9b78d2 100644 --- a/Core/Definitions/Game/ArtObject/ArtObject.cs +++ b/Core/Definitions/Game/ArtObject/ArtObject.cs @@ -7,8 +7,8 @@ namespace FEZRepacker.Core.Definitions.Game.ArtObject { - [XnbType("FezEngine.Structure.ArtObject")] - [XnbReaderType("FezEngine.Readers.ArtObjectReader")] + [XnbType("FezEngine.Structure.ArtObject, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ArtObjectReader, FezEngine")] public class ArtObject { [XnbProperty] diff --git a/Core/Definitions/Game/ArtObject/VertexInstance.cs b/Core/Definitions/Game/ArtObject/VertexInstance.cs index df771c8..eb7c7fc 100644 --- a/Core/Definitions/Game/ArtObject/VertexInstance.cs +++ b/Core/Definitions/Game/ArtObject/VertexInstance.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.ArtObject { - [XnbType("FezEngine.Structure.Geometry.VertexPositionNormalTextureInstance")] - [XnbReaderType("FezEngine.Readers.VertexPositionNormalTextureInstanceReader")] + [XnbType("FezEngine.Structure.Geometry.VertexPositionNormalTextureInstance, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.VertexPositionNormalTextureInstanceReader, FezEngine")] // Original name in FezEngine: VertexPositionNormalTextureInstance public class VertexInstance { diff --git a/Core/Definitions/Game/Common/ActorType.cs b/Core/Definitions/Game/Common/ActorType.cs index 0f2391e..cd3d7f9 100644 --- a/Core/Definitions/Game/Common/ActorType.cs +++ b/Core/Definitions/Game/Common/ActorType.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Common { - [XnbType("FezEngine.Structure.ActorType")] + [XnbType("FezEngine.Structure.ActorType, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum ActorType { None, diff --git a/Core/Definitions/Game/Common/FaceOrientation.cs b/Core/Definitions/Game/Common/FaceOrientation.cs index 1c32656..1ca422b 100644 --- a/Core/Definitions/Game/Common/FaceOrientation.cs +++ b/Core/Definitions/Game/Common/FaceOrientation.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Common { - [XnbType("FezEngine.FaceOrientation")] + [XnbType("FezEngine.FaceOrientation, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum FaceOrientation { Left, diff --git a/Core/Definitions/Game/Common/LevelNodeType.cs b/Core/Definitions/Game/Common/LevelNodeType.cs index a07202c..ac8adcf 100644 --- a/Core/Definitions/Game/Common/LevelNodeType.cs +++ b/Core/Definitions/Game/Common/LevelNodeType.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Common { - [XnbType("FezEngine.LevelNodeType")] + [XnbType("FezEngine.LevelNodeType, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum LevelNodeType { Node, diff --git a/Core/Definitions/Game/Common/NpcAction.cs b/Core/Definitions/Game/Common/NpcAction.cs index 8115cb5..ba021a3 100644 --- a/Core/Definitions/Game/Common/NpcAction.cs +++ b/Core/Definitions/Game/Common/NpcAction.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Common { - [XnbType("FezEngine.Structure.NpcAction")] + [XnbType("FezEngine.Structure.NpcAction, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum NpcAction { None, diff --git a/Core/Definitions/Game/Graphics/AnimatedTexture.cs b/Core/Definitions/Game/Graphics/AnimatedTexture.cs index 0ac6157..0f2a5fb 100644 --- a/Core/Definitions/Game/Graphics/AnimatedTexture.cs +++ b/Core/Definitions/Game/Graphics/AnimatedTexture.cs @@ -1,15 +1,18 @@ -namespace FEZRepacker.Core.Definitions.Game.Graphics +using System.Text.Json.Serialization; + +namespace FEZRepacker.Core.Definitions.Game.Graphics { - [XnbType("FezEngine.Structure.AnimatedTexture")] - [XnbReaderType("FezEngine.Readers.AnimatedTextureReader")] + [XnbType("FezEngine.Structure.AnimatedTexture, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.AnimatedTextureReader, FezEngine")] public class AnimatedTexture { // Definition has been altered since the Texture property is // serialized in a different way than Texture reader does it - + [JsonIgnore] [XnbProperty] public int AtlasWidth { get; set; } + [JsonIgnore] [XnbProperty] public int AtlasHeight { get; set; } @@ -19,6 +22,7 @@ public class AnimatedTexture [XnbProperty] public int FrameHeight { get; set; } + [JsonIgnore] [XnbProperty(UseConverter = true, SkipIdentifier = true)] public byte[] TextureData { get; set; } = { }; diff --git a/Core/Definitions/Game/Graphics/FrameContent.cs b/Core/Definitions/Game/Graphics/FrameContent.cs index 5a0f6d8..b40c9fe 100644 --- a/Core/Definitions/Game/Graphics/FrameContent.cs +++ b/Core/Definitions/Game/Graphics/FrameContent.cs @@ -3,8 +3,8 @@ namespace FEZRepacker.Core.Definitions.Game.Graphics { - [XnbType("FezEngine.Content.FrameContent")] - [XnbReaderType("FezEngine.Readers.FrameReader")] + [XnbType("FezEngine.Content.FrameContent, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.FrameReader, FezEngine")] public class FrameContent { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Graphics/IndexedPrimitives.cs b/Core/Definitions/Game/Graphics/IndexedPrimitives.cs index 85288fc..2b7ca53 100644 --- a/Core/Definitions/Game/Graphics/IndexedPrimitives.cs +++ b/Core/Definitions/Game/Graphics/IndexedPrimitives.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Graphics { - [XnbType("FezEngine.Structure.Geometry.ShaderInstancedIndexedPrimitives")] - [XnbReaderType("FezEngine.Readers.ShaderInstancedIndexedPrimitivesReader")] + [XnbType("FezEngine.Structure.Geometry.ShaderInstancedIndexedPrimitives, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ShaderInstancedIndexedPrimitivesReader, FezEngine")] // Original name in FezEngine: ShaderInstancedIndexedPrimitives public class IndexedPrimitives { diff --git a/Core/Definitions/Game/Level/AmbienceTrack.cs b/Core/Definitions/Game/Level/AmbienceTrack.cs index 0b3b7f2..0995ceb 100644 --- a/Core/Definitions/Game/Level/AmbienceTrack.cs +++ b/Core/Definitions/Game/Level/AmbienceTrack.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.AmbienceTrack")] - [XnbReaderType("FezEngine.Readers.AmbienceTrackReader")] + [XnbType("FezEngine.Structure.AmbienceTrack, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.AmbienceTrackReader, FezEngine")] public class AmbienceTrack { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/ArtObjectActorSettings.cs b/Core/Definitions/Game/Level/ArtObjectActorSettings.cs index e9ad833..31d6db3 100644 --- a/Core/Definitions/Game/Level/ArtObjectActorSettings.cs +++ b/Core/Definitions/Game/Level/ArtObjectActorSettings.cs @@ -3,8 +3,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.ArtObjectActorSettings")] - [XnbReaderType("FezEngine.Readers.ArtObjectActorSettingsReader")] + [XnbType("FezEngine.Structure.ArtObjectActorSettings, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ArtObjectActorSettingsReader, FezEngine")] public class ArtObjectActorSettings { [XnbProperty] @@ -13,7 +13,7 @@ public class ArtObjectActorSettings [XnbProperty(UseConverter = true)] public ActorType ContainedTrile { get; set; } - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? AttachedGroup { get; set; } [XnbProperty(UseConverter = true)] @@ -40,7 +40,7 @@ public class ArtObjectActorSettings [XnbProperty(UseConverter = true)] public PathSegment Segment { get; set; } = new(); - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? NextNode { get; set; } [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/ArtObjectInstance.cs b/Core/Definitions/Game/Level/ArtObjectInstance.cs index c13e0ff..2f5a620 100644 --- a/Core/Definitions/Game/Level/ArtObjectInstance.cs +++ b/Core/Definitions/Game/Level/ArtObjectInstance.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.ArtObjectInstance")] - [XnbReaderType("FezEngine.Readers.ArtObjectInstanceReader")] + [XnbType("FezEngine.Structure.ArtObjectInstance, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ArtObjectInstanceReader, FezEngine")] public class ArtObjectInstance { [XnbProperty] diff --git a/Core/Definitions/Game/Level/BackgroundPlane.cs b/Core/Definitions/Game/Level/BackgroundPlane.cs index 5c5cbe2..013d5ce 100644 --- a/Core/Definitions/Game/Level/BackgroundPlane.cs +++ b/Core/Definitions/Game/Level/BackgroundPlane.cs @@ -3,8 +3,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.BackgroundPlane")] - [XnbReaderType("FezEngine.Readers.BackgroundPlaneReader")] + [XnbType("FezEngine.Structure.BackgroundPlane, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.BackgroundPlaneReader, FezEngine")] public class BackgroundPlane { [XnbProperty] @@ -40,7 +40,7 @@ public class BackgroundPlane [XnbProperty] public float Opacity { get; set; } = 1.0f; - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? AttachedGroup { get; set; } [XnbProperty] @@ -78,7 +78,7 @@ public class BackgroundPlane [XnbProperty(UseConverter = true)] public ActorType ActorType { get; set; } - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? AttachedPlane { get; set; } [XnbProperty] diff --git a/Core/Definitions/Game/Level/CameraNodeData.cs b/Core/Definitions/Game/Level/CameraNodeData.cs index d3eb804..bb9709c 100644 --- a/Core/Definitions/Game/Level/CameraNodeData.cs +++ b/Core/Definitions/Game/Level/CameraNodeData.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.CameraNodeData")] - [XnbReaderType("FezEngine.Readers.CameraNodeDataReader")] + [XnbType("FezEngine.Structure.CameraNodeData, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.CameraNodeDataReader, FezEngine")] public class CameraNodeData { [XnbProperty] diff --git a/Core/Definitions/Game/Level/CodeInput.cs b/Core/Definitions/Game/Level/CodeInput.cs index 2542cc9..7b971d5 100644 --- a/Core/Definitions/Game/Level/CodeInput.cs +++ b/Core/Definitions/Game/Level/CodeInput.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.Input.CodeInput")] + [XnbType("FezEngine.Structure.Input.CodeInput, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] [Flags] public enum CodeInput { diff --git a/Core/Definitions/Game/Level/DotDialogueLine.cs b/Core/Definitions/Game/Level/DotDialogueLine.cs index 3334cce..34a0812 100644 --- a/Core/Definitions/Game/Level/DotDialogueLine.cs +++ b/Core/Definitions/Game/Level/DotDialogueLine.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.DotDialogueLine")] - [XnbReaderType("FezEngine.Readers.DotDialogueLineReader")] + [XnbType("FezEngine.Structure.DotDialogueLine, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.DotDialogueLineReader, FezEngine")] public class DotDialogueLine { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/Level.cs b/Core/Definitions/Game/Level/Level.cs index b24e078..b00e317 100644 --- a/Core/Definitions/Game/Level/Level.cs +++ b/Core/Definitions/Game/Level/Level.cs @@ -1,11 +1,12 @@ using FEZRepacker.Core.Definitions.Game.Common; using FEZRepacker.Core.Definitions.Game.Level.Scripting; using FEZRepacker.Core.Definitions.Game.XNA; +using FEZRepacker.Core.Helpers; namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.Level")] - [XnbReaderType("FezEngine.Readers.LevelReader")] + [XnbType("FezEngine.Structure.Level, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.LevelReader, FezEngine")] public class Level { [XnbProperty(UseConverter = true)] @@ -57,10 +58,10 @@ public class Level public string TrileSetName { get; set; } = ""; [XnbProperty(UseConverter = true)] - public Dictionary Volumes { get; set; } = new(); + public IDictionary Volumes { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public Dictionary Scripts { get; set; } = new(); + public IDictionary Scripts { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] public string SongName { get; set; } = ""; @@ -72,22 +73,22 @@ public class Level public int FAPFadeOutLength { get; set; } [XnbProperty(UseConverter = true)] - public Dictionary Triles { get; set; } = new(); + public IDictionary Triles { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public Dictionary ArtObjects { get; set; } = new(); + public IDictionary ArtObjects { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public Dictionary BackgroundPlanes { get; set; } = new(); + public IDictionary BackgroundPlanes { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public Dictionary Groups { get; set; } = new(); + public IDictionary Groups { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public Dictionary NonPlayerCharacters { get; set; } = new(); + public IDictionary NonPlayerCharacters { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public Dictionary Paths { get; set; } = new(); + public IDictionary Paths { get; set; } = new OrderedDictionary(); [XnbProperty] public bool Descending { get; set; } diff --git a/Core/Definitions/Game/Level/LiquidType.cs b/Core/Definitions/Game/Level/LiquidType.cs index b7f6cc4..09e38ec 100644 --- a/Core/Definitions/Game/Level/LiquidType.cs +++ b/Core/Definitions/Game/Level/LiquidType.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.LiquidType")] + [XnbType("FezEngine.Structure.LiquidType, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum LiquidType { None, diff --git a/Core/Definitions/Game/Level/MovementPath.cs b/Core/Definitions/Game/Level/MovementPath.cs index e5a25c9..5722d96 100644 --- a/Core/Definitions/Game/Level/MovementPath.cs +++ b/Core/Definitions/Game/Level/MovementPath.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.MovementPath")] - [XnbReaderType("FezEngine.Readers.MovementPathReader")] + [XnbType("FezEngine.Structure.MovementPath, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.MovementPathReader, FezEngine")] public class MovementPath { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/NpcActionContent.cs b/Core/Definitions/Game/Level/NpcActionContent.cs index c6f527e..37e4005 100644 --- a/Core/Definitions/Game/Level/NpcActionContent.cs +++ b/Core/Definitions/Game/Level/NpcActionContent.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.NpcActionContent")] - [XnbReaderType("FezEngine.Readers.NpcActionContentReader")] + [XnbType("FezEngine.Structure.NpcActionContent, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.NpcActionContentReader, FezEngine")] public class NpcActionContent { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/NpcInstance.cs b/Core/Definitions/Game/Level/NpcInstance.cs index 949babc..0202b60 100644 --- a/Core/Definitions/Game/Level/NpcInstance.cs +++ b/Core/Definitions/Game/Level/NpcInstance.cs @@ -3,8 +3,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.NpcInstance")] - [XnbReaderType("FezEngine.Readers.NpcInstanceReader")] + [XnbType("FezEngine.Structure.NpcInstance, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.NpcInstanceReader, FezEngine")] public class NpcInstance { [XnbProperty] @@ -32,9 +32,9 @@ public class NpcInstance public ActorType ActorType { get; set; } [XnbProperty(UseConverter = true)] - public List Speech { get; set; } = new(); + public List Speech { get; set; } = null!; [XnbProperty(UseConverter = true)] - public Dictionary Actions { get; set; } = new(); + public IDictionary Actions { get; set; } = null!; } } \ No newline at end of file diff --git a/Core/Definitions/Game/Level/PathEndBehabiour.cs b/Core/Definitions/Game/Level/PathEndBehabiour.cs index b0b364a..d62d9cd 100644 --- a/Core/Definitions/Game/Level/PathEndBehabiour.cs +++ b/Core/Definitions/Game/Level/PathEndBehabiour.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.PathEndBehavior")] + [XnbType("FezEngine.Structure.PathEndBehavior, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum PathEndBehavior { Bounce, diff --git a/Core/Definitions/Game/Level/PathSegment.cs b/Core/Definitions/Game/Level/PathSegment.cs index 8cdba7b..5ad8d83 100644 --- a/Core/Definitions/Game/Level/PathSegment.cs +++ b/Core/Definitions/Game/Level/PathSegment.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.PathSegment")] - [XnbReaderType("FezEngine.Readers.PathSegmentReader")] + [XnbType("FezEngine.Structure.PathSegment, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.PathSegmentReader, FezEngine")] public class PathSegment { [XnbProperty] diff --git a/Core/Definitions/Game/Level/Scripting/ComparisonOperator.cs b/Core/Definitions/Game/Level/Scripting/ComparisonOperator.cs index 3bf2bba..47a1dfd 100644 --- a/Core/Definitions/Game/Level/Scripting/ComparisonOperator.cs +++ b/Core/Definitions/Game/Level/Scripting/ComparisonOperator.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Level.Scripting { - [XnbType("FezEngine.Structure.Scripting.ComparisonOperator")] + [XnbType("FezEngine.Structure.Scripting.ComparisonOperator, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum ComparisonOperator { None = -1, diff --git a/Core/Definitions/Game/Level/Scripting/Entity.cs b/Core/Definitions/Game/Level/Scripting/Entity.cs index efafc9f..39f25c8 100644 --- a/Core/Definitions/Game/Level/Scripting/Entity.cs +++ b/Core/Definitions/Game/Level/Scripting/Entity.cs @@ -1,13 +1,13 @@ namespace FEZRepacker.Core.Definitions.Game.Level.Scripting { - [XnbType("FezEngine.Structure.Scripting.Entity")] - [XnbReaderType("FezEngine.Readers.EntityReader")] + [XnbType("FezEngine.Structure.Scripting.Entity, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.EntityReader, FezEngine")] public class Entity { [XnbProperty] public string Type { get; set; } = ""; - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? Identifier { get; set; } } } diff --git a/Core/Definitions/Game/Level/Scripting/Script.cs b/Core/Definitions/Game/Level/Scripting/Script.cs index b97de71..c7f223e 100644 --- a/Core/Definitions/Game/Level/Scripting/Script.cs +++ b/Core/Definitions/Game/Level/Scripting/Script.cs @@ -1,13 +1,13 @@ namespace FEZRepacker.Core.Definitions.Game.Level.Scripting { - [XnbType("FezEngine.Structure.Scripting.Script")] - [XnbReaderType("FezEngine.Readers.ScriptReader")] + [XnbType("FezEngine.Structure.Scripting.Script, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ScriptReader, FezEngine")] public class Script { [XnbProperty] public string Name { get; set; } = "Untitled"; - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public TimeSpan? Timeout { get; set; } [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/Scripting/ScriptAction.cs b/Core/Definitions/Game/Level/Scripting/ScriptAction.cs index 92186c9..aa0b064 100644 --- a/Core/Definitions/Game/Level/Scripting/ScriptAction.cs +++ b/Core/Definitions/Game/Level/Scripting/ScriptAction.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level.Scripting { - [XnbType("FezEngine.Structure.Scripting.ScriptAction")] - [XnbReaderType("FezEngine.Readers.ScriptActionReader")] + [XnbType("FezEngine.Structure.Scripting.ScriptAction, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ScriptActionReader, FezEngine")] public class ScriptAction { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs b/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs index 70c05b1..b138e36 100644 --- a/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs +++ b/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level.Scripting { - [XnbType("FezEngine.Structure.Scripting.ScriptCondition")] - [XnbReaderType("FezEngine.Readers.ScriptConditionReader")] + [XnbType("FezEngine.Structure.Scripting.ScriptCondition, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ScriptConditionReader, FezEngine")] public class ScriptCondition { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs b/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs index f85d6d1..3cfd45d 100644 --- a/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs +++ b/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level.Scripting { - [XnbType("FezEngine.Structure.Scripting.ScriptTrigger")] - [XnbReaderType("FezEngine.Readers.ScriptTriggerReader")] + [XnbType("FezEngine.Structure.Scripting.ScriptTrigger, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.ScriptTriggerReader, FezEngine")] public class ScriptTrigger { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/SpeechLine.cs b/Core/Definitions/Game/Level/SpeechLine.cs index 571757c..6c623dc 100644 --- a/Core/Definitions/Game/Level/SpeechLine.cs +++ b/Core/Definitions/Game/Level/SpeechLine.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.SpeechLine")] - [XnbReaderType("FezEngine.Readers.SpeechLineReader")] + [XnbType("FezEngine.Structure.SpeechLine, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.SpeechLineReader, FezEngine")] public class SpeechLine { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/TrileEmplacement.cs b/Core/Definitions/Game/Level/TrileEmplacement.cs index 1364beb..78ac066 100644 --- a/Core/Definitions/Game/Level/TrileEmplacement.cs +++ b/Core/Definitions/Game/Level/TrileEmplacement.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.TrileEmplacement")] - [XnbReaderType("FezEngine.Readers.TrileEmplacementReader")] + [XnbType("FezEngine.Structure.TrileEmplacement, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrileEmplacementReader, FezEngine")] public class TrileEmplacement : IEquatable, IComparable { [XnbProperty] diff --git a/Core/Definitions/Game/Level/TrileFace.cs b/Core/Definitions/Game/Level/TrileFace.cs index 240160c..2a141bd 100644 --- a/Core/Definitions/Game/Level/TrileFace.cs +++ b/Core/Definitions/Game/Level/TrileFace.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.TrileFace")] - [XnbReaderType("FezEngine.Readers.TrileFaceReader")] + [XnbType("FezEngine.Structure.TrileFace, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrileFaceReader, FezEngine")] public class TrileFace { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/TrileGroup.cs b/Core/Definitions/Game/Level/TrileGroup.cs index 2b412f3..0b29419 100644 --- a/Core/Definitions/Game/Level/TrileGroup.cs +++ b/Core/Definitions/Game/Level/TrileGroup.cs @@ -3,14 +3,14 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.TrileGroup")] - [XnbReaderType("FezEngine.Readers.TrileGroupReader")] + [XnbType("FezEngine.Structure.TrileGroup, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrileGroupReader, FezEngine")] public class TrileGroup { [XnbProperty(UseConverter = true)] public List Triles { get; set; } = new(); - [XnbProperty(UseConverter = true, Optional = true, SkipIdentifier = true)] + [XnbProperty(UseConverter = true)] public MovementPath? Path { get; set; } = null; [XnbProperty] diff --git a/Core/Definitions/Game/Level/TrileInstance.cs b/Core/Definitions/Game/Level/TrileInstance.cs index 38fd8fd..f46ceef 100644 --- a/Core/Definitions/Game/Level/TrileInstance.cs +++ b/Core/Definitions/Game/Level/TrileInstance.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.TrileInstance")] - [XnbReaderType("FezEngine.Readers.TrileInstanceReader")] + [XnbType("FezEngine.Structure.TrileInstance, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrileInstanceReader, FezEngine")] public class TrileInstance { [XnbProperty] @@ -19,6 +19,6 @@ public class TrileInstance public TrileInstanceActorSettings? ActorSettings { get; set; } = null; [XnbProperty(UseConverter = true)] - public List OverlappedTriles { get; set; } = new(); + public List OverlappedTriles { get; set; } = null!; } } diff --git a/Core/Definitions/Game/Level/TrileInstanceActorSettings.cs b/Core/Definitions/Game/Level/TrileInstanceActorSettings.cs index 7cf17fe..9680675 100644 --- a/Core/Definitions/Game/Level/TrileInstanceActorSettings.cs +++ b/Core/Definitions/Game/Level/TrileInstanceActorSettings.cs @@ -1,11 +1,11 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.InstanceActorSettings")] - [XnbReaderType("FezEngine.Readers.InstanceActorSettingsReader")] + [XnbType("FezEngine.Structure.InstanceActorSettings, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.InstanceActorSettingsReader, FezEngine")] // Original name in FezEngine: InstanceActorSettings public class TrileInstanceActorSettings { - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? ContainedTrile { get; set; } [XnbProperty(UseConverter = true)] @@ -20,7 +20,7 @@ public class TrileInstanceActorSettings [XnbProperty(UseConverter = true)] public string SequenceAlternateSampleName { get; set; } = ""; - [XnbProperty(Optional = true)] + [XnbProperty(UseConverter = true)] public int? HostVolume { get; set; } } } diff --git a/Core/Definitions/Game/Level/VibrationMotor.cs b/Core/Definitions/Game/Level/VibrationMotor.cs index 86fb786..8311db7 100644 --- a/Core/Definitions/Game/Level/VibrationMotor.cs +++ b/Core/Definitions/Game/Level/VibrationMotor.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.Input.VibrationMotor")] + [XnbType("FezEngine.Structure.Input.VibrationMotor, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum VibrationMotor { None, diff --git a/Core/Definitions/Game/Level/Viewpoint.cs b/Core/Definitions/Game/Level/Viewpoint.cs index 374878c..7a402c2 100644 --- a/Core/Definitions/Game/Level/Viewpoint.cs +++ b/Core/Definitions/Game/Level/Viewpoint.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Viewpoint")] + [XnbType("FezEngine.Viewpoint, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum Viewpoint { None, diff --git a/Core/Definitions/Game/Level/Volume.cs b/Core/Definitions/Game/Level/Volume.cs index 71e7707..570b204 100644 --- a/Core/Definitions/Game/Level/Volume.cs +++ b/Core/Definitions/Game/Level/Volume.cs @@ -3,8 +3,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.Volume")] - [XnbReaderType("FezEngine.Readers.VolumeReader")] + [XnbType("FezEngine.Structure.Volume, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.VolumeReader, FezEngine")] public class Volume { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/Level/VolumeActorSettings.cs b/Core/Definitions/Game/Level/VolumeActorSettings.cs index a451423..98677fb 100644 --- a/Core/Definitions/Game/Level/VolumeActorSettings.cs +++ b/Core/Definitions/Game/Level/VolumeActorSettings.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.Level { - [XnbType("FezEngine.Structure.VolumeActorSettings")] - [XnbReaderType("FezEngine.Readers.VolumeActorSettingsReader")] + [XnbType("FezEngine.Structure.VolumeActorSettings, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.VolumeActorSettingsReader, FezEngine")] public class VolumeActorSettings { [XnbProperty] diff --git a/Core/Definitions/Game/MapTree/MapNode.cs b/Core/Definitions/Game/MapTree/MapNode.cs index 13cf67e..0891238 100644 --- a/Core/Definitions/Game/MapTree/MapNode.cs +++ b/Core/Definitions/Game/MapTree/MapNode.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.MapTree { - [XnbType("FezEngine.Structure.MapNode")] - [XnbReaderType("FezEngine.Readers.MapNodeReader")] + [XnbType("FezEngine.Structure.MapNode, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.MapNodeReader, FezEngine")] public class MapNode { [XnbProperty] diff --git a/Core/Definitions/Game/MapTree/MapNodeConnection.cs b/Core/Definitions/Game/MapTree/MapNodeConnection.cs index d9d0072..52aa3f3 100644 --- a/Core/Definitions/Game/MapTree/MapNodeConnection.cs +++ b/Core/Definitions/Game/MapTree/MapNodeConnection.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.MapTree { - [XnbType("FezEngine.Structure.MapNode+Connection")] - [XnbReaderType("FezEngine.Readers.MapNodeConnectionReader")] + [XnbType("FezEngine.Structure.MapNode+Connection, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.MapNodeConnectionReader, FezEngine")] public class MapNodeConnection { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/MapTree/MapTree.cs b/Core/Definitions/Game/MapTree/MapTree.cs index 09b3029..da60a45 100644 --- a/Core/Definitions/Game/MapTree/MapTree.cs +++ b/Core/Definitions/Game/MapTree/MapTree.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.MapTree { - [XnbType("FezEngine.Structure.MapTree")] - [XnbReaderType("FezEngine.Readers.MapTreeReader")] + [XnbType("FezEngine.Structure.MapTree, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.MapTreeReader, FezEngine")] public class MapTree { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/MapTree/WinConditions.cs b/Core/Definitions/Game/MapTree/WinConditions.cs index cc2a780..d6bc8f8 100644 --- a/Core/Definitions/Game/MapTree/WinConditions.cs +++ b/Core/Definitions/Game/MapTree/WinConditions.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.MapTree { - [XnbType("FezEngine.Structure.WinConditions")] - [XnbReaderType("FezEngine.Readers.WinConditionsReader")] + [XnbType("FezEngine.Structure.WinConditions, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.WinConditionsReader, FezEngine")] public class WinConditions { [XnbProperty] diff --git a/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs b/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs index 0b5def2..e4f1f4f 100644 --- a/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs +++ b/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs @@ -2,8 +2,8 @@ namespace FEZRepacker.Core.Definitions.Game.NpcMetadata { - [XnbType("FezEngine.Structure.NpcMetadata")] - [XnbReaderType("FezEngine.Readers.NpcMetadataReader")] + [XnbType("FezEngine.Structure.NpcMetadata, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.NpcMetadataReader, FezEngine")] public class NpcMetadata { [XnbProperty] diff --git a/Core/Definitions/Game/Sky/Sky.cs b/Core/Definitions/Game/Sky/Sky.cs index f1f501d..7c5fc94 100644 --- a/Core/Definitions/Game/Sky/Sky.cs +++ b/Core/Definitions/Game/Sky/Sky.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Sky { - [XnbType("FezEngine.Structure.Sky")] - [XnbReaderType("FezEngine.Readers.SkyReader")] + [XnbType("FezEngine.Structure.Sky, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.SkyReader, FezEngine")] public class Sky { [XnbProperty] diff --git a/Core/Definitions/Game/Sky/SkyLayer.cs b/Core/Definitions/Game/Sky/SkyLayer.cs index 0b3b375..f6b7e85 100644 --- a/Core/Definitions/Game/Sky/SkyLayer.cs +++ b/Core/Definitions/Game/Sky/SkyLayer.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.Sky { - [XnbType("FezEngine.Structure.SkyLayer")] - [XnbReaderType("FezEngine.Readers.SkyLayerReader")] + [XnbType("FezEngine.Structure.SkyLayer, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.SkyLayerReader, FezEngine")] public class SkyLayer { [XnbProperty] diff --git a/Core/Definitions/Game/TrackedSong/AssembleChords.cs b/Core/Definitions/Game/TrackedSong/AssembleChords.cs index 56b2811..ab9323f 100644 --- a/Core/Definitions/Game/TrackedSong/AssembleChords.cs +++ b/Core/Definitions/Game/TrackedSong/AssembleChords.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.TrackedSong { - [XnbType("FezEngine.Structure.AssembleChords")] + [XnbType("FezEngine.Structure.AssembleChords, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum AssembleChords { C_maj, diff --git a/Core/Definitions/Game/TrackedSong/Loop.cs b/Core/Definitions/Game/TrackedSong/Loop.cs index 79fde2b..8f13b21 100644 --- a/Core/Definitions/Game/TrackedSong/Loop.cs +++ b/Core/Definitions/Game/TrackedSong/Loop.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.TrackedSong { - [XnbType("FezEngine.Structure.Loop")] - [XnbReaderType("FezEngine.Readers.LoopReader")] + [XnbType("FezEngine.Structure.Loop, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.LoopReader, FezEngine")] public class Loop { [XnbProperty] diff --git a/Core/Definitions/Game/TrackedSong/ShardNotes.cs b/Core/Definitions/Game/TrackedSong/ShardNotes.cs index 345dd30..c7b01a6 100644 --- a/Core/Definitions/Game/TrackedSong/ShardNotes.cs +++ b/Core/Definitions/Game/TrackedSong/ShardNotes.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.TrackedSong { - [XnbType("FezEngine.Structure.ShardNotes")] + [XnbType("FezEngine.Structure.ShardNotes, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum ShardNotes { C2, diff --git a/Core/Definitions/Game/TrackedSong/TrackedSong.cs b/Core/Definitions/Game/TrackedSong/TrackedSong.cs index d981409..0bd69bd 100644 --- a/Core/Definitions/Game/TrackedSong/TrackedSong.cs +++ b/Core/Definitions/Game/TrackedSong/TrackedSong.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.TrackedSong { - [XnbType("FezEngine.Structure.TrackedSong")] - [XnbReaderType("FezEngine.Readers.TrackedSongReader")] + [XnbType("FezEngine.Structure.TrackedSong, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrackedSongReader, FezEngine")] public class TrackedSong { [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/TrileSet/CollisionType.cs b/Core/Definitions/Game/TrileSet/CollisionType.cs index 4a828fd..05015a6 100644 --- a/Core/Definitions/Game/TrileSet/CollisionType.cs +++ b/Core/Definitions/Game/TrileSet/CollisionType.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.TrileSet { - [XnbType("FezEngine.CollisionType")] + [XnbType("FezEngine.CollisionType, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum CollisionType { AllSides, diff --git a/Core/Definitions/Game/TrileSet/SurfaceType.cs b/Core/Definitions/Game/TrileSet/SurfaceType.cs index 52bcd6b..5cde764 100644 --- a/Core/Definitions/Game/TrileSet/SurfaceType.cs +++ b/Core/Definitions/Game/TrileSet/SurfaceType.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.TrileSet { - [XnbType("FezEngine.Structure.SurfaceType")] + [XnbType("FezEngine.Structure.SurfaceType, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] public enum SurfaceType { Grass, diff --git a/Core/Definitions/Game/TrileSet/Trile.cs b/Core/Definitions/Game/TrileSet/Trile.cs index 27eac67..8765d37 100644 --- a/Core/Definitions/Game/TrileSet/Trile.cs +++ b/Core/Definitions/Game/TrileSet/Trile.cs @@ -7,8 +7,8 @@ namespace FEZRepacker.Core.Definitions.Game.TrileSet { - [XnbType("FezEngine.Structure.Trile")] - [XnbReaderType("FezEngine.Readers.TrileReader")] + [XnbType("FezEngine.Structure.Trile, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrileReader, FezEngine")] public class Trile { [XnbProperty] @@ -36,11 +36,11 @@ public class Trile public bool ForceHugging { get; set; } [XnbProperty(UseConverter = true)] - public Dictionary Faces { get; set; } = new(); + public IDictionary Faces { get; set; } = null!; [JsonIgnore] [XnbProperty(UseConverter = true)] - public IndexedPrimitives Geometry { get; set; } = new(); + public IndexedPrimitives Geometry { get; set; } = null!; [XnbProperty(UseConverter = true)] public ActorType Type { get; set; } diff --git a/Core/Definitions/Game/TrileSet/TrileSet.cs b/Core/Definitions/Game/TrileSet/TrileSet.cs index 329434f..0673a8f 100644 --- a/Core/Definitions/Game/TrileSet/TrileSet.cs +++ b/Core/Definitions/Game/TrileSet/TrileSet.cs @@ -1,19 +1,19 @@ using System.Text.Json.Serialization; using FEZRepacker.Core.Definitions.Game.XNA; - +using FEZRepacker.Core.Helpers; namespace FEZRepacker.Core.Definitions.Game.TrileSet { - [XnbType("FezEngine.Structure.TrileSet")] - [XnbReaderType("FezEngine.Readers.TrileSetReader")] + [XnbType("FezEngine.Structure.TrileSet, FezEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")] + [XnbReaderType("FezEngine.Readers.TrileSetReader, FezEngine")] public class TrileSet { [XnbProperty] public string Name { get; set; } = ""; [XnbProperty(UseConverter = true)] - public Dictionary Triles { get; set; } = new(); + public IDictionary Triles { get; set; } = new OrderedDictionary(); [JsonIgnore] [XnbProperty(UseConverter = true)] diff --git a/Core/Definitions/Game/XNA/Color.cs b/Core/Definitions/Game/XNA/Color.cs index b2fe85e..7e35d5f 100644 --- a/Core/Definitions/Game/XNA/Color.cs +++ b/Core/Definitions/Game/XNA/Color.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Color")] + [XnbType("Microsoft.Xna.Framework.Color, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.ColorReader")] public class Color { diff --git a/Core/Definitions/Game/XNA/Effect.cs b/Core/Definitions/Game/XNA/Effect.cs index 3cf8e2c..bf3a5f8 100644 --- a/Core/Definitions/Game/XNA/Effect.cs +++ b/Core/Definitions/Game/XNA/Effect.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Graphics.Effect")] - [XnbReaderType("Microsoft.Xna.Framework.Content.EffectReader")] + [XnbType("Microsoft.Xna.Framework.Graphics.Effect, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] + [XnbReaderType("Microsoft.Xna.Framework.Content.EffectReader, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] public class Effect { // Definition of this class is nothing alike what's actually in the game. diff --git a/Core/Definitions/Game/XNA/Matrix.cs b/Core/Definitions/Game/XNA/Matrix.cs index c0cd9ae..bbdca4e 100644 --- a/Core/Definitions/Game/XNA/Matrix.cs +++ b/Core/Definitions/Game/XNA/Matrix.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Matrix")] + [XnbType("Microsoft.Xna.Framework.Matrix, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.MatrixReader")] public class Matrix { diff --git a/Core/Definitions/Game/XNA/PrimitiveType.cs b/Core/Definitions/Game/XNA/PrimitiveType.cs index 0b6cfea..2d8a53d 100644 --- a/Core/Definitions/Game/XNA/PrimitiveType.cs +++ b/Core/Definitions/Game/XNA/PrimitiveType.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Graphics.PrimitiveType")] + [XnbType("Microsoft.Xna.Framework.Graphics.PrimitiveType, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] public enum PrimitiveType { TriangleList, diff --git a/Core/Definitions/Game/XNA/Quaternion.cs b/Core/Definitions/Game/XNA/Quaternion.cs index 88e0fd5..d59daa7 100644 --- a/Core/Definitions/Game/XNA/Quaternion.cs +++ b/Core/Definitions/Game/XNA/Quaternion.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Quaternion")] + [XnbType("Microsoft.Xna.Framework.Quaternion, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.QuaternionReader")] public struct Quaternion : IEquatable { diff --git a/Core/Definitions/Game/XNA/Rectangle.cs b/Core/Definitions/Game/XNA/Rectangle.cs index 5669f49..66a1d07 100644 --- a/Core/Definitions/Game/XNA/Rectangle.cs +++ b/Core/Definitions/Game/XNA/Rectangle.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Rectangle")] + [XnbType("Microsoft.Xna.Framework.Rectangle, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.RectangleReader")] public class Rectangle { diff --git a/Core/Definitions/Game/XNA/SoundEffect.cs b/Core/Definitions/Game/XNA/SoundEffect.cs index 0aff7c8..620f481 100644 --- a/Core/Definitions/Game/XNA/SoundEffect.cs +++ b/Core/Definitions/Game/XNA/SoundEffect.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Audio.SoundEffect")] + [XnbType("Microsoft.Xna.Framework.Audio.SoundEffect, Microsoft.Xna.Framework.Audio, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.SoundEffectReader")] public class SoundEffect { @@ -43,6 +43,6 @@ public class SoundEffect public int LoopLength { get; set; } [XnbProperty] - public int UnknownValue { get; set; } + public int DurationMs { get; set; } } } diff --git a/Core/Definitions/Game/XNA/SpriteFont.cs b/Core/Definitions/Game/XNA/SpriteFont.cs index c38f1c8..503e596 100644 --- a/Core/Definitions/Game/XNA/SpriteFont.cs +++ b/Core/Definitions/Game/XNA/SpriteFont.cs @@ -1,8 +1,8 @@  namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Graphics.SpriteFont")] - [XnbReaderType("Microsoft.Xna.Framework.Content.SpriteFontReader")] + [XnbType("Microsoft.Xna.Framework.Graphics.SpriteFont, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] + [XnbReaderType("Microsoft.Xna.Framework.Content.SpriteFontReader, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] public class SpriteFont { [XnbProperty(UseConverter = true)] @@ -27,6 +27,6 @@ public class SpriteFont public List KerningData { get; set; } = new(); [XnbProperty(UseConverter = true, Optional = true)] - public char DefaultCharacter { get; set; } = '\u0000'; + public char? DefaultCharacter { get; set; } } } diff --git a/Core/Definitions/Game/XNA/SurfaceFormat.cs b/Core/Definitions/Game/XNA/SurfaceFormat.cs index 052f212..4a7bdb3 100644 --- a/Core/Definitions/Game/XNA/SurfaceFormat.cs +++ b/Core/Definitions/Game/XNA/SurfaceFormat.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Graphics.SurfaceFormat")] + [XnbType("Microsoft.Xna.Framework.Graphics.SurfaceFormat, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] public enum SurfaceFormat { Color = 0, diff --git a/Core/Definitions/Game/XNA/Texture2D.cs b/Core/Definitions/Game/XNA/Texture2D.cs index dbd5205..4fdbc7b 100644 --- a/Core/Definitions/Game/XNA/Texture2D.cs +++ b/Core/Definitions/Game/XNA/Texture2D.cs @@ -1,7 +1,7 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Graphics.Texture2D")] - [XnbReaderType("Microsoft.Xna.Framework.Content.Texture2DReader")] + [XnbType("Microsoft.Xna.Framework.Graphics.Texture2D, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] + [XnbReaderType("Microsoft.Xna.Framework.Content.Texture2DReader, Microsoft.Xna.Framework.Graphics, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] public class Texture2D { [XnbProperty(UseConverter = true, SkipIdentifier = true)] diff --git a/Core/Definitions/Game/XNA/Vector2.cs b/Core/Definitions/Game/XNA/Vector2.cs index f6ae404..60c39e2 100644 --- a/Core/Definitions/Game/XNA/Vector2.cs +++ b/Core/Definitions/Game/XNA/Vector2.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Vector2")] + [XnbType("Microsoft.Xna.Framework.Vector2, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.Vector2Reader")] public struct Vector2 : IEquatable { diff --git a/Core/Definitions/Game/XNA/Vector3.cs b/Core/Definitions/Game/XNA/Vector3.cs index 9fdee22..6119d7a 100644 --- a/Core/Definitions/Game/XNA/Vector3.cs +++ b/Core/Definitions/Game/XNA/Vector3.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Vector3")] + [XnbType("Microsoft.Xna.Framework.Vector3, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.Vector3Reader")] public struct Vector3 : IEquatable { diff --git a/Core/Definitions/Game/XNA/Vector4.cs b/Core/Definitions/Game/XNA/Vector4.cs index 4dde5b7..58da68a 100644 --- a/Core/Definitions/Game/XNA/Vector4.cs +++ b/Core/Definitions/Game/XNA/Vector4.cs @@ -1,6 +1,6 @@ namespace FEZRepacker.Core.Definitions.Game.XNA { - [XnbType("Microsoft.Xna.Framework.Vector4")] + [XnbType("Microsoft.Xna.Framework.Vector4, Microsoft.Xna.Framework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=842cf8be1de50553")] [XnbReaderType("Microsoft.Xna.Framework.Content.Vector4Reader")] public struct Vector4 : IEquatable { diff --git a/Core/Definitions/Game/XnbReaderTypeAttribute.cs b/Core/Definitions/Game/XnbReaderTypeAttribute.cs index 476299d..b2d918e 100644 --- a/Core/Definitions/Game/XnbReaderTypeAttribute.cs +++ b/Core/Definitions/Game/XnbReaderTypeAttribute.cs @@ -11,6 +11,7 @@ namespace FEZRepacker.Core.Definitions.Game internal sealed class XnbReaderTypeAttribute : Attribute { public XnbAssemblyQualifier Qualifier { get; set; } + public bool IsPrivate { get; set; } public XnbReaderTypeAttribute(string _qualifier) { diff --git a/Core/Definitions/Json/LevelJsonModel.cs b/Core/Definitions/Json/LevelJsonModel.cs index d44068b..b5668c5 100644 --- a/Core/Definitions/Json/LevelJsonModel.cs +++ b/Core/Definitions/Json/LevelJsonModel.cs @@ -3,6 +3,7 @@ using FEZRepacker.Core.Definitions.Game.Level; using FEZRepacker.Core.Definitions.Game.Level.Scripting; using FEZRepacker.Core.Definitions.Game.XNA; +using FEZRepacker.Core.Helpers; namespace FEZRepacker.Core.Definitions.Json { @@ -35,13 +36,13 @@ public class LevelJsonModel : JsonModel public int FAPFadeOutLength { get; set; } public string TrileSetName { get; set; } = ""; public List Triles { get; set; } = new(); - public Dictionary Groups { get; set; } = new(); - public Dictionary Volumes { get; set; } = new(); - public Dictionary Scripts { get; set; } = new(); - public Dictionary ArtObjects { get; set; } = new(); - public Dictionary BackgroundPlanes { get; set; } = new(); - public Dictionary Paths { get; set; } = new(); - public Dictionary NonPlayerCharacters { get; set; } = new(); + public IDictionary Groups { get; set; } = new OrderedDictionary(); + public IDictionary Volumes { get; set; } = new OrderedDictionary(); + public IDictionary Scripts { get; set; } = new OrderedDictionary(); + public IDictionary ArtObjects { get; set; } = new OrderedDictionary(); + public IDictionary BackgroundPlanes { get; set; } = new OrderedDictionary(); + public IDictionary Paths { get; set; } = new OrderedDictionary(); + public IDictionary NonPlayerCharacters { get; set; } = new OrderedDictionary(); public LevelJsonModel() { @@ -91,20 +92,8 @@ public Level Deserialize() ArtObjects = ArtObjects, }; - foreach (var trileModel in Triles) - { - var trile = trileModel.Deserialize(); - if (!level.Triles.ContainsKey(trileModel.Emplacement)) - { - level.Triles[trileModel.Emplacement] = trile; - } - else - { - level.Triles[trileModel.Emplacement].OverlappedTriles.Add(trile); - } - } - - level.Groups = Groups.ToDictionary(pair => pair.Key, pair => pair.Value.Deserialize()); + ArrangeTrilesIntoLevel(level); + RecreateGroupsInLevel(level); return level; } @@ -144,21 +133,83 @@ public void SerializeFrom(Level level) Volumes = level.Volumes; ArtObjects = level.ArtObjects; - // sort tiles into modified structures - Triles = new(); + ExtractTrileListFromLevel(level); + ExtractCleanedGroupsFromLevel(level); + } + + private void ExtractTrileListFromLevel(Level level) + { + // OverlappedTriles list in original Level's Trile Instance structure exists as a way to store + // multiple triles under the same TrileEmplacement key in Triles dictionary. + // This is wasteful for JSON format. To avoid this, custom model for TrileInstance containing Emplacement + // is made, so overlapping triles can exist next to one another. + // We're merging them back into OverlappedTriles array in ArrangeTrilesIntoLevel during deconversion. + + Triles = new(level.Triles.Count); foreach (var trileRecord in level.Triles) { var pos = trileRecord.Key; var instance = trileRecord.Value; Triles.Add(new TrileInstanceJsonModel(pos, instance)); + + if (instance.OverlappedTriles == null) + { + continue; + } + foreach (var overlapping in instance.OverlappedTriles) { Triles.Add(new TrileInstanceJsonModel(pos, overlapping)); } } + } - // create groups of modified paths - Groups = level.Groups.ToDictionary(pair => pair.Key, pair => new TrileGroupJsonModel(pair.Value)); + private void ArrangeTrilesIntoLevel(Level level) + { + level.Triles = new OrderedDictionary(); + foreach (var trileModel in Triles) + { + var trile = trileModel.Deserialize(); + + if (level.Triles.TryGetValue(trileModel.Emplacement, out var existingTrile)) + { + if (existingTrile.OverlappedTriles == null) + { + existingTrile.OverlappedTriles = new List(); + } + + level.Triles[trileModel.Emplacement].OverlappedTriles.Add(trile); + continue; + } + + level.Triles[trileModel.Emplacement] = trile; + } + } + + private void ExtractCleanedGroupsFromLevel(Level level) + { + // Groups store exact copies of contained TrileInstances. This is very wasteful, and even the game + // dumps them and looks them up again using TrileEmplacement calculated from trile's position + // (see FezEngine.Structure.Level.OnDeserialization()) + // We're dumping all trile data using custom model for TrileGroup, + // and then rebuilding it in RecreateGroupsInLevel. + + Groups = new Dictionary(); + foreach (var pair in level.Groups) + { + Groups[pair.Key] = new TrileGroupJsonModel(pair.Value); + } + } + + private void RecreateGroupsInLevel(Level level) + { + level.Groups = new Dictionary(); + foreach (var groupModel in Groups) + { + var group = groupModel.Value.Deserialize(); + groupModel.Value.ReconstructTrilesInGroup(group, level); + level.Groups[groupModel.Key] = group; + } } } } diff --git a/Core/Definitions/Json/SpriteFontPropertiesJsonModel.cs b/Core/Definitions/Json/SpriteFontPropertiesJsonModel.cs index 32e1bce..77e48a0 100644 --- a/Core/Definitions/Json/SpriteFontPropertiesJsonModel.cs +++ b/Core/Definitions/Json/SpriteFontPropertiesJsonModel.cs @@ -16,7 +16,7 @@ public class CharacterData public int LineSpacing { get; set; } public float Spacing { get; set; } - public char DefaultCharacter { get; set; } + public char? DefaultCharacter { get; set; } public List Characters { get; set; } public SpriteFontPropertiesJsonModel() diff --git a/Core/Definitions/Json/TrileGroupJsonModel.cs b/Core/Definitions/Json/TrileGroupJsonModel.cs index 1a6c5e2..69943f5 100644 --- a/Core/Definitions/Json/TrileGroupJsonModel.cs +++ b/Core/Definitions/Json/TrileGroupJsonModel.cs @@ -58,13 +58,30 @@ public TrileGroup Deserialize() AssociatedSound = AssociatedSound, }; - trileGroup.Triles = Triles - .Select(x => new TrileInstance() { Position = new(x.X, x.Y, x.Z) }) - .ToList(); - return trileGroup; } + public void ReconstructTrilesInGroup(TrileGroup trileGroup, Level level) + { + trileGroup.Triles = new List(); + foreach (var emplacement in Triles) + { + if (!level.Triles.TryGetValue(emplacement, out var existingTrile)) + { + // If we couldn't find linked trile instance in level, then something went + // terribly wrong with the level file. This fallback is here to make sure + // the game tries to find it as well and fail miserably so user knows about it + trileGroup.Triles.Add(new TrileInstance() + { + Position = new (emplacement.X, emplacement.Y, emplacement.Z) + }); + continue; + } + + trileGroup.Triles.Add(existingTrile); + } + } + private void CopyBasicPropertiesOverFrom(TrileGroup trileGroup) { Path = trileGroup.Path!; @@ -88,6 +105,8 @@ public void SerializeFrom(TrileGroup trileGroup) { CopyBasicPropertiesOverFrom(trileGroup); + // leaving only emplacements, as triles are most likely exact copies of triles from level + // see LevelJsonModel.ExtractCleanedGroupsFromLevel for details Triles = trileGroup.Triles.Select(x => new TrileEmplacement(x.Position)).ToList(); } } diff --git a/Core/FEZRepacker.Core.csproj b/Core/FEZRepacker.Core.csproj index b3eba64..9801b3c 100644 --- a/Core/FEZRepacker.Core.csproj +++ b/Core/FEZRepacker.Core.csproj @@ -25,6 +25,10 @@ + + + + diff --git a/Core/Helpers/BinaryStreamExtensions.cs b/Core/Helpers/BinaryStreamExtensions.cs index 339bafe..f47feee 100644 --- a/Core/Helpers/BinaryStreamExtensions.cs +++ b/Core/Helpers/BinaryStreamExtensions.cs @@ -56,6 +56,14 @@ public static void Write(this BinaryWriter writer, Color color) writer.Write(color.A); } + public static void WriteInts(this BinaryWriter writer, params int[] values) + { + foreach (var value in values) + { + writer.Write(value); + } + } + // Source: hhttps://source.dot.net/#Microsoft.Build.Framework/BinaryReaderExtensions.cs,20 public static int Read7BitEncodedInt(this BinaryReader reader) { diff --git a/Core/Helpers/DirectXTex/Bc.cs b/Core/Helpers/DirectXTex/Bc.cs new file mode 100644 index 0000000..b658a51 --- /dev/null +++ b/Core/Helpers/DirectXTex/Bc.cs @@ -0,0 +1,976 @@ + +// C# port of BC.cpp from DirectXTex repository +// for the purpose of block-compression (BC) functionality (DXT1, DXT3 and DXT5) +// https://github.com/microsoft/DirectXTex/blob/a6c5c5939a9d3a3ca3d88a1b59a7c319d37fcf78/DirectXTex/BC.cpp +// +// Original code licensed under MIT License, Copyright (c) Microsoft Corporation. +// +// Certain things have been simplified under assumption of +// no flags nor experimental compile-time flags being used. + +using System.Numerics; + +namespace DirectX +{ + internal static class Bc + { + private const int NUM_PIXELS_PER_BLOCK = 16; + private const float FLT_MIN = 1.17549435E-38f; + + private static readonly HDRColorA g_Luminance = new(0.2125f / 0.7154f, 1.0f, 0.0721f / 0.7154f, 1.0f); + private static readonly HDRColorA g_LuminanceInv = new(0.7154f / 0.2125f, 1.0f, 0.7154f / 0.0721f, 1.0f); + + private struct HDRColorA(float r, float g, float b, float a) + { + public float r = r; + public float g = g; + public float b = b; + public float a = a; + } + + public struct D3DX_BC1 + { + public ushort[] rgb; + public uint bitmap; + + public D3DX_BC1() + { + rgb = new ushort[2]; + bitmap = 0; + } + }; + + public struct D3DX_BC2 + { + public uint[] bitmap; + public D3DX_BC1 bc1; + + public D3DX_BC2() + { + bitmap = new uint[2]; + bc1 = new D3DX_BC1(); + } + }; + + public struct D3DX_BC3 + { + public byte[] alpha; + public byte[] bitmap; + public D3DX_BC1 bc1; + + public D3DX_BC3() + { + alpha = new byte[2]; + bitmap = new byte[6]; + bc1 = new D3DX_BC1(); + } + }; + + private struct XMU565 + { + public ushort Packed; + + public XMU565(ushort packed) => Packed = packed; + + public byte R => (byte)((Packed >> 0) & 0x1F); + public byte G => (byte)((Packed >> 5) & 0x3F); + public byte B => (byte)((Packed >> 11) & 0x1F); + } + + private static void HDRColorALerp(ref HDRColorA pOut, HDRColorA pC1, HDRColorA pC2, float s) + { + pOut.r = pC1.r + s * (pC2.r - pC1.r); + pOut.g = pC1.g + s * (pC2.g - pC1.g); + pOut.b = pC1.b + s * (pC2.b - pC1.b); + pOut.a = pC1.a + s * (pC2.a - pC1.a); + } + + private static Vector4 XMLoadU565(XMU565 source) + { + return new Vector4(source.R, source.G, source.B, 0.0f); + } + + private static Vector4 XMVectorSwizzle(Vector4 V, uint E0, uint E1, uint E2, uint E3) + { + float[] f = { V.X, V.Y, V.Z, V.W }; + return new Vector4(f[E0], f[E1], f[E2], f[E3]); + } + + private static void OptimizeAlpha(bool bRange, ref float pX, ref float pY, ReadOnlySpan pPoints, uint cSteps) + { + float[] pC6 = [ 5.0f/5.0f, 4.0f/5.0f, 3.0f/5.0f, 2.0f/5.0f, 1.0f/5.0f, 0.0f/5.0f ]; + float[] pD6 = [ 0.0f/5.0f, 1.0f/5.0f, 2.0f/5.0f, 3.0f/5.0f, 4.0f/5.0f, 5.0f/5.0f ]; + float[] pC8 = [ 7.0f/7.0f, 6.0f/7.0f, 5.0f/7.0f, 4.0f/7.0f, 3.0f/7.0f, 2.0f/7.0f, 1.0f/7.0f, 0.0f/7.0f ]; + float[] pD8 = [ 0.0f/7.0f, 1.0f/7.0f, 2.0f/7.0f, 3.0f/7.0f, 4.0f/7.0f, 5.0f/7.0f, 6.0f/7.0f, 7.0f/7.0f ]; + + float[] pC = (6 == cSteps) ? pC6 : pC8; + float[] pD = (6 == cSteps) ? pD6 : pD8; + + float MAX_VALUE = 1.0f; + float MIN_VALUE; + if (bRange) + { + MIN_VALUE = -1.0f; + } + else + { + MIN_VALUE = 0.0f; + } + + float fX = MAX_VALUE; + float fY = MIN_VALUE; + + if(8 == cSteps) + { + for(int iPoint = 0; iPoint < NUM_PIXELS_PER_BLOCK; iPoint++) + { + if(pPoints[iPoint] < fX) + fX = pPoints[iPoint]; + + if(pPoints[iPoint] > fY) + fY = pPoints[iPoint]; + } + } + else + { + for(int iPoint = 0; iPoint < NUM_PIXELS_PER_BLOCK; iPoint++) + { + if(pPoints[iPoint] < fX && pPoints[iPoint] > MIN_VALUE) + fX = pPoints[iPoint]; + + if(pPoints[iPoint] > fY && pPoints[iPoint] < MAX_VALUE) + fY = pPoints[iPoint]; + } + + if (fX == fY) + { + fY = MAX_VALUE; + } + } + + float fSteps = (float) (cSteps - 1); + + for(int iIteration = 0; iIteration < 8; iIteration++) + { + float fScale; + + if((fY - fX) < (1.0f / 256.0f)) + break; + + fScale = fSteps / (fY - fX); + + float[] pSteps = new float[8]; + + for(int iStep = 0; iStep < cSteps; iStep++) + pSteps[iStep] = pC[iStep] * fX + pD[iStep] * fY; + + if(6 == cSteps) + { + pSteps[6] = MIN_VALUE; + pSteps[7] = MAX_VALUE; + } + + float dX = 0.0f; + float dY = 0.0f; + float d2X = 0.0f; + float d2Y = 0.0f; + + for(int iPoint = 0; iPoint < NUM_PIXELS_PER_BLOCK; iPoint++) + { + float fDot = (pPoints[iPoint] - fX) * fScale; + + uint iStep; + + if(fDot <= 0.0f) + iStep = (uint)(((6 == cSteps) && (pPoints[iPoint] <= fX * 0.5f)) ? 6 : 0); + else if(fDot >= fSteps) + iStep = ((6 == cSteps) && (pPoints[iPoint] >= (fY + 1.0f) * 0.5f)) ? 7 : (cSteps - 1); + else + iStep = (uint)(fDot + 0.5f); + + + if(iStep < cSteps) + { + float fDiff = pSteps[iStep] - pPoints[iPoint]; + + dX += pC[iStep] * fDiff; + d2X += pC[iStep] * pC[iStep]; + + dY += pD[iStep] * fDiff; + d2Y += pD[iStep] * pD[iStep]; + } + } + + if(d2X > 0.0f) + fX -= dX / d2X; + + if(d2Y > 0.0f) + fY -= dY / d2Y; + + if(fX > fY) + { + float f = fX; fX = fY; fY = f; + } + + if((dX * dX < (1.0f / 64.0f)) && (dY * dY < (1.0f / 64.0f))) + break; + } + + pX = (fX < MIN_VALUE) ? MIN_VALUE : (fX > MAX_VALUE) ? MAX_VALUE : fX; + pY = (fY < MIN_VALUE) ? MIN_VALUE : (fY > MAX_VALUE) ? MAX_VALUE : fY; + } + + private static void assert(bool condition) + { + if (!condition) + { + throw new Exception(); + } + } + + private static void Decode565(HDRColorA pColor, ushort w565) + { + pColor.r = (float)((w565 >> 11) & 31) * (1.0f / 31.0f); + pColor.g = (float)((w565 >> 5) & 63) * (1.0f / 63.0f); + pColor.b = (float)((w565 >> 0) & 31) * (1.0f / 31.0f); + pColor.a = 1.0f; + } + + private static ushort Encode565(HDRColorA pColor) + { + HDRColorA Color; + + Color.r = (pColor.r < 0.0f) ? 0.0f : (pColor.r > 1.0f) ? 1.0f : pColor.r; + Color.g = (pColor.g < 0.0f) ? 0.0f : (pColor.g > 1.0f) ? 1.0f : pColor.g; + Color.b = (pColor.b < 0.0f) ? 0.0f : (pColor.b > 1.0f) ? 1.0f : pColor.b; + + ushort w; + + w = (ushort)(((int)(Color.r * 31.0f + 0.5f) << 11) | + ((int)(Color.g * 63.0f + 0.5f) << 5) | + ((int)(Color.b * 31.0f + 0.5f) << 0)); + + return w; + } + + static void OptimizeRGB(ref HDRColorA pX, ref HDRColorA pY, HDRColorA[] pPoints, int cSteps, uint flags) + { + const float fEpsilon = (0.25f / 64.0f) * (0.25f / 64.0f); + + float[] pC3 = [2.0f / 2.0f, 1.0f / 2.0f, 0.0f / 2.0f]; + float[] pD3 = [0.0f / 2.0f, 1.0f / 2.0f, 2.0f / 2.0f]; + float[] pC4 = [3.0f / 3.0f, 2.0f / 3.0f, 1.0f / 3.0f, 0.0f / 3.0f]; + float[] pD4 = [0.0f / 3.0f, 1.0f / 3.0f, 2.0f / 3.0f, 3.0f / 3.0f]; + + float[] pC = (3 == cSteps) ? pC3 : pC4; + float[] pD = (3 == cSteps) ? pD3 : pD4; + + HDRColorA X = g_Luminance; + HDRColorA Y = new(0.0f, 0.0f, 0.0f, 1.0f); + + for (int iPoint = 0; iPoint < NUM_PIXELS_PER_BLOCK; iPoint++) + { + if (pPoints[iPoint].r < X.r) + X.r = pPoints[iPoint].r; + + if (pPoints[iPoint].g < X.g) + X.g = pPoints[iPoint].g; + + if (pPoints[iPoint].b < X.b) + X.b = pPoints[iPoint].b; + + if (pPoints[iPoint].r > Y.r) + Y.r = pPoints[iPoint].r; + + if (pPoints[iPoint].g > Y.g) + Y.g = pPoints[iPoint].g; + + if (pPoints[iPoint].b > Y.b) + Y.b = pPoints[iPoint].b; + } + + HDRColorA AB; + + AB.r = Y.r - X.r; + AB.g = Y.g - X.g; + AB.b = Y.b - X.b; + + float fAB = AB.r * AB.r + AB.g * AB.g + AB.b * AB.b; + + if (fAB < FLT_MIN) + { + pX.r = X.r; + pX.g = X.g; + pX.b = X.b; + pY.r = Y.r; + pY.g = Y.g; + pY.b = Y.b; + return; + } + + float fABInv = 1.0f / fAB; + + HDRColorA Dir; + Dir.r = AB.r * fABInv; + Dir.g = AB.g * fABInv; + Dir.b = AB.b * fABInv; + + HDRColorA Mid; + Mid.r = (X.r + Y.r) * 0.5f; + Mid.g = (X.g + Y.g) * 0.5f; + Mid.b = (X.b + Y.b) * 0.5f; + + float[] fDir = new float[4]; + fDir[0] = fDir[1] = fDir[2] = fDir[3] = 0.0f; + + + for (int iPoint = 0; iPoint < NUM_PIXELS_PER_BLOCK; iPoint++) + { + HDRColorA Pt; + Pt.r = (pPoints[iPoint].r - Mid.r) * Dir.r; + Pt.g = (pPoints[iPoint].g - Mid.g) * Dir.g; + Pt.b = (pPoints[iPoint].b - Mid.b) * Dir.b; + + float f; + + f = Pt.r + Pt.g + Pt.b; + fDir[0] += f * f; + + f = Pt.r + Pt.g - Pt.b; + fDir[1] += f * f; + + f = Pt.r - Pt.g + Pt.b; + fDir[2] += f * f; + + f = Pt.r - Pt.g - Pt.b; + fDir[3] += f * f; + } + + float fDirMax = fDir[0]; + int iDirMax = 0; + + for (int iDir = 1; iDir < 4; iDir++) + { + if (fDir[iDir] > fDirMax) + { + fDirMax = fDir[iDir]; + iDirMax = iDir; + } + } + + if ((iDirMax & 2) > 0) + { + (X.g, Y.g) = (Y.g, X.g); + } + + if ((iDirMax & 1) > 0) + { + (X.b, Y.b) = (Y.b, X.b); + } + + if (fAB < 1.0f / 4096.0f) + { + pX.r = X.r; + pX.g = X.g; + pX.b = X.b; + pY.r = Y.r; + pY.g = Y.g; + pY.b = Y.b; + return; + } + + float fSteps = (float)(cSteps - 1); + for (int iIteration = 0; iIteration < 8; iIteration++) + { + HDRColorA[] pSteps = new HDRColorA[4]; + + for (int iStep = 0; iStep < cSteps; iStep++) + { + pSteps[iStep].r = X.r * pC[iStep] + Y.r * pD[iStep]; + pSteps[iStep].g = X.g * pC[iStep] + Y.g * pD[iStep]; + pSteps[iStep].b = X.b * pC[iStep] + Y.b * pD[iStep]; + } + + Dir.r = Y.r - X.r; + Dir.g = Y.g - X.g; + Dir.b = Y.b - X.b; + + float fLen = (Dir.r * Dir.r + Dir.g * Dir.g + Dir.b * Dir.b); + + if (fLen < (1.0f / 4096.0f)) + break; + + float fScale = fSteps / fLen; + + Dir.r *= fScale; + Dir.g *= fScale; + Dir.b *= fScale; + + float d2X, d2Y; + HDRColorA dX, dY; + d2X = d2Y = dX.r = dX.g = dX.b = dY.r = dY.g = dY.b = 0.0f; + + for (int iPoint = 0; iPoint < NUM_PIXELS_PER_BLOCK; iPoint++) + { + float fDot = (pPoints[iPoint].r - X.r) * Dir.r + + (pPoints[iPoint].g - X.g) * Dir.g + + (pPoints[iPoint].b - X.b) * Dir.b; + + int iStep; + if (fDot <= 0.0f) + iStep = 0; + if (fDot >= fSteps) + iStep = cSteps - 1; + else + iStep = (int)(fDot + 0.5f); + + + HDRColorA Diff; + Diff.r = pSteps[iStep].r - pPoints[iPoint].r; + Diff.g = pSteps[iStep].g - pPoints[iPoint].g; + Diff.b = pSteps[iStep].b - pPoints[iPoint].b; + + float fC = pC[iStep] * (1.0f / 8.0f); + float fD = pD[iStep] * (1.0f / 8.0f); + + d2X += fC * pC[iStep]; + dX.r += fC * Diff.r; + dX.g += fC * Diff.g; + dX.b += fC * Diff.b; + + d2Y += fD * pD[iStep]; + dY.r += fD * Diff.r; + dY.g += fD * Diff.g; + dY.b += fD * Diff.b; + } + + if (d2X > 0.0f) + { + float f = -1.0f / d2X; + + X.r += dX.r * f; + X.g += dX.g * f; + X.b += dX.b * f; + } + + if (d2Y > 0.0f) + { + float f = -1.0f / d2Y; + + Y.r += dY.r * f; + Y.g += dY.g * f; + Y.b += dY.b * f; + } + + if ((dX.r * dX.r < fEpsilon) && (dX.g * dX.g < fEpsilon) && (dX.b * dX.b < fEpsilon) && + (dY.r * dY.r < fEpsilon) && (dY.g * dY.g < fEpsilon) && (dY.b * dY.b < fEpsilon)) + { + break; + } + } + + pX.r = X.r; + pX.g = X.g; + pX.b = X.b; + pY.r = Y.r; + pY.g = Y.g; + pY.b = Y.b; + } + + + static void DecodeBC1(ref Span pColor, D3DX_BC1 pBC ) + { + Vector4 s_Scale = new + ( + 1.0f / 31.0f, 1.0f / 63.0f, 1.0f / 31.0f, 1.0f + ); + + Vector4 clr0 = XMLoadU565(new XMU565(pBC.rgb[0])); + Vector4 clr1 = XMLoadU565(new XMU565(pBC.rgb[1])); + + clr0 *= s_Scale; + clr1 *= s_Scale; + + clr0 = XMVectorSwizzle(clr0, 2, 1, 0, 3); + clr1 = XMVectorSwizzle(clr1, 2, 1, 0, 3); + + clr0.W = 1.0f; // clr0 = XMVectorSelect(g_XMIdentityR3, clr0, g_XMSelect1110); + clr1.W = 1.0f; // clr1 = XMVectorSelect(g_XMIdentityR3, clr1, g_XMSelect1110); + + Vector4 clr2, clr3; + if (pBC.rgb[0] <= pBC.rgb[1]) + { + clr2 = Vector4.Lerp(clr0, clr1, 0.5f); + clr3 = Vector4.Zero; + } + else + { + clr2 = Vector4.Lerp(clr0, clr1, 1.0f / 3.0f); + clr3 = Vector4.Lerp(clr0, clr1, 2.0f / 3.0f); + } + + uint dw = pBC.bitmap; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i, dw >>= 2) + { + switch (dw & 3) + { + case 0: pColor[i] = clr0; break; + case 1: pColor[i] = clr1; break; + case 2: pColor[i] = clr2; break; + + default: pColor[i] = clr3; break; + } + } + } + + static void EncodeBC1(ref D3DX_BC1 pBC, HDRColorA[] pColor, bool bColorKey, float alphaRef, uint flags) + { + int uSteps; + + if (bColorKey) + { + int uColorKey = 0; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + if (pColor[i].a < alphaRef) + uColorKey++; + } + + if (NUM_PIXELS_PER_BLOCK == uColorKey) + { + pBC.rgb[0] = 0x0000; + pBC.rgb[1] = 0xffff; + pBC.bitmap = 0xffffffff; + return; + } + + uSteps = (uColorKey > 0) ? 3 : 4; + } + else + { + uSteps = 4; + } + + HDRColorA[] Color = new HDRColorA[NUM_PIXELS_PER_BLOCK]; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + HDRColorA Clr; + Clr.r = pColor[i].r; + Clr.g = pColor[i].g; + Clr.b = pColor[i].b; + + Color[i].r = (float)(int)(Clr.r * 31.0f + 0.5f) * (1.0f / 31.0f); + Color[i].g = (float)(int)(Clr.g * 63.0f + 0.5f) * (1.0f / 63.0f); + Color[i].b = (float)(int)(Clr.b * 31.0f + 0.5f) * (1.0f / 31.0f); + + Color[i].a = 1.0f; + + Color[i].r *= g_Luminance.r; + Color[i].g *= g_Luminance.g; + Color[i].b *= g_Luminance.b; + } + + HDRColorA ColorA = new(0, 0, 0, 0); + HDRColorA ColorB = new(0, 0, 0, 0); + HDRColorA ColorC = new(0, 0, 0, 0); + HDRColorA ColorD = new(0, 0, 0, 0); + + OptimizeRGB(ref ColorA, ref ColorB, Color, uSteps, flags); + + ColorC.r = ColorA.r * g_LuminanceInv.r; + ColorC.g = ColorA.g * g_LuminanceInv.g; + ColorC.b = ColorA.b * g_LuminanceInv.b; + + ColorD.r = ColorB.r * g_LuminanceInv.r; + ColorD.g = ColorB.g * g_LuminanceInv.g; + ColorD.b = ColorB.b * g_LuminanceInv.b; + + ushort wColorA = Encode565(ColorC); + ushort wColorB = Encode565(ColorD); + + if ((uSteps == 4) && (wColorA == wColorB)) + { + pBC.rgb[0] = wColorA; + pBC.rgb[1] = wColorB; + pBC.bitmap = 0x00000000; + return; + } + + Decode565(ColorC, wColorA); + Decode565(ColorD, wColorB); + + ColorA.r = ColorC.r * g_Luminance.r; + ColorA.g = ColorC.g * g_Luminance.g; + ColorA.b = ColorC.b * g_Luminance.b; + + ColorB.r = ColorD.r * g_Luminance.r; + ColorB.g = ColorD.g * g_Luminance.g; + ColorB.b = ColorD.b * g_Luminance.b; + + HDRColorA[] Step = new HDRColorA[4]; + + if ((3 == uSteps) == (wColorA <= wColorB)) + { + pBC.rgb[0] = wColorA; + pBC.rgb[1] = wColorB; + + Step[0] = ColorA; + Step[1] = ColorB; + } + else + { + pBC.rgb[0] = wColorB; + pBC.rgb[1] = wColorA; + + Step[0] = ColorB; + Step[1] = ColorA; + } + + int[] pSteps3 = [0, 2, 1]; + int[] pSteps4 = [0, 2, 3, 1]; + int[] pSteps; + + if (3 == uSteps) + { + pSteps = pSteps3; + + HDRColorALerp(ref Step[2], Step[0], Step[1], 0.5f); + } + else + { + pSteps = pSteps4; + + HDRColorALerp(ref Step[2], Step[0], Step[1], 1.0f / 3.0f); + HDRColorALerp(ref Step[3], Step[0], Step[1], 2.0f / 3.0f); + } + + HDRColorA Dir; + Dir.r = Step[1].r - Step[0].r; + Dir.g = Step[1].g - Step[0].g; + Dir.b = Step[1].b - Step[0].b; + + float fSteps = (float)(uSteps - 1); + float fScale = (wColorA != wColorB) ? (fSteps / (Dir.r * Dir.r + Dir.g * Dir.g + Dir.b * Dir.b)) : 0.0f; + + Dir.r *= fScale; + Dir.g *= fScale; + Dir.b *= fScale; + + uint dw = 0; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + if ((3 == uSteps) && (pColor[i].a < alphaRef)) + { + dw = (uint)((3 << 30) | (dw >> 2)); + } + else + { + HDRColorA Clr = new (0,0,0,0); + + Clr.r = pColor[i].r * g_Luminance.r; + Clr.g = pColor[i].g * g_Luminance.g; + Clr.b = pColor[i].b * g_Luminance.b; + + float fDot = (Clr.r - Step[0].r) * Dir.r + (Clr.g - Step[0].g) * Dir.g + + (Clr.b - Step[0].b) * Dir.b; + uint iStep; + + if (fDot <= 0.0f) + iStep = 0; + else if (fDot >= fSteps) + iStep = 1; + else + iStep = (uint)(pSteps[(int)(fDot + 0.5f)]); + + dw = (iStep << 30) | (dw >> 2); + } + } + + pBC.bitmap = dw; + } + + //===================================================================================== + // Entry points + //===================================================================================== + + public static void D3DXDecodeBC1(Span pColor, D3DX_BC1 pBC) + { + DecodeBC1(ref pColor, pBC); + } + + public static void D3DXEncodeBC1(ref D3DX_BC1 pBC, Span pColor, float alphaRef, uint flags) + { + HDRColorA[] Color = new HDRColorA[NUM_PIXELS_PER_BLOCK]; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + Color[i] = new(pColor[i].X, pColor[i].Y, pColor[i].Z, pColor[i].W); + } + + EncodeBC1(ref pBC, Color, true, alphaRef, flags); + } + + + public static void D3DXDecodeBC2(Span pColor, D3DX_BC2 pBC2) + { + DecodeBC1(ref pColor, pBC2.bc1); + + uint dw = pBC2.bitmap[0]; + + for (int i = 0; i < 8; ++i, dw >>= 4) + pColor[i].W = (float)(dw & 0xf) * (1.0f / 15.0f); + + dw = pBC2.bitmap[1]; + + for (int i = 8; i < NUM_PIXELS_PER_BLOCK; ++i, dw >>= 4) + pColor[i].W = (float)(dw & 0xf) * (1.0f / 15.0f); + } + + public static void D3DXEncodeBC2(ref D3DX_BC2 pBC, Span pColor, uint flags) + { + HDRColorA[] Color = new HDRColorA[NUM_PIXELS_PER_BLOCK]; + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + Color[i] = new(pColor[i].X, pColor[i].Y, pColor[i].Z, pColor[i].W); + } + + pBC.bitmap[0] = 0; + pBC.bitmap[1] = 0; + + float[] fError = new float[NUM_PIXELS_PER_BLOCK]; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + float fAlph = Color[i].a; + + uint u = (uint)(int)(fAlph * 15.0f + 0.5f); + + pBC.bitmap[i >> 3] >>= 4; + pBC.bitmap[i >> 3] |= (u << 28); + } + + EncodeBC1(ref pBC.bc1, Color, false, 0.0f, flags); + } + + public static void D3DXDecodeBC3(Span pColor, D3DX_BC3 pBC3) + { + DecodeBC1(ref pColor, pBC3.bc1); + + float[] fAlpha = new float[8]; + + fAlpha[0] = ((float)pBC3.alpha[0]) * (1.0f / 255.0f); + fAlpha[1] = ((float)pBC3.alpha[1]) * (1.0f / 255.0f); + + if (pBC3.alpha[0] > pBC3.alpha[1]) + { + for (int i = 1; i < 7; ++i) + fAlpha[i + 1] = (fAlpha[0] * (7 - i) + fAlpha[1] * i) * (1.0f / 7.0f); + } + else + { + for (int i = 1; i < 5; ++i) + fAlpha[i + 1] = (fAlpha[0] * (5 - i) + fAlpha[1] * i) * (1.0f / 5.0f); + + fAlpha[6] = 0.0f; + fAlpha[7] = 1.0f; + } + + uint dw = pBC3.bitmap[0] | (uint)(pBC3.bitmap[1] << 8) | (uint)(pBC3.bitmap[2] << 16); + + for (int i = 0; i < 8; ++i, dw >>= 3) + pColor[i].W = fAlpha[dw & 0x7]; + + dw = pBC3.bitmap[3] | (uint)(pBC3.bitmap[4] << 8) | (uint)(pBC3.bitmap[5] << 16); + + for (int i = 8; i < NUM_PIXELS_PER_BLOCK; ++i, dw >>= 3) + pColor[i].W = fAlpha[dw & 0x7]; + } + + public static void D3DXEncodeBC3(ref D3DX_BC3 pBC, Span pColor, uint flags) + { + HDRColorA[] Color = new HDRColorA[NUM_PIXELS_PER_BLOCK]; + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + Color[i] = new(pColor[i].X, pColor[i].Y, pColor[i].Z, pColor[i].W); + } + + + float[] fAlpha = new float[NUM_PIXELS_PER_BLOCK]; + float[] fError = new float[NUM_PIXELS_PER_BLOCK]; + + float fMinAlpha = Color[0].a; + float fMaxAlpha = Color[0].a; + + for (int i = 0; i < NUM_PIXELS_PER_BLOCK; ++i) + { + float fAlph = Color[i].a; + + fAlpha[i] = (int)(fAlph * 255.0f + 0.5f) * (1.0f / 255.0f); + + if (fAlpha[i] < fMinAlpha) + fMinAlpha = fAlpha[i]; + else if (fAlpha[i] > fMaxAlpha) + fMaxAlpha = fAlpha[i]; + } + + EncodeBC1(ref pBC.bc1, Color, false, 0.0f, flags); + + if (1.0f == fMinAlpha) + { + pBC.alpha[0] = 0xff; + pBC.alpha[1] = 0xff; + Array.Clear(pBC.bitmap, 0, 6); + return; + } + + uint uSteps = (uint)(((0.0f == fMinAlpha) || (1.0f == fMaxAlpha)) ? 6 : 8); + + float fAlphaA = 0.0f, fAlphaB = 0.0f; + OptimizeAlpha(false, ref fAlphaA, ref fAlphaB, fAlpha, uSteps); + + byte bAlphaA = (byte)(int)(fAlphaA * 255.0f + 0.5f); + byte bAlphaB = (byte)(int)(fAlphaB * 255.0f + 0.5f); + + fAlphaA = (float)bAlphaA * (1.0f / 255.0f); + fAlphaB = (float)bAlphaB * (1.0f / 255.0f); + + if ((8 == uSteps) && (bAlphaA == bAlphaB)) + { + pBC.alpha[0] = bAlphaA; + pBC.alpha[1] = bAlphaB; + Array.Clear(pBC.bitmap, 0, 6); + return; + } + + int[] pSteps6 = [0, 2, 3, 4, 5, 1]; + int[] pSteps8 = [0, 2, 3, 4, 5, 6, 7, 1]; + + int[] pSteps; + float[] fStep = new float[8]; + + if (6 == uSteps) + { + pBC.alpha[0] = bAlphaA; + pBC.alpha[1] = bAlphaB; + + fStep[0] = fAlphaA; + fStep[1] = fAlphaB; + + for (int i = 1; i < 5; ++i) + fStep[i + 1] = (fStep[0] * (5 - i) + fStep[1] * i) * (1.0f / 5.0f); + + fStep[6] = 0.0f; + fStep[7] = 1.0f; + + pSteps = pSteps6; + } + else + { + pBC.alpha[0] = bAlphaB; + pBC.alpha[1] = bAlphaA; + + fStep[0] = fAlphaB; + fStep[1] = fAlphaA; + + for (int i = 1; i < 7; ++i) + fStep[i + 1] = (fStep[0] * (7 - i) + fStep[1] * i) * (1.0f / 7.0f); + + pSteps = pSteps8; + } + + float fSteps = (float)(uSteps - 1); + float fScale = (fStep[0] != fStep[1]) ? (fSteps / (fStep[1] - fStep[0])) : 0.0f; + + for (int iSet = 0; iSet < 2; iSet++) + { + uint dw = 0; + + int iMin = iSet * 8; + int iLim = iMin + 8; + + for (int i = iMin; i < iLim; ++i) + { + float fAlph = Color[i].a; + float fDot = (fAlph - fStep[0]) * fScale; + + uint iStep; + if (fDot <= 0.0f) + iStep = (uint)(((6 == uSteps) && (fAlph <= fStep[0] * 0.5f)) ? 6 : 0); + else if (fDot >= fSteps) + iStep = (uint)(((6 == uSteps) && (fAlph >= (fStep[1] + 1.0f) * 0.5f)) ? 7 : 1); + else + iStep = (uint)(pSteps[(int)(fDot + 0.5f)]); + + dw = (iStep << 21) | (dw >> 3); + } + + pBC.bitmap[0 + iSet * 3] = (byte)(dw & 0xFF); + pBC.bitmap[1 + iSet * 3] = (byte)((dw >> 8) & 0xFF); + pBC.bitmap[2 + iSet * 3] = (byte)((dw >> 16) & 0xFF); + } + } + + //===================================================================================== + // custom BC block readers and writers for convenience + //===================================================================================== + + public static D3DX_BC1 ReadBC1(BinaryReader reader) + { + return new D3DX_BC1() + { + rgb = [reader.ReadUInt16(), reader.ReadUInt16()], + bitmap = reader.ReadUInt32() + }; + } + + public static D3DX_BC2 ReadBC2(BinaryReader reader) + { + return new D3DX_BC2() + { + bitmap = [reader.ReadUInt32(), reader.ReadUInt32()], + bc1 = ReadBC1(reader) + }; + } + + public static D3DX_BC3 ReadBC3(BinaryReader reader) + { + return new D3DX_BC3() + { + alpha = reader.ReadBytes(2), + bitmap = reader.ReadBytes(6), + bc1 = ReadBC1(reader) + }; + } + + public static void WriteBC1(BinaryWriter writer, D3DX_BC1 value) + { + writer.Write(value.rgb[0]); + writer.Write(value.rgb[1]); + writer.Write(value.bitmap); + } + + public static void WriteBC2(BinaryWriter writer, D3DX_BC2 value) + { + writer.Write(value.bitmap[0]); + writer.Write(value.bitmap[1]); + WriteBC1(writer, value.bc1); + } + + public static void WriteBC3(BinaryWriter writer, D3DX_BC3 value) + { + writer.Write(value.alpha[0]); + writer.Write(value.alpha[1]); + writer.Write(value.bitmap[0]); + writer.Write(value.bitmap[1]); + writer.Write(value.bitmap[2]); + writer.Write(value.bitmap[3]); + writer.Write(value.bitmap[4]); + writer.Write(value.bitmap[5]); + WriteBC1(writer, value.bc1); + } + } +} \ No newline at end of file diff --git a/Core/Helpers/DirectXTex/DirectXTexCompress.cs b/Core/Helpers/DirectXTex/DirectXTexCompress.cs new file mode 100644 index 0000000..8de652a --- /dev/null +++ b/Core/Helpers/DirectXTex/DirectXTexCompress.cs @@ -0,0 +1,151 @@ +using System.Numerics; + +namespace DirectX +{ + internal static class DirectXTexCompress + { + public static byte[] DecompressDxt1(byte[] imageData, int width, int height) + { + return DecompressDxtBlocks(imageData, width, height, (reader, pixels) => + Bc.D3DXDecodeBC1(pixels.AsSpan(), Bc.ReadBC1(reader)) + ); + } + + public static byte[] CompressDxt1(byte[] imageData, int width, int height) + { + var bc = new Bc.D3DX_BC1(); + return CompressDxtBlocks(imageData, width, height, (writer, pixels) => + { + Bc.D3DXEncodeBC1(ref bc, pixels.AsSpan(), 0.5f, 0); + Bc.WriteBC1(writer, bc); + }); + } + + public static byte[] DecompressDxt3(byte[] imageData, int width, int height) + { + return DecompressDxtBlocks(imageData, width, height, (reader, pixels) => + Bc.D3DXDecodeBC2(pixels.AsSpan(), Bc.ReadBC2(reader)) + ); + } + + public static byte[] CompressDxt3(byte[] imageData, int width, int height) + { + var bc = new Bc.D3DX_BC2(); + return CompressDxtBlocks(imageData, width, height, (writer, pixels) => + { + Bc.D3DXEncodeBC2(ref bc, pixels.AsSpan(), 0); + Bc.WriteBC2(writer, bc); + }); + } + + public static byte[] DecompressDxt5(byte[] imageData, int width, int height) + { + return DecompressDxtBlocks(imageData, width, height, (reader, pixels) => + Bc.D3DXDecodeBC3(pixels.AsSpan(), Bc.ReadBC3(reader)) + ); + } + + public static byte[] CompressDxt5(byte[] imageData, int width, int height) + { + var bc = new Bc.D3DX_BC3(); + return CompressDxtBlocks(imageData, width, height, (writer, pixels) => + { + Bc.D3DXEncodeBC3(ref bc, pixels.AsSpan(), 0); + Bc.WriteBC3(writer, bc); + }); + } + + private static byte[] DecompressDxtBlocks( + byte[] imageData, int width, int height, Action decodeMethod) + { + int blocksX = (width + 3) / 4; + int blocksY = (height + 3) / 4; + var output = new byte[width * height * 4]; + var pixels = new Vector4[16]; + + using var reader = new BinaryReader(new MemoryStream(imageData)); + + for (int by = 0; by < blocksY; by++) + { + for (int bx = 0; bx < blocksX; bx++) + { + decodeMethod.Invoke(reader, pixels); + WriteBlock(output, pixels, bx * 4, by * 4, width, height); + } + } + + return output; + } + + private static byte[] CompressDxtBlocks( + byte[] imageData, int width, int height, Action encodeMethod) + { + int blocksX = (width + 3) / 4; + int blocksY = (height + 3) / 4; + using var output = new MemoryStream(); + using var writer = new BinaryWriter(output); + var pixels = new Vector4[16]; + + for (int by = 0; by < blocksY; by++) + { + for (int bx = 0; bx < blocksX; bx++) + { + ReadBlock(imageData, pixels, bx * 4, by * 4, width, height); + encodeMethod.Invoke(writer, pixels); + } + } + + return output.ToArray(); + } + + private static void ReadBlock(byte[] imageData, Vector4[] pixels, int startX, int startY, int width, int height) + { + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + int px = startX + x; + int py = startY + y; + int i = y * 4 + x; + + if (px < width && py < height) + { + int src = (py * width + px) * 4; + pixels[i] = new Vector4( + imageData[src + 0] / 255f, + imageData[src + 1] / 255f, + imageData[src + 2] / 255f, + imageData[src + 3] / 255f + ); + } + else + { + pixels[i] = Vector4.Zero; + } + } + } + } + + private static void WriteBlock(byte[] output, Vector4[] pixels, int startX, int startY, int width, int height) + { + for (int y = 0; y < 4; y++) + { + for (int x = 0; x < 4; x++) + { + int px = startX + x; + int py = startY + y; + + if (px >= width || py >= height) + continue; + + int dst = (py * width + px) * 4; + var p = pixels[y * 4 + x]; + output[dst + 0] = (byte)(int)(Math.Min(Math.Max(p.X, 0f), 1f) * 255f + 0.5f); + output[dst + 1] = (byte)(int)(Math.Min(Math.Max(p.Y, 0f), 1f) * 255f + 0.5f); + output[dst + 2] = (byte)(int)(Math.Min(Math.Max(p.Z, 0f), 1f) * 255f + 0.5f); + output[dst + 3] = (byte)(int)(Math.Min(Math.Max(p.W, 0f), 1f) * 255f + 0.5f); + } + } + } + } +} diff --git a/Core/Helpers/DxtUtil.cs b/Core/Helpers/DxtUtil.cs deleted file mode 100644 index a2fb546..0000000 --- a/Core/Helpers/DxtUtil.cs +++ /dev/null @@ -1,443 +0,0 @@ -// #region License -// /* -// Microsoft Public License (Ms-PL) -// MonoGame - Copyright © 2009 The MonoGame Team -// -// All rights reserved. -// -// This license governs use of the accompanying software. If you use the software, you accept this license. If you do not -// accept the license, do not use the software. -// -// 1. Definitions -// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under -// U.S. copyright law. -// -// A "contribution" is the original software, or any additions or changes to the software. -// A "contributor" is any person that distributes its contribution under this license. -// "Licensed patents" are a contributor's patent claims that read directly on its contribution. -// -// 2. Grant of Rights -// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, -// each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. -// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, -// each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. -// -// 3. Conditions and Limitations -// (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. -// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, -// your patent license from such contributor to the software ends automatically. -// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution -// notices that are present in the software. -// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including -// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object -// code form, you may only do so under a license that complies with this license. -// (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees -// or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent -// permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular -// purpose and non-infringement. -// */ -// #endregion License -// -using System; -using System.IO; - -namespace Microsoft.Xna.Framework.Graphics -{ - internal static class DxtUtil - { - internal static byte[] DecompressDxt1(byte[] imageData, int width, int height) - { - using (MemoryStream imageStream = new MemoryStream(imageData)) - return DecompressDxt1(imageStream, width, height); - } - - internal static byte[] DecompressDxt1(Stream imageStream, int width, int height) - { - byte[] imageData = new byte[width * height * 4]; - - using (BinaryReader imageReader = new BinaryReader(imageStream)) - { - int blockCountX = (width + 3) / 4; - int blockCountY = (height + 3) / 4; - - for (int y = 0; y < blockCountY; y++) - { - for (int x = 0; x < blockCountX; x++) - { - DecompressDxt1Block(imageReader, x, y, blockCountX, width, height, imageData); - } - } - } - - return imageData; - } - - private static void DecompressDxt1Block(BinaryReader imageReader, int x, int y, int blockCountX, int width, int height, byte[] imageData) - { - ushort c0 = imageReader.ReadUInt16(); - ushort c1 = imageReader.ReadUInt16(); - - byte r0, g0, b0; - byte r1, g1, b1; - ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); - ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); - - uint lookupTable = imageReader.ReadUInt32(); - - for (int blockY = 0; blockY < 4; blockY++) - { - for (int blockX = 0; blockX < 4; blockX++) - { - byte r = 0, g = 0, b = 0, a = 255; - uint index = (lookupTable >> 2 * (4 * blockY + blockX)) & 0x03; - - if (c0 > c1) - { - switch (index) - { - case 0: - r = r0; - g = g0; - b = b0; - break; - case 1: - r = r1; - g = g1; - b = b1; - break; - case 2: - r = (byte)((2 * r0 + r1) / 3); - g = (byte)((2 * g0 + g1) / 3); - b = (byte)((2 * b0 + b1) / 3); - break; - case 3: - r = (byte)((r0 + 2 * r1) / 3); - g = (byte)((g0 + 2 * g1) / 3); - b = (byte)((b0 + 2 * b1) / 3); - break; - } - } - else - { - switch (index) - { - case 0: - r = r0; - g = g0; - b = b0; - break; - case 1: - r = r1; - g = g1; - b = b1; - break; - case 2: - r = (byte)((r0 + r1) / 2); - g = (byte)((g0 + g1) / 2); - b = (byte)((b0 + b1) / 2); - break; - case 3: - r = 0; - g = 0; - b = 0; - a = 0; - break; - } - } - - int px = (x << 2) + blockX; - int py = (y << 2) + blockY; - if ((px < width) && (py < height)) - { - int offset = ((py * width) + px) << 2; - imageData[offset] = r; - imageData[offset + 1] = g; - imageData[offset + 2] = b; - imageData[offset + 3] = a; - } - } - } - } - - internal static byte[] DecompressDxt3(byte[] imageData, int width, int height) - { - using (MemoryStream imageStream = new MemoryStream(imageData)) - return DecompressDxt3(imageStream, width, height); - } - - internal static byte[] DecompressDxt3(Stream imageStream, int width, int height) - { - byte[] imageData = new byte[width * height * 4]; - - using (BinaryReader imageReader = new BinaryReader(imageStream)) - { - int blockCountX = (width + 3) / 4; - int blockCountY = (height + 3) / 4; - - for (int y = 0; y < blockCountY; y++) - { - for (int x = 0; x < blockCountX; x++) - { - DecompressDxt3Block(imageReader, x, y, blockCountX, width, height, imageData); - } - } - } - - return imageData; - } - - private static void DecompressDxt3Block(BinaryReader imageReader, int x, int y, int blockCountX, int width, int height, byte[] imageData) - { - byte a0 = imageReader.ReadByte(); - byte a1 = imageReader.ReadByte(); - byte a2 = imageReader.ReadByte(); - byte a3 = imageReader.ReadByte(); - byte a4 = imageReader.ReadByte(); - byte a5 = imageReader.ReadByte(); - byte a6 = imageReader.ReadByte(); - byte a7 = imageReader.ReadByte(); - - ushort c0 = imageReader.ReadUInt16(); - ushort c1 = imageReader.ReadUInt16(); - - byte r0, g0, b0; - byte r1, g1, b1; - ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); - ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); - - uint lookupTable = imageReader.ReadUInt32(); - - int alphaIndex = 0; - for (int blockY = 0; blockY < 4; blockY++) - { - for (int blockX = 0; blockX < 4; blockX++) - { - byte r = 0, g = 0, b = 0, a = 0; - - uint index = (lookupTable >> 2 * (4 * blockY + blockX)) & 0x03; - - switch (alphaIndex) - { - case 0: - a = (byte)((a0 & 0x0F) | ((a0 & 0x0F) << 4)); - break; - case 1: - a = (byte)((a0 & 0xF0) | ((a0 & 0xF0) >> 4)); - break; - case 2: - a = (byte)((a1 & 0x0F) | ((a1 & 0x0F) << 4)); - break; - case 3: - a = (byte)((a1 & 0xF0) | ((a1 & 0xF0) >> 4)); - break; - case 4: - a = (byte)((a2 & 0x0F) | ((a2 & 0x0F) << 4)); - break; - case 5: - a = (byte)((a2 & 0xF0) | ((a2 & 0xF0) >> 4)); - break; - case 6: - a = (byte)((a3 & 0x0F) | ((a3 & 0x0F) << 4)); - break; - case 7: - a = (byte)((a3 & 0xF0) | ((a3 & 0xF0) >> 4)); - break; - case 8: - a = (byte)((a4 & 0x0F) | ((a4 & 0x0F) << 4)); - break; - case 9: - a = (byte)((a4 & 0xF0) | ((a4 & 0xF0) >> 4)); - break; - case 10: - a = (byte)((a5 & 0x0F) | ((a5 & 0x0F) << 4)); - break; - case 11: - a = (byte)((a5 & 0xF0) | ((a5 & 0xF0) >> 4)); - break; - case 12: - a = (byte)((a6 & 0x0F) | ((a6 & 0x0F) << 4)); - break; - case 13: - a = (byte)((a6 & 0xF0) | ((a6 & 0xF0) >> 4)); - break; - case 14: - a = (byte)((a7 & 0x0F) | ((a7 & 0x0F) << 4)); - break; - case 15: - a = (byte)((a7 & 0xF0) | ((a7 & 0xF0) >> 4)); - break; - } - ++alphaIndex; - - switch (index) - { - case 0: - r = r0; - g = g0; - b = b0; - break; - case 1: - r = r1; - g = g1; - b = b1; - break; - case 2: - r = (byte)((2 * r0 + r1) / 3); - g = (byte)((2 * g0 + g1) / 3); - b = (byte)((2 * b0 + b1) / 3); - break; - case 3: - r = (byte)((r0 + 2 * r1) / 3); - g = (byte)((g0 + 2 * g1) / 3); - b = (byte)((b0 + 2 * b1) / 3); - break; - } - - int px = (x << 2) + blockX; - int py = (y << 2) + blockY; - if ((px < width) && (py < height)) - { - int offset = ((py * width) + px) << 2; - imageData[offset] = r; - imageData[offset + 1] = g; - imageData[offset + 2] = b; - imageData[offset + 3] = a; - } - } - } - } - - internal static byte[] DecompressDxt5(byte[] imageData, int width, int height) - { - using (MemoryStream imageStream = new MemoryStream(imageData)) - return DecompressDxt5(imageStream, width, height); - } - - internal static byte[] DecompressDxt5(Stream imageStream, int width, int height) - { - byte[] imageData = new byte[width * height * 4]; - - using (BinaryReader imageReader = new BinaryReader(imageStream)) - { - int blockCountX = (width + 3) / 4; - int blockCountY = (height + 3) / 4; - - for (int y = 0; y < blockCountY; y++) - { - for (int x = 0; x < blockCountX; x++) - { - DecompressDxt5Block(imageReader, x, y, blockCountX, width, height, imageData); - } - } - } - - return imageData; - } - - private static void DecompressDxt5Block(BinaryReader imageReader, int x, int y, int blockCountX, int width, int height, byte[] imageData) - { - byte alpha0 = imageReader.ReadByte(); - byte alpha1 = imageReader.ReadByte(); - - ulong alphaMask = (ulong)imageReader.ReadByte(); - alphaMask += (ulong)imageReader.ReadByte() << 8; - alphaMask += (ulong)imageReader.ReadByte() << 16; - alphaMask += (ulong)imageReader.ReadByte() << 24; - alphaMask += (ulong)imageReader.ReadByte() << 32; - alphaMask += (ulong)imageReader.ReadByte() << 40; - - ushort c0 = imageReader.ReadUInt16(); - ushort c1 = imageReader.ReadUInt16(); - - byte r0, g0, b0; - byte r1, g1, b1; - ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); - ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); - - uint lookupTable = imageReader.ReadUInt32(); - - for (int blockY = 0; blockY < 4; blockY++) - { - for (int blockX = 0; blockX < 4; blockX++) - { - byte r = 0, g = 0, b = 0, a = 255; - uint index = (lookupTable >> 2 * (4 * blockY + blockX)) & 0x03; - - uint alphaIndex = (uint)((alphaMask >> 3 * (4 * blockY + blockX)) & 0x07); - if (alphaIndex == 0) - { - a = alpha0; - } - else if (alphaIndex == 1) - { - a = alpha1; - } - else if (alpha0 > alpha1) - { - a = (byte)(((8 - alphaIndex) * alpha0 + (alphaIndex - 1) * alpha1) / 7); - } - else if (alphaIndex == 6) - { - a = 0; - } - else if (alphaIndex == 7) - { - a = 0xff; - } - else - { - a = (byte)(((6 - alphaIndex) * alpha0 + (alphaIndex - 1) * alpha1) / 5); - } - - switch (index) - { - case 0: - r = r0; - g = g0; - b = b0; - break; - case 1: - r = r1; - g = g1; - b = b1; - break; - case 2: - r = (byte)((2 * r0 + r1) / 3); - g = (byte)((2 * g0 + g1) / 3); - b = (byte)((2 * b0 + b1) / 3); - break; - case 3: - r = (byte)((r0 + 2 * r1) / 3); - g = (byte)((g0 + 2 * g1) / 3); - b = (byte)((b0 + 2 * b1) / 3); - break; - } - - int px = (x << 2) + blockX; - int py = (y << 2) + blockY; - if ((px < width) && (py < height)) - { - int offset = ((py * width) + px) << 2; - imageData[offset] = r; - imageData[offset + 1] = g; - imageData[offset + 2] = b; - imageData[offset + 3] = a; - } - } - } - } - - private static void ConvertRgb565ToRgb888(ushort color, out byte r, out byte g, out byte b) - { - int temp; - - temp = (color >> 11) * 255 + 16; - r = (byte)((temp / 32 + temp) / 32); - temp = ((color & 0x07E0) >> 5) * 255 + 32; - g = (byte)((temp / 64 + temp) / 64); - temp = (color & 0x001F) * 255 + 16; - b = (byte)((temp / 32 + temp) / 32); - } - } -} - diff --git a/Core/Helpers/FezGeometryUtil.cs b/Core/Helpers/FezGeometryUtil.cs index 3e38104..f1697f6 100644 --- a/Core/Helpers/FezGeometryUtil.cs +++ b/Core/Helpers/FezGeometryUtil.cs @@ -8,26 +8,33 @@ internal static class FezGeometryUtil { /// Recalculates texture coordinates to match what game does with them during loading process. public static void RecalculateCubemapTexCoords( - IndexedPrimitives geometry, Vector3 geometryBounds + IndexedPrimitives geometry, Vector3 geometryBounds, bool widen ) { + // keeping arithmetics vaguely similar to the one in game to prevent number mismatch after conversion foreach (var vertex in geometry.Vertices) { - (int textureOffset, Vector3 xAxis, Vector3 yAxis) = vertex.NormalByte switch + (float textureOffset, Vector3 xAxis, Vector3 yAxis) = vertex.NormalByte switch { - 5 => (0, new Vector3(1, 0, 0), new Vector3(0, -1, 0)), // front - 3 => (1, new Vector3(0, 0, -1), new Vector3(0, -1, 0)), // right - 2 => (2, new Vector3(-1, 0, 0), new Vector3(0, -1, 0)), // back - 0 => (3, new Vector3(0, 0, 1), new Vector3(0, -1, 0)), // left - 4 => (4, new Vector3(1, 0, 0), new Vector3(0, 0, 1)), // top - 1 => (5, new Vector3(1, 0, 0), new Vector3(0, 0, -1)), // bottom + 5 => (0, new Vector3(1, 0, 0), new Vector3(0, 1, 0)), // front + 3 => (0.25f, new Vector3(0, 0, -1), new Vector3(0, 1, 0)), // right + 2 => (0.375f, new Vector3(-1, 0, 0), new Vector3(0, 1, 0)), // back + 0 => (0.375f, new Vector3(0, 0, 1), new Vector3(0, 1, 0)), // left + 4 => (0.5f, new Vector3(1, 0, 0), new Vector3(0, 0, 1)), // top + 1 => (0.625f, new Vector3(1, 0, 0), new Vector3(0, 0, 1)), // bottom _ => (0, new Vector3(), new Vector3()) }; - var texturePlanePosition = vertex.Position / geometryBounds; - vertex.TextureCoordinate = new Vector2( - (Vector3.Dot(texturePlanePosition, xAxis) + 0.5f + textureOffset) / 6.0f, - Vector3.Dot(texturePlanePosition, yAxis) + 0.5f - ); + var texturePlanePosition = (Vector3.One - vertex.Normal) * (vertex.Position / geometryBounds) * 2f + vertex.Normal; + var texturePlanePositionNormalized = texturePlanePosition / 2f + new Vector3(0.5f, 0.5f, 0.5f); + float uCoordinate = Vector3.Dot(xAxis, texturePlanePositionNormalized); + float vCoordinate = Vector3.Dot(yAxis, texturePlanePositionNormalized); + if (vertex.NormalByte != 4) + { + vCoordinate = 1.0f - vCoordinate; + } + + float finalUCoordinate = (textureOffset + uCoordinate / 8f) * (widen ? 1.3333334f : 1.0f); + vertex.TextureCoordinate = new Vector2(finalUCoordinate, vCoordinate); } } diff --git a/Core/Helpers/Json/ConfiguredJsonSerializer.cs b/Core/Helpers/Json/ConfiguredJsonSerializer.cs index 00762a1..f7651dc 100644 --- a/Core/Helpers/Json/ConfiguredJsonSerializer.cs +++ b/Core/Helpers/Json/ConfiguredJsonSerializer.cs @@ -61,25 +61,35 @@ public static T DeserializeFromFileBundle(FileBundle bundle) private static JsonSerializerOptions GetOptions() { - if (_serializerOptions == null) _serializerOptions = new() + if (_serializerOptions == null) { - IncludeFields = true, - WriteIndented = true, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - Converters = { - new JsonStringEnumConverter(), - new Vector2JsonConverter(), - new Vector3JsonConverter(), - new QuaternionJsonConverter(), - new ColorJsonConverter(), - new TimeSpanJsonConverter(), - new TrileEmplacementJsonConverter(), - new TrileEmplacementListJsonConverter(), - new ScriptTriggerJsonConverter(), - new ScriptConditionJsonConverter(), - new ScriptActionJsonConverter(), + _serializerOptions = new() + { + IncludeFields = true, + WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = { + new RoundTripFloatConverter(), + new JsonStringEnumConverter(), + new Vector2JsonConverter(), + new Vector3JsonConverter(), + new QuaternionJsonConverter(), + new ColorJsonConverter(), + new TimeSpanJsonConverter(), + new TrileEmplacementJsonConverter(), + new TrileEmplacementListJsonConverter(), + new ScriptTriggerJsonConverter(), + new ScriptConditionJsonConverter(), + new ScriptActionJsonConverter(), + new OrderedDictionaryConverterFactory(), + } + }; + + foreach (var context in JsonContexts.List) + { + _serializerOptions.TypeInfoResolverChain.Add(context); } - }; + } return _serializerOptions; } diff --git a/Core/Helpers/Json/CustomConverters/OrderedDictionaryConverter.cs b/Core/Helpers/Json/CustomConverters/OrderedDictionaryConverter.cs new file mode 100644 index 0000000..91fee49 --- /dev/null +++ b/Core/Helpers/Json/CustomConverters/OrderedDictionaryConverter.cs @@ -0,0 +1,76 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FEZRepacker.Core.Helpers.Json.CustomConverters +{ + public class OrderedDictionaryConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + if (!typeToConvert.IsGenericType) + { + return false; + } + + return typeToConvert.GetGenericTypeDefinition() == typeof(IDictionary<,>); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type[] typeArgs = typeToConvert.GetGenericArguments(); + var keyType = typeArgs[0]; + var valueType = typeArgs[1]; + var converterType = typeof(OrderedDictionaryConverter<,>).MakeGenericType(keyType, valueType); + + return (JsonConverter)Activator.CreateInstance(converterType)!; + } + } + + public class OrderedDictionaryConverter + : JsonConverter> where TKey : notnull + { + public override IDictionary Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); + + var keyConverter = TypeDescriptor.GetConverter(typeof(TKey)); + if (!keyConverter.CanConvertFrom(typeof(string))) throw new JsonException(); + + var result = new OrderedDictionary(); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) return result; + + if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException(); + + TKey key = (TKey)keyConverter.ConvertFromInvariantString(reader.GetString()!)!; + + reader.Read(); + + TValue value = JsonSerializer.Deserialize(ref reader, options)!; + + result.Add(key, value); + } + + throw new JsonException(); + } + + public override void Write( + Utf8JsonWriter writer, + IDictionary value, + JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach (var kvp in value) + { + writer.WritePropertyName(JsonSerializer.Serialize(kvp.Key, options).Trim('"')); + JsonSerializer.Serialize(writer, kvp.Value, options); + } + writer.WriteEndObject(); + } + } +} diff --git a/Core/Helpers/Json/CustomConverters/RoundTripFloatConverter.cs b/Core/Helpers/Json/CustomConverters/RoundTripFloatConverter.cs new file mode 100644 index 0000000..0d98892 --- /dev/null +++ b/Core/Helpers/Json/CustomConverters/RoundTripFloatConverter.cs @@ -0,0 +1,15 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FEZRepacker.Core.Helpers.Json.CustomConverters +{ + public class RoundTripFloatConverter : JsonConverter + { + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetSingle(); + + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) + => writer.WriteRawValue(value.ToString("R", CultureInfo.InvariantCulture)); + } +} \ No newline at end of file diff --git a/Core/Helpers/Json/CustomConverters/ScriptPropertiesJsonConverters.cs b/Core/Helpers/Json/CustomConverters/ScriptPropertiesJsonConverters.cs index 868129c..3b1ae2d 100644 --- a/Core/Helpers/Json/CustomConverters/ScriptPropertiesJsonConverters.cs +++ b/Core/Helpers/Json/CustomConverters/ScriptPropertiesJsonConverters.cs @@ -151,7 +151,8 @@ public override ScriptAction Read( action.Arguments = beforeFuncSplit[1].Split(','); for (int i = 0; i < action.Arguments.Length; i++) { - action.Arguments[i] = action.Arguments[i].Trim(); + var arg = action.Arguments[i].Trim(); + action.Arguments[i] = arg == "null" ? "" : arg; } } @@ -170,7 +171,8 @@ public override void Write( for (int i = 0; i < value.Arguments.Count(); i++) { if (i > 0) output += ", "; - output += value.Arguments[i]; + var arg = value.Arguments[i]; + output += string.IsNullOrEmpty(arg) ? "null" : arg; } output += ")"; if (value.Blocking) output = "#" + output; diff --git a/Core/Helpers/Json/CustomConverters/TimeSpanJsonConverter.cs b/Core/Helpers/Json/CustomConverters/TimeSpanJsonConverter.cs index 555d51c..94ee538 100644 --- a/Core/Helpers/Json/CustomConverters/TimeSpanJsonConverter.cs +++ b/Core/Helpers/Json/CustomConverters/TimeSpanJsonConverter.cs @@ -10,7 +10,7 @@ public override TimeSpan Read( Type typeToConvert, JsonSerializerOptions options) { - return TimeSpan.FromSeconds(reader.GetDouble()); + return TimeSpan.FromTicks((long)Math.Round(reader.GetDecimal() * TimeSpan.TicksPerSecond)); } public override void Write( @@ -18,7 +18,7 @@ public override void Write( TimeSpan timespan, JsonSerializerOptions options) { - writer.WriteNumberValue((decimal)timespan.TotalSeconds); + writer.WriteNumberValue((decimal)timespan.Ticks / TimeSpan.TicksPerSecond); } } } diff --git a/Core/Helpers/Json/CustomConverters/TrileEmplacementListJsonConverter.cs b/Core/Helpers/Json/CustomConverters/TrileEmplacementListJsonConverter.cs index f90d63d..b9a6ac6 100644 --- a/Core/Helpers/Json/CustomConverters/TrileEmplacementListJsonConverter.cs +++ b/Core/Helpers/Json/CustomConverters/TrileEmplacementListJsonConverter.cs @@ -51,10 +51,10 @@ public override void Write( foreach (var pos in triles) { - if (options.WriteIndented) - { - writer.WriteRawValue("\n" + new string(' ', writer.CurrentDepth * 2) + $"[{pos.X}, {pos.Y}, {pos.Z}]"); - } + writer.WriteRawValue(options.WriteIndented + ? $"\n{new string(' ', writer.CurrentDepth * 2)}[{pos.X}, {pos.Y}, {pos.Z}]" + : $"[{pos.X},{pos.Y},{pos.Z}]" + ); } writer.WriteEndArray(); diff --git a/Core/Helpers/Json/CustomConverters/VectorJsonConverters.cs b/Core/Helpers/Json/CustomConverters/VectorJsonConverters.cs index ac1d0c8..49fffc9 100644 --- a/Core/Helpers/Json/CustomConverters/VectorJsonConverters.cs +++ b/Core/Helpers/Json/CustomConverters/VectorJsonConverters.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Globalization; +using System.Text.Json; using System.Text.Json.Serialization; using FEZRepacker.Core.Definitions.Game.XNA; @@ -33,12 +34,11 @@ public override void Write( Vector3 vector, JsonSerializerOptions options) { - // Preserve decimal places in JSON number by converting float value to decimal - writer.WriteStartArray(); - writer.WriteNumberValue((decimal)vector.X); - writer.WriteNumberValue((decimal)vector.Y); - writer.WriteNumberValue((decimal)vector.Z); - writer.WriteEndArray(); + var x = vector.X.ToString("R", CultureInfo.InvariantCulture); + var y = vector.Y.ToString("R", CultureInfo.InvariantCulture); + var z = vector.Z.ToString("R", CultureInfo.InvariantCulture); + + writer.WriteRawValue(options.WriteIndented ? $"[{x}, {y}, {z}]" : $"[{x},{y},{z}]"); } } @@ -68,11 +68,10 @@ public override void Write( Vector2 vector, JsonSerializerOptions options) { - // Preserve decimal places in JSON number by converting float value to decimal - writer.WriteStartArray(); - writer.WriteNumberValue((decimal)vector.X); - writer.WriteNumberValue((decimal)vector.Y); - writer.WriteEndArray(); + var x = vector.X.ToString("R", CultureInfo.InvariantCulture); + var y = vector.Y.ToString("R", CultureInfo.InvariantCulture); + + writer.WriteRawValue(options.WriteIndented ? $"[{x}, {y}]" : $"[{x},{y}]"); } } } diff --git a/Core/Helpers/Json/JsonContexts.cs b/Core/Helpers/Json/JsonContexts.cs new file mode 100644 index 0000000..122139f --- /dev/null +++ b/Core/Helpers/Json/JsonContexts.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; + +using FEZRepacker.Core.Definitions.Game.ArtObject; +using FEZRepacker.Core.Definitions.Game.Graphics; +using FEZRepacker.Core.Definitions.Game.NpcMetadata; +using FEZRepacker.Core.Definitions.Game.Sky; +using FEZRepacker.Core.Definitions.Game.TrackedSong; +using FEZRepacker.Core.Definitions.Game.TrileSet; +using FEZRepacker.Core.Definitions.Game.XNA; +using FEZRepacker.Core.Definitions.Json; + +namespace FEZRepacker.Core.Helpers.Json +{ + internal static class JsonContexts + { + public static List List => + [ + AnimatedTextureJsonSerializerContext.Default, + ArtObjectJsonSerializerContext.Default, + LevelJsonSerializerContext.Default, + MapTreeJsonSerializerContext.Default, + NpcMetadataJsonSerializerContext.Default, + SkyJsonSerializerContext.Default, + SpriteFontJsonSerializerContext.Default, + TextStorageJsonSerializerContext.Default, + TrackedSongJsonSerializerContext.Default, + TrileSetJsonSerializerContext.Default + ]; + } + + [JsonSerializable(typeof(AnimatedTexture))] + internal partial class AnimatedTextureJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(ArtObject))] + [JsonSerializable(typeof(IndexedPrimitives))] + internal partial class ArtObjectJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(LevelJsonModel))] + internal partial class LevelJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(MapTreeJsonModel))] + internal partial class MapTreeJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(NpcMetadata))] + internal partial class NpcMetadataJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(Sky))] + internal partial class SkyJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(SpriteFontPropertiesJsonModel))] + internal partial class SpriteFontJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(OrderedDictionary>))] + internal partial class TextStorageJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(TrackedSong))] + internal partial class TrackedSongJsonSerializerContext : JsonSerializerContext {} + + [JsonSerializable(typeof(TrileSet))] + [JsonSerializable(typeof(IndexedPrimitives))] + + internal partial class TrileSetJsonSerializerContext : JsonSerializerContext {} +} diff --git a/Core/Helpers/LzxDecoder.cs b/Core/Helpers/LzxDecoder.cs index a4c55c1..22336bf 100644 --- a/Core/Helpers/LzxDecoder.cs +++ b/Core/Helpers/LzxDecoder.cs @@ -3,6 +3,8 @@ * (C) 2003-2004 Stuart Caie. * (C) 2011 Ali Scissons. * + * Slightly modified with performance in mind for FEZRepacker + * * The LZX method was created by Jonathan Forbes and Tomi Poutanen, adapted * by Microsoft Corporation. * @@ -52,6 +54,8 @@ namespace Microsoft.Xna.Framework.Content class LzxDecoder { + private const int UINT_BITS = sizeof(uint) * 8; + public static uint[] position_base; public static byte[] extra_bits; @@ -87,7 +91,7 @@ public LzxDecoder(int window) m_state = new LzxState(); m_state.actual_size = 0; m_state.window = new byte[wndsize]; - for (int i = 0; i < wndsize; i++) m_state.window[i] = 0xDC; + m_state.window.AsSpan().Fill(0xDC); m_state.actual_size = wndsize; m_state.window_size = wndsize; m_state.window_posn = 0; @@ -115,9 +119,9 @@ public LzxDecoder(int window) m_state.LENGTH_len = new byte[LzxConstants.LENGTH_MAXSYMBOLS + LzxConstants.LENTABLE_SAFETY]; m_state.ALIGNED_table = new ushort[(1 << LzxConstants.ALIGNED_TABLEBITS) + (LzxConstants.ALIGNED_MAXSYMBOLS << 1)]; m_state.ALIGNED_len = new byte[LzxConstants.ALIGNED_MAXSYMBOLS + LzxConstants.LENTABLE_SAFETY]; - /* initialise tables to 0 (because deltas will be applied to them) */ - for (int i = 0; i < LzxConstants.MAINTREE_MAXSYMBOLS; i++) m_state.MAINTREE_len[i] = 0; - for (int i = 0; i < LzxConstants.LENGTH_MAXSYMBOLS; i++) m_state.LENGTH_len[i] = 0; + /* initialize tables to 0 (because deltas will be applied to them) */ + m_state.MAINTREE_len.AsSpan().Clear(); + m_state.LENGTH_len.AsSpan().Clear(); } public int Decompress(Stream inData, int inLen, Stream outData, int outLen) @@ -445,9 +449,7 @@ public int Decompress(Stream inData, int inLen, Stream outData, int outLen) case LzxConstants.BLOCKTYPE.UNCOMPRESSED: if ((inData.Position + this_run) > endpos) return -1; //TODO throw proper exception - byte[] temp_buffer = new byte[this_run]; - inData.Read(temp_buffer, 0, this_run); - temp_buffer.CopyTo(window, (int)window_posn); + inData.Read(window, (int)window_posn, this_run); window_posn += (uint)this_run; break; @@ -522,8 +524,7 @@ private int MakeDecodeTable(uint nsyms, uint nbits, byte[] length, ushort[] tabl if ((pos += bit_mask) > table_mask) return 1; /* table overrun */ /* fill all possible lookups of this symbol with the symbol itself */ - fill = bit_mask; - while (fill-- > 0) table[leaf++] = sym; + table.AsSpan((int)leaf, (int)bit_mask).Fill(sym); } } bit_mask >>= 1; @@ -534,7 +535,7 @@ private int MakeDecodeTable(uint nsyms, uint nbits, byte[] length, ushort[] tabl if (pos != table_mask) { /* clear the remainder of the table */ - for (sym = (ushort)pos; sym < table_mask; sym++) table[sym] = 0; + table.AsSpan((int)pos, (int)(table_mask - pos)).Clear(); /* give ourselves room for codes to grow by up to 16 more bits */ pos <<= 16; @@ -601,13 +602,15 @@ private void ReadLengths(byte[] lens, uint first, uint last, BitBuffer bitbuf) LzxConstants.PRETREE_MAXSYMBOLS, LzxConstants.PRETREE_TABLEBITS, bitbuf); if (z == 17) { - y = bitbuf.ReadBits(4); y += 4; - while (y-- != 0) lens[x++] = 0; + y = bitbuf.ReadBits(4) + 4; + lens.AsSpan((int)x, (int)y).Clear(); + x += y; } else if (z == 18) { - y = bitbuf.ReadBits(5); y += 20; - while (y-- != 0) lens[x++] = 0; + y = bitbuf.ReadBits(5) + 20; + lens.AsSpan((int)x, (int)y).Clear(); + x += y; } else if (z == 19) { @@ -631,7 +634,7 @@ private uint ReadHuffSym(ushort[] table, byte[] lengths, uint nsyms, uint nbits, bitbuf.EnsureBits(16); if ((i = table[bitbuf.PeekBits((byte)nbits)]) >= nsyms) { - j = (uint)(1 << (int)((sizeof(uint) * 8) - nbits)); + j = (uint)(1 << (int)(UINT_BITS - nbits)); do { j >>= 1; i <<= 1; i |= (bitbuf.GetBuffer() & j) != 0 ? (uint)1 : 0; @@ -670,14 +673,14 @@ public void EnsureBits(byte bits) int lo = (byte)byteStream.ReadByte(); int hi = (byte)byteStream.ReadByte(); //int amount2shift = sizeof(uint)*8 - 16 - bitsleft; - buffer |= (uint)(((hi << 8) | lo) << (sizeof(uint) * 8 - 16 - bitsleft)); + buffer |= (uint)(((hi << 8) | lo) << (UINT_BITS - 16 - bitsleft)); bitsleft += 16; } } public uint PeekBits(byte bits) { - return (buffer >> ((sizeof(uint) * 8) - bits)); + return (buffer >> (UINT_BITS - bits)); } public void RemoveBits(byte bits) diff --git a/Core/Helpers/OrderedDictionary.cs b/Core/Helpers/OrderedDictionary.cs new file mode 100644 index 0000000..65ea304 --- /dev/null +++ b/Core/Helpers/OrderedDictionary.cs @@ -0,0 +1,131 @@ +using System.Collections; + +using FEZRepacker.Core.Definitions.Game; + +namespace FEZRepacker.Core.Helpers +{ + [XnbType("System.Collections.Generic.Dictionary, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] + [XnbReaderType("Microsoft.Xna.Framework.Content.DictionaryReader", IsPrivate = true)] + internal class OrderedDictionary : IDictionary + { + private readonly Dictionary _dictionary; + private readonly List _keys; + private readonly List _values; + + public int Count => _dictionary.Count; + public ICollection Keys => _keys.AsReadOnly(); + public ICollection Values => _values.AsReadOnly(); + + public TValue this[TKey key] + { + get + { + return _dictionary[key]; + } + set + { + if (_dictionary.ContainsKey(key)) + { + RemoveFromLists(key); + } + + _dictionary[key] = value; + _keys.Add(key); + _values.Add(value); + } + } + + public OrderedDictionary() : this(0) { } + + public OrderedDictionary(int capacity) + { + _dictionary = new Dictionary(capacity); + _keys = new List(capacity); + _values = new List(capacity); + } + + public void Add(TKey key, TValue value) + { + _dictionary.Add(key, value); + _keys.Add(key); + _values.Add(value); + } + + public void Clear() + { + _dictionary.Clear(); + _keys.Clear(); + _values.Clear(); + } + + public bool ContainsKey(TKey key) + { + return _dictionary.ContainsKey(key); + } + + public bool ContainsValue(TValue value) + { + return _dictionary.ContainsValue(value); + } + + public IEnumerator> GetEnumerator() + { + for (var i = 0; i < _keys.Count; i++) + { + yield return new KeyValuePair(_keys[i], _values[i]); + } + } + + private void RemoveFromLists(TKey key) + { + int index = _keys.IndexOf(key); + if (index == -1) + { + return; + } + + _keys.RemoveAt(index); + _values.RemoveAt(index); + } + + public bool Remove(TKey key) + { + var removed = _dictionary.Remove(key); + if (removed) + { + RemoveFromLists(key); + } + return removed; + } + + public bool TryGetValue(TKey key, out TValue value) + { + return _dictionary.TryGetValue(key, out value); + } + + bool ICollection>.IsReadOnly => + ((ICollection>)_dictionary).IsReadOnly; + + void ICollection>.Add(KeyValuePair item) => + Add(item.Key, item.Value); + + bool ICollection>.Contains(KeyValuePair item) => + ((ICollection>)_dictionary).Contains(item); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => + ((ICollection>)_dictionary).CopyTo(array, arrayIndex); + + bool ICollection>.Remove(KeyValuePair item) + { + bool removed = ((ICollection>)_dictionary).Remove(item); + + if (removed) { + RemoveFromLists(item.Key); + } + + return removed; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} \ No newline at end of file diff --git a/Core/Helpers/SpriteSheetPackerUtil.cs b/Core/Helpers/SpriteSheetPackerUtil.cs new file mode 100644 index 0000000..b8fd9d5 --- /dev/null +++ b/Core/Helpers/SpriteSheetPackerUtil.cs @@ -0,0 +1,293 @@ +using FEZRepacker.Core.Definitions.Game.Graphics; +using FEZRepacker.Core.Definitions.Game.XNA; + +/* + * Modified image packer from SpriteSheetPacker by Nick Gravelyn + * including rectangle packer by Javier Arevalo + * + * The algorithm was modified for usage of regenerating sprite sheets + * accurate to those included as AnimatedTextures in FEZ. + * Following simplifications were applied: + * - frames are processed sequentially, ordered by their index + * - pack space is always a power of 2 + * - all frames are equal size + */ + +namespace FEZRepacker.Core.Helpers +{ + public static class SpriteSheetPackerUtil + { + private const int InitialAtlasSize = 2048; + + public static void PackFrames(this AnimatedTexture tex, int padding) + { + tex.AtlasWidth = InitialAtlasSize; + tex.AtlasHeight = InitialAtlasSize; + var lastAtlasWidth = InitialAtlasSize; + var lastAtlasHeight = InitialAtlasSize; + + var shrinkVertical = false; + + while (true) + { + if (!tex.TryPackFramesInCurrentAtlasSize(padding)) + { + if (shrinkVertical) + { + break; + } + + shrinkVertical = true; + tex.AtlasWidth += tex.FrameWidth + padding + padding; + tex.AtlasHeight += tex.FrameHeight + padding + padding; + continue; + } + + tex.ResizeAtlasToSmallestFrameFit(); + + if (!shrinkVertical) + { + tex.AtlasWidth -= padding; + } + tex.AtlasHeight -= padding; + + tex.ResizeAtlasToNextPowerOfTwo(); + + if (tex.AtlasWidth == lastAtlasWidth && tex.AtlasHeight == lastAtlasHeight) + { + if (shrinkVertical) + { + break; + } + shrinkVertical = true; + } + + lastAtlasWidth = tex.AtlasWidth; + lastAtlasHeight = tex.AtlasHeight; + + if (!shrinkVertical) + { + tex.AtlasWidth -= tex.FrameWidth; + } + tex.AtlasHeight -= tex.FrameHeight; + } + + tex.ResizeAtlasToSmallestFrameFit(); + tex.ResizeAtlasToNextPowerOfTwo(); + } + + private static bool TryPackFramesInCurrentAtlasSize(this AnimatedTexture tex, int padding) + { + ArevaloRectanglePacker rectanglePacker = new ArevaloRectanglePacker(tex.AtlasWidth, tex.AtlasHeight); + + for (var i = 0; i < tex.Frames.Count; i++) + { + var frameRect = tex.Frames[i].Rectangle; + if (!rectanglePacker.TryPack(frameRect, padding)) + { + return false; + } + tex.Frames[i].Rectangle = frameRect; + } + + return true; + } + + private static void ResizeAtlasToSmallestFrameFit(this AnimatedTexture tex) + { + tex.AtlasWidth = tex.AtlasHeight = 0; + foreach (var frame in tex.Frames) + { + tex.AtlasWidth = Math.Max(tex.AtlasWidth, frame.Rectangle.X + frame.Rectangle.Width); + tex.AtlasHeight = Math.Max(tex.AtlasHeight, frame.Rectangle.Y + frame.Rectangle.Height); + } + } + + private static void ResizeAtlasToNextPowerOfTwo(this AnimatedTexture tex) + { + tex.AtlasWidth = NextPowerOfTwo(tex.AtlasWidth); + tex.AtlasHeight = NextPowerOfTwo(tex.AtlasHeight); + } + + private static int NextPowerOfTwo(int num) + { + const int bitsInInt = sizeof(int) * 8; + + num--; + for (var i = 1; i < bitsInInt; i <<= 1) + { + num |= num >> i; + } + return num + 1; + } + + #region ArevaloRectanglePacker + + private class ArevaloRectanglePacker(int packingAreaWidth, int packingAreaHeight) + { + private struct Point(int x, int y) + { + public int X = x; + public int Y = y; + } + + private class AnchorRankComparer : IComparer + { + public static readonly AnchorRankComparer Default = new(); + public int Compare(Point left, Point right) => (left.X + left.Y) - (right.X + right.Y); + } + + private int _searchAreaHeight = 1; + private int _searchAreaWidth = 1; + + private readonly List _anchors = [new(0, 0)]; + private readonly List _packedRectangles = new(); + + public bool TryPack(Rectangle rectangle, int padding) + { + var rectWidth = rectangle.Width + padding; + var rectHeight = rectangle.Height + padding; + var anchorIndex = SelectAnchorRecursive(rectWidth, rectHeight, _searchAreaWidth, _searchAreaHeight); + + if (anchorIndex == -1) + { + rectangle.X = rectangle.Y = 0; + return false; + } + + var placement = _anchors[anchorIndex]; + + OptimizePlacement(placement, rectWidth, rectHeight); + + var blocksAnchor = + ((placement.X + rectWidth) > _anchors[anchorIndex].X) && + ((placement.Y + rectHeight) > _anchors[anchorIndex].Y); + + if (blocksAnchor) + { + _anchors.RemoveAt(anchorIndex); + } + + InsertAnchor(new Point(placement.X + rectWidth, placement.Y)); + InsertAnchor(new Point(placement.X, placement.Y + rectHeight)); + + _packedRectangles.Add(new Rectangle(placement.X, placement.Y, rectWidth, rectHeight)); + rectangle.X = placement.X; + rectangle.Y = placement.Y; + + return true; + } + + private void OptimizePlacement(Point placement, int rectangleWidth, int rectangleHeight) + { + var rectangle = new Rectangle(placement.X, placement.Y, rectangleWidth, rectangleHeight); + + var leftMost = placement.X; + while (IsFree(rectangle, packingAreaWidth, packingAreaHeight)) + { + leftMost = rectangle.X; + --rectangle.X; + } + + rectangle.X = placement.X; + + var topMost = placement.Y; + while (IsFree(rectangle, packingAreaWidth, packingAreaHeight)) + { + topMost = rectangle.Y; + --rectangle.Y; + } + + if ((placement.X - leftMost) > (placement.Y - topMost)) + { + placement.X = leftMost; + } + else + { + placement.Y = topMost; + } + } + + private int SelectAnchorRecursive(int rectWidth, int rectHeight, int testedAreaWidth, int testedAreaHeight) + { + while (true) + { + var freeAnchorIndex = FindFirstFreeAnchor(rectWidth, rectHeight, testedAreaWidth, testedAreaHeight); + if (freeAnchorIndex >= 0) + { + _searchAreaWidth = testedAreaWidth; + _searchAreaHeight = testedAreaHeight; + + return freeAnchorIndex; + } + + var canEnlargeWidth = (testedAreaWidth < packingAreaWidth); + var canEnlargeHeight = (testedAreaHeight < packingAreaHeight); + var shouldEnlargeHeight = (!canEnlargeWidth) || (testedAreaHeight < testedAreaWidth); + + if (canEnlargeHeight && shouldEnlargeHeight) + { + testedAreaHeight = Math.Min(testedAreaHeight * 2, packingAreaHeight); + continue; + } + + if (canEnlargeWidth) + { + testedAreaWidth = Math.Min(testedAreaWidth * 2, packingAreaWidth); + continue; + } + + return -1; + } + } + + private int FindFirstFreeAnchor(int rectWidth, int rectHeight, int testedAreaWidth, int testedAreaHeight) + { + var potentialLocation = new Rectangle(0, 0, rectWidth, rectHeight); + + for (var index = 0; index < _anchors.Count; ++index) + { + potentialLocation.X = _anchors[index].X; + potentialLocation.Y = _anchors[index].Y; + + if (IsFree(potentialLocation, testedAreaWidth, testedAreaHeight)) + { + return index; + } + } + + return -1; + } + + private bool IsFree(Rectangle rectangle, int testedPackingAreaWidth, int testedPackingAreaHeight) + { + var leavesPackingArea = + (rectangle.X < 0) || (rectangle.Y < 0) || + (rectangle.X + rectangle.Width > testedPackingAreaWidth) || + (rectangle.Y + rectangle.Height > testedPackingAreaHeight); + + return !leavesPackingArea && !IntersectsPackedRectangle(rectangle); + } + + private bool IntersectsPackedRectangle(Rectangle rectangle) + { + return _packedRectangles.Any(packed => + rectangle.X < packed.X + packed.Width && + rectangle.X + rectangle.Width > packed.X && + rectangle.Y < packed.Y + packed.Height && + rectangle.Y + rectangle.Height > packed.Y); + } + + private void InsertAnchor(Point anchor) + { + int insertIndex = _anchors.BinarySearch(anchor, AnchorRankComparer.Default); + if (insertIndex < 0) + insertIndex = ~insertIndex; + + _anchors.Insert(insertIndex, anchor); + } + } + + #endregion + } +} diff --git a/Core/Helpers/TexturesUtil.cs b/Core/Helpers/TexturesUtil.cs index 3c0eb26..cb4576e 100644 --- a/Core/Helpers/TexturesUtil.cs +++ b/Core/Helpers/TexturesUtil.cs @@ -1,6 +1,6 @@ -using FEZRepacker.Core.Definitions.Game.XNA; +using DirectX; -using Microsoft.Xna.Framework.Graphics; +using FEZRepacker.Core.Definitions.Game.XNA; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -18,13 +18,26 @@ public enum CubemapPart public static Image ImageFromTexture2D(Texture2D txt) { - var textureData = GetConvertedTextureData(txt); - return Image.LoadPixelData(textureData, txt.Width, txt.Height); + var convertedData = txt.Format switch + { + SurfaceFormat.Color => txt.TextureData, + SurfaceFormat.Dxt1 => DirectXTexCompress.DecompressDxt1(txt.TextureData, txt.Width, txt.Height), + SurfaceFormat.Dxt3 => DirectXTexCompress.DecompressDxt3(txt.TextureData, txt.Width, txt.Height), + SurfaceFormat.Dxt5 => DirectXTexCompress.DecompressDxt5(txt.TextureData, txt.Width, txt.Height), + _ => null + }; + + if (convertedData == null) + { + throw new InvalidDataException($"Texture2D has unsupported format ({txt.Format})"); + } + + return Image.LoadPixelData(convertedData, txt.Width, txt.Height); } - public static Texture2D ImageToTexture2D(Image img) + public static Texture2D ImageToTexture2D(Image img, SurfaceFormat format = SurfaceFormat.Color) { - Texture2D texture = new Texture2D() + var texture = new Texture2D() { Format = SurfaceFormat.Color, MipmapLevels = 1, @@ -35,6 +48,20 @@ public static Texture2D ImageToTexture2D(Image img) texture.TextureData = new byte[img.Width * img.Height * 4]; img.CopyPixelDataTo(texture.TextureData); + var compressedData = format switch + { + SurfaceFormat.Dxt1 => DirectXTexCompress.CompressDxt1(texture.TextureData, img.Width, img.Height), + SurfaceFormat.Dxt3 => DirectXTexCompress.CompressDxt3(texture.TextureData, img.Width, img.Height), + SurfaceFormat.Dxt5 => DirectXTexCompress.CompressDxt5(texture.TextureData, img.Width, img.Height), + _ => null + }; + + if (compressedData != null) + { + texture.Format = format; + texture.TextureData = compressedData; + } + return texture; } @@ -86,28 +113,5 @@ public static Image ConstructCubemap(Stream? imageAlbedoStream, Stream? return albedoImage; } - - - private static byte[] GetConvertedTextureData(Texture2D txt) - { - // Most of FEZ textures are saved in raw format (SurfaceFormat.Color) - // but some of them are not - try to convert them. - - var convertedData = txt.Format switch - { - SurfaceFormat.Color => txt.TextureData, - SurfaceFormat.Dxt1 => DxtUtil.DecompressDxt1(txt.TextureData, txt.Width, txt.Height), - SurfaceFormat.Dxt3 => DxtUtil.DecompressDxt3(txt.TextureData, txt.Width, txt.Height), - SurfaceFormat.Dxt5 => DxtUtil.DecompressDxt5(txt.TextureData, txt.Width, txt.Height), - _ => null - }; - - if (convertedData == null) - { - throw new InvalidDataException($"Texture2D has unsupported format ({txt.Format})"); - } - - return convertedData; - } } } diff --git a/Core/XNB/ContentSerialization/GenericContentSerializer.cs b/Core/XNB/ContentSerialization/GenericContentSerializer.cs deleted file mode 100644 index a69481b..0000000 --- a/Core/XNB/ContentSerialization/GenericContentSerializer.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.Linq.Expressions; -using System.Reflection; - -using FEZRepacker.Core.Definitions.Game; -using FEZRepacker.Core.Definitions.Game.XNA; -using FEZRepacker.Core.Helpers; - -namespace FEZRepacker.Core.XNB.ContentSerialization -{ - /// - /// Generalizes content type creation by using reflection on given type containing XnbTypeAttribute - /// by parsing each public property with XnbPropertyAttribute based on parameters contained in it. - /// This replaces the need of creating a content type for each custom asset type. - /// - /// Class type with XnbTypeAttribute to form content type from. - internal class GenericContentSerializer : XnbContentSerializer - { - private XnbAssemblyQualifier _name; - - private readonly Func _typeBuilder; - private Dictionary _propertyMap = new(); - private Dictionary _underlyingTypeMap = new(); - - public override XnbAssemblyQualifier Name => _name; - - public GenericContentSerializer() : base() - { - var qualifier = XnbAssemblyQualifier.GetFromXnbReaderType(typeof(T)); - if (qualifier.HasValue) _name = qualifier.Value; - PopulateReflectionMaps(); - _typeBuilder = CreateContainedTypeConstructor(); - } - - private void PopulateReflectionMaps() - { - _propertyMap = typeof(T).GetProperties() - .Where(property => Attribute.IsDefined(property, typeof(XnbPropertyAttribute))) - .ToDictionary( - property => property, - property => (property.GetCustomAttributes(typeof(XnbPropertyAttribute), false).Single() as XnbPropertyAttribute)! - ).OrderBy(pair => pair.Value.Order) - .ToDictionary(pair => pair.Key, pair => pair.Value); - - _underlyingTypeMap = _propertyMap.ToDictionary( - pair => pair.Key, - pair => Nullable.GetUnderlyingType(pair.Key.PropertyType) ?? pair.Key.PropertyType - ); - } - - private Func CreateContainedTypeConstructor() - { - var t = typeof(T); - var ex = new Expression[] { Expression.New(typeof(T)) }; - var block = Expression.Block(t, ex); - return Expression.Lambda>(block).Compile(); - } - - public override object Deserialize(XnbContentReader reader) - { - object content = _typeBuilder()!; - - foreach (var propertyMapRecord in _propertyMap) - { - var property = propertyMapRecord.Key; - var attribute = propertyMapRecord.Value; - - Type propertyType = _underlyingTypeMap[property]; - - if (attribute.Optional) - { - if (!reader.ReadBoolean()) continue; - } - - object? readValue = null; - - if (attribute.UseConverter) - { - readValue = reader.ReadContent(propertyType, attribute.SkipIdentifier); - } - else if (propertyType == typeof(bool)) readValue = reader.ReadBoolean(); - else if (propertyType == typeof(int)) readValue = reader.ReadInt32(); - else if (propertyType == typeof(byte)) readValue = reader.ReadByte(); - else if (propertyType == typeof(short)) readValue = reader.ReadInt16(); - else if (propertyType == typeof(float)) readValue = reader.ReadSingle(); - else if (propertyType == typeof(char)) readValue = reader.ReadChar(); - else if (propertyType == typeof(string)) readValue = reader.ReadString(); - else if (propertyType == typeof(Vector2)) readValue = reader.ReadVector2(); - else if (propertyType == typeof(Vector3)) readValue = reader.ReadVector3(); - else if (propertyType == typeof(Quaternion)) readValue = reader.ReadQuaternion(); - else if (propertyType == typeof(Color)) readValue = reader.ReadColor(); - else if (propertyType == typeof(TimeSpan)) readValue = new TimeSpan(reader.ReadInt64()); - else throw new NotSupportedException($"Type {propertyType.FullName} is not supported"); - - if (readValue != null) property.SetValue(content, readValue); - } - - return content; - } - - public override void Serialize(object data, XnbContentWriter writer) - { - foreach (var propertyMapRecord in _propertyMap) - { - var property = propertyMapRecord.Key; - var attribute = propertyMapRecord.Value; - - Type propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; - - object? writeValue = property.GetValue(data); - - if (attribute.Optional) - { - int typeID = writer.Identity.ContentTypes.FindIndex(t => t.ContentType == propertyType) + 1; - if (writeValue != null) - { - if (typeID > 0) writer.Write7BitEncodedInt(typeID); - else writer.Write(true); - } - else writer.Write(false); - - } - - if (writeValue != null) - { - if (attribute.UseConverter) - { - writer.WriteContent(propertyType, writeValue, attribute.SkipIdentifier); - } - else if (propertyType == typeof(bool)) writer.Write((bool)writeValue!); - else if (propertyType == typeof(int)) writer.Write((int)writeValue!); - else if (propertyType == typeof(byte)) writer.Write((byte)writeValue!); - else if (propertyType == typeof(short)) writer.Write((short)writeValue!); - else if (propertyType == typeof(float)) writer.Write((float)writeValue!); - else if (propertyType == typeof(char)) writer.Write(((char)writeValue!)); - else if (propertyType == typeof(string)) writer.Write((string)writeValue!); - else if (propertyType == typeof(Vector2)) writer.Write((Vector2)writeValue!); - else if (propertyType == typeof(Vector3)) writer.Write((Vector3)writeValue!); - else if (propertyType == typeof(Quaternion)) writer.Write((Quaternion)writeValue!); - else if (propertyType == typeof(Color)) writer.Write((Color)writeValue!); - else if (propertyType == typeof(TimeSpan)) writer.Write(((TimeSpan)writeValue!).Ticks); - else throw new NotSupportedException($"Type {propertyType.FullName} is not supported"); - } - } - } - } -} diff --git a/Core/XNB/ContentSerialization/System/ArrayContentSerializer.cs b/Core/XNB/ContentSerialization/System/ArrayContentSerializer.cs index e96a87e..e5f4ab2 100644 --- a/Core/XNB/ContentSerialization/System/ArrayContentSerializer.cs +++ b/Core/XNB/ContentSerialization/System/ArrayContentSerializer.cs @@ -5,6 +5,10 @@ internal class ArrayContentSerializer : XnbContentSerializer where T : n { private XnbAssemblyQualifier _name; private readonly bool _skipElementType; + + public override XnbAssemblyQualifier Name => _name; + public override Type[] UnderlyingContentTypes => [typeof(T)]; + public ArrayContentSerializer(bool skipElementType = true) : base() { // creating type assembly qualifier name @@ -12,16 +16,13 @@ public ArrayContentSerializer(bool skipElementType = true) : base() _name.Namespace = "Microsoft.Xna.Framework.Content"; _name.Name = "ArrayReader"; - var genericQualifier = XnbAssemblyQualifier.GetFromXnbType(typeof(T)); - if (genericQualifier.HasValue) _name.Templates[0] = genericQualifier.Value; + _name.Templates[0] = XnbAssemblyQualifier.GetForType(typeof(T)); // some arrays have element type prefixes, some dont. // i have no idea what's the rule here, im just making it a variable _skipElementType = skipElementType; } - public override XnbAssemblyQualifier Name => _name; - public override object Deserialize(XnbContentReader reader) { int dataCount = reader.ReadInt32(); @@ -36,6 +37,7 @@ public override object Deserialize(XnbContentReader reader) public override void Serialize(object data, XnbContentWriter writer) { + writer.TryClaimContentType(typeof(T)); T[] array = (T[])data; writer.Write(array.Length); diff --git a/Core/XNB/ContentSerialization/System/ByteArrayContentSerializer.cs b/Core/XNB/ContentSerialization/System/ByteArrayContentSerializer.cs index 6fd3215..a4398d3 100644 --- a/Core/XNB/ContentSerialization/System/ByteArrayContentSerializer.cs +++ b/Core/XNB/ContentSerialization/System/ByteArrayContentSerializer.cs @@ -8,7 +8,6 @@ public ByteArrayContentSerializer() : base() _name = typeof(ArrayContentSerializer).FullName ?? ""; _name.Namespace = "Microsoft.Xna.Framework.Content"; _name.Name = "ArrayReader"; - IsPrivate = true; } public override XnbAssemblyQualifier Name => _name; diff --git a/Core/XNB/ContentSerialization/System/DictionaryContentSerializer.cs b/Core/XNB/ContentSerialization/System/DictionaryContentSerializer.cs index 1771692..cdbcb59 100644 --- a/Core/XNB/ContentSerialization/System/DictionaryContentSerializer.cs +++ b/Core/XNB/ContentSerialization/System/DictionaryContentSerializer.cs @@ -1,11 +1,17 @@ -namespace FEZRepacker.Core.XNB.ContentSerialization.System +using FEZRepacker.Core.Helpers; + +namespace FEZRepacker.Core.XNB.ContentSerialization.System { - internal class DictionaryContentSerializer : XnbContentSerializer> where K : notnull + internal class DictionaryContentSerializer : XnbContentSerializer> where K : notnull { private XnbAssemblyQualifier _name; private bool skipKeyIdentifier; private bool skipValueIdentifier; + public override Type ContentType => typeof(IDictionary); + public override XnbAssemblyQualifier Name => _name; + public override Type[] UnderlyingContentTypes => [typeof(K), typeof(V)]; + public DictionaryContentSerializer( bool skipKeyIdentifier = false, bool skipValueIdentifier = false @@ -16,22 +22,17 @@ public DictionaryContentSerializer( _name.Namespace = "Microsoft.Xna.Framework.Content"; _name.Name = "DictionaryReader"; - var genericKeyQualifier = XnbAssemblyQualifier.GetFromXnbType(typeof(K)); - if (genericKeyQualifier.HasValue) _name.Templates[0] = genericKeyQualifier.Value; - - var genericValueQualifier = XnbAssemblyQualifier.GetFromXnbType(typeof(V)); - if (genericValueQualifier.HasValue) _name.Templates[1] = genericValueQualifier.Value; + _name.Templates[0] = XnbAssemblyQualifier.GetForType(typeof(K)); + _name.Templates[1] = XnbAssemblyQualifier.GetForType(typeof(V)); this.skipKeyIdentifier = skipKeyIdentifier; this.skipValueIdentifier = skipValueIdentifier; } - public override XnbAssemblyQualifier Name => _name; - public override object Deserialize(XnbContentReader reader) { - Dictionary data = new Dictionary(); int dataCount = reader.ReadInt32(); + var data = new OrderedDictionary(dataCount); for (int i = 0; i < dataCount; i++) { K? key = reader.ReadContent(skipKeyIdentifier); @@ -43,13 +44,16 @@ public override object Deserialize(XnbContentReader reader) public override void Serialize(object data, XnbContentWriter writer) { - Dictionary dict = (Dictionary)data; + writer.TryClaimContentType(typeof(K)); + writer.TryClaimContentType(typeof(V)); + + var dict = (IDictionary)data; writer.Write(dict.Count); foreach (var record in dict) { - writer.WriteContent(record.Key, skipKeyIdentifier); - writer.WriteContent(record.Value, skipValueIdentifier); + writer.WriteContent(record.Key, skipKeyIdentifier); + writer.WriteContent(record.Value, skipValueIdentifier); } } } diff --git a/Core/XNB/ContentSerialization/System/EnumContentSerializer.cs b/Core/XNB/ContentSerialization/System/EnumContentSerializer.cs index f478a56..965c172 100644 --- a/Core/XNB/ContentSerialization/System/EnumContentSerializer.cs +++ b/Core/XNB/ContentSerialization/System/EnumContentSerializer.cs @@ -3,7 +3,10 @@ internal class EnumContentSerializer : XnbContentSerializer where T : Enum { private XnbAssemblyQualifier _name; - + + public override XnbAssemblyQualifier Name => _name; + public override Type[] UnderlyingContentTypes => [typeof(int)]; + public EnumContentSerializer() : base() { // creating type assembly qualifier name @@ -11,11 +14,9 @@ public EnumContentSerializer() : base() _name.Namespace = "Microsoft.Xna.Framework.Content"; _name.Name = "EnumReader"; - var enumQualifier = XnbAssemblyQualifier.GetFromXnbType(typeof(T)); - if (enumQualifier.HasValue) _name.Templates[0] = enumQualifier.Value; + _name.Templates[0] = XnbAssemblyQualifier.GetForType(typeof(T)); } - - public override XnbAssemblyQualifier Name => _name; + public override object Deserialize(XnbContentReader reader) { @@ -25,6 +26,15 @@ public override object Deserialize(XnbContentReader reader) public override void Serialize(object data, XnbContentWriter writer) { int value = (int)data; + + if (!IsPrivate) + { + // to ensure underlying type serializer claim, we're writing integer value as content + // but only when private to prevent internal helper serializers from being claimed. + writer.WriteContent(value, true); + return; + } + writer.Write(value); } } diff --git a/Core/XNB/ContentSerialization/System/ListContentSerializer.cs b/Core/XNB/ContentSerialization/System/ListContentSerializer.cs index 319e1b8..4841da8 100644 --- a/Core/XNB/ContentSerialization/System/ListContentSerializer.cs +++ b/Core/XNB/ContentSerialization/System/ListContentSerializer.cs @@ -4,6 +4,10 @@ internal class ListContentSerializer : XnbContentSerializer> where T { private XnbAssemblyQualifier _name; private bool _skipElementType; + + public override XnbAssemblyQualifier Name => _name; + public override Type[] UnderlyingContentTypes => [typeof(T)]; + public ListContentSerializer(bool skipElementType = false) : base() { // creating type assembly qualifier name @@ -11,8 +15,7 @@ public ListContentSerializer(bool skipElementType = false) : base() _name.Namespace = "Microsoft.Xna.Framework.Content"; _name.Name = "ListReader"; - var genericQualifier = XnbAssemblyQualifier.GetFromXnbType(typeof(T)); - if (genericQualifier.HasValue) _name.Templates[0] = genericQualifier.Value; + _name.Templates[0] = XnbAssemblyQualifier.GetForType(typeof(T)); // similarly to arrays, elements can have type identifier prefix // but unlike arrays, this is much less common @@ -20,12 +23,10 @@ public ListContentSerializer(bool skipElementType = false) : base() _skipElementType = skipElementType; } - public override XnbAssemblyQualifier Name => _name; - public override object Deserialize(XnbContentReader reader) { - List data = new List(); int dataCount = reader.ReadInt32(); + List data = new List(dataCount); for (int i = 0; i < dataCount; i++) { T? value = reader.ReadContent(_skipElementType); @@ -36,6 +37,7 @@ public override object Deserialize(XnbContentReader reader) public override void Serialize(object data, XnbContentWriter writer) { + writer.TryClaimContentType(typeof(T)); List list = (List)data; writer.Write(list.Count); @@ -44,10 +46,5 @@ public override void Serialize(object data, XnbContentWriter writer) writer.WriteContent(value, _skipElementType); } } - - public override bool IsEmpty(object data) - { - return ((List)data).Count == 0; - } } } diff --git a/Core/XNB/ContentSerialization/System/StringContentSerializer.cs b/Core/XNB/ContentSerialization/System/StringContentSerializer.cs index bd75c4a..3954242 100644 --- a/Core/XNB/ContentSerialization/System/StringContentSerializer.cs +++ b/Core/XNB/ContentSerialization/System/StringContentSerializer.cs @@ -2,11 +2,6 @@ { internal class StringContentSerializer : XnbContentSerializer { - private bool isNullable; - - public StringContentSerializer(bool nullable = true) : base() { - isNullable = nullable; - } public override XnbAssemblyQualifier Name => "Microsoft.Xna.Framework.Content.StringReader"; @@ -19,10 +14,5 @@ public override void Serialize(object data, XnbContentWriter writer) { writer.Write((string)data); } - - public override bool IsEmpty(object data) - { - return isNullable && ((string)data).Length == 0; - } } } diff --git a/Core/XNB/ContentSerialization/XnbContentSerializer.cs b/Core/XNB/ContentSerialization/XnbContentSerializer.cs index 62be2a7..e08869a 100644 --- a/Core/XNB/ContentSerialization/XnbContentSerializer.cs +++ b/Core/XNB/ContentSerialization/XnbContentSerializer.cs @@ -7,6 +7,7 @@ internal abstract class XnbContentSerializer { public abstract XnbAssemblyQualifier Name { get; } public abstract Type ContentType { get; } + public virtual Type[] UnderlyingContentTypes => []; public bool IsPrivate { get; protected set; } public XnbContentSerializer() @@ -28,14 +29,12 @@ public XnbContentSerializer() public abstract void Serialize(object data, XnbContentWriter writer); /// - /// Used to determine whether an object of this content type is empty. - /// Used by a converter to read/write nullable types. + /// Marks content serializer private - its qualifier name won't appear in XNB header. /// - /// - /// - public virtual bool IsEmpty(object data) + public XnbContentSerializer MarkPrivate() { - return false; + IsPrivate = true; + return this; } } diff --git a/Core/XNB/ContentTypes/AnimatedTextureContentIdentity.cs b/Core/XNB/ContentTypes/AnimatedTextureContentIdentity.cs index 4eb1250..84c824d 100644 --- a/Core/XNB/ContentTypes/AnimatedTextureContentIdentity.cs +++ b/Core/XNB/ContentTypes/AnimatedTextureContentIdentity.cs @@ -2,20 +2,19 @@ using FEZRepacker.Core.XNB.ContentSerialization.System; using FEZRepacker.Core.Definitions.Game.Graphics; -using FEZRepacker.Core.Definitions.Game.XNA; namespace FEZRepacker.Core.XNB.ContentTypes { internal class AnimatedTextureContentIdentity : XnbPrimaryContentIdentity { - protected override List ContentTypesFactory => new() + protected override List ContentSerializersFactory => new() { - new GenericContentSerializer(), - new ByteArrayContentSerializer(), + new AnimatedTextureContentSerializer(), + new ByteArrayContentSerializer().MarkPrivate(), new ListContentSerializer(), - new GenericContentSerializer(), + new FrameContentContentSerializer(), new TimeSpanContentSerializer(), - new GenericContentSerializer() + new RectangleContentSerializer() }; } } diff --git a/Core/XNB/ContentTypes/ArtObjectContentIdentity.cs b/Core/XNB/ContentTypes/ArtObjectContentIdentity.cs index b1bc389..441a38f 100644 --- a/Core/XNB/ContentTypes/ArtObjectContentIdentity.cs +++ b/Core/XNB/ContentTypes/ArtObjectContentIdentity.cs @@ -1,7 +1,6 @@  using FEZRepacker.Core.Definitions.Game.ArtObject; using FEZRepacker.Core.Definitions.Game.Common; -using FEZRepacker.Core.Definitions.Game.Graphics; using FEZRepacker.Core.Definitions.Game.XNA; using FEZRepacker.Core.XNB.ContentSerialization; using FEZRepacker.Core.XNB.ContentSerialization.System; @@ -10,15 +9,15 @@ namespace FEZRepacker.Core.XNB.ContentTypes { internal class ArtObjectContentIdentity : XnbPrimaryContentIdentity { - protected override List ContentTypesFactory => new() + protected override List ContentSerializersFactory => new() { - new GenericContentSerializer(), - new GenericContentSerializer(), - new EnumContentSerializer(), - new ByteArrayContentSerializer(), - new GenericContentSerializer>(), - new GenericContentSerializer(), - new GenericContentSerializer(), + new ArtObjectContentSerializer(), + new Texture2DContentSerializer(), + new EnumContentSerializer().MarkPrivate(), + new ByteArrayContentSerializer().MarkPrivate(), + new IndexedPrimitivesContentSerializer(), + new VertexInstanceContentSerializer(), + new MatrixContentSerializer(), new EnumContentSerializer(), new Int32ContentSerializer(), new ArrayContentSerializer(), diff --git a/Core/XNB/ContentTypes/EffectContentIdentity.cs b/Core/XNB/ContentTypes/EffectContentIdentity.cs index 6e3a746..114ad3c 100644 --- a/Core/XNB/ContentTypes/EffectContentIdentity.cs +++ b/Core/XNB/ContentTypes/EffectContentIdentity.cs @@ -1,5 +1,4 @@  -using FEZRepacker.Core.Definitions.Game.XNA; using FEZRepacker.Core.XNB.ContentSerialization; using FEZRepacker.Core.XNB.ContentSerialization.System; @@ -7,10 +6,10 @@ namespace FEZRepacker.Core.XNB.ContentTypes { internal class EffectContentIdentity : XnbPrimaryContentIdentity { - protected override List ContentTypesFactory => new() + protected override List ContentSerializersFactory => new() { - new GenericContentSerializer(), - new ByteArrayContentSerializer() + new EffectContentSerializer(), + new ByteArrayContentSerializer().MarkPrivate() }; } diff --git a/Core/XNB/ContentTypes/LevelContentIdentity.cs b/Core/XNB/ContentTypes/LevelContentIdentity.cs index a5e80f8..5d86c23 100644 --- a/Core/XNB/ContentTypes/LevelContentIdentity.cs +++ b/Core/XNB/ContentTypes/LevelContentIdentity.cs @@ -9,68 +9,68 @@ namespace FEZRepacker.Core.XNB.ContentTypes { internal class LevelContentIdentity : XnbPrimaryContentIdentity { - protected override List ContentTypesFactory => new() + protected override List ContentSerializersFactory => new() { - new GenericContentSerializer(), + new LevelContentSerializer(), new StringContentSerializer(), - new GenericContentSerializer(), - new GenericContentSerializer(), + new TrileFaceContentSerializer(), + new TrileEmplacementContentSerializer(), new EnumContentSerializer(), new Int32ContentSerializer(), new BooleanContentSerializer(), new EnumContentSerializer(), - new DictionaryContentSerializer(), - new GenericContentSerializer(), + new DictionaryContentSerializer(true, false), + new VolumeContentSerializer(), new ArrayContentSerializer(), - new GenericContentSerializer(), + new VolumeActorSettingsContentSerializer(), new ListContentSerializer(), - new GenericContentSerializer(), + new DotDialogueLineContentSerializer(), new ArrayContentSerializer(), new EnumContentSerializer(), - new DictionaryContentSerializer(), - new GenericContentSerializer