From 6fbfcc3d720fa4d75cd725b12c5295e021306ecf Mon Sep 17 00:00:00 2001 From: zerocker Date: Mon, 27 Jul 2026 14:46:54 +0900 Subject: [PATCH 1/3] fix: explicitly define more nullable types --- Core/Definitions/Game/ArtObject/ArtObject.cs | 4 +- .../Game/Graphics/IndexedPrimitives.cs | 10 +- Core/Definitions/Game/Level/AmbienceTrack.cs | 2 +- .../Game/Level/ArtObjectActorSettings.cs | 2 +- .../Game/Level/ArtObjectInstance.cs | 6 +- .../Definitions/Game/Level/DotDialogueLine.cs | 2 +- Core/Definitions/Game/Level/Level.cs | 26 ++-- Core/Definitions/Game/Level/MovementPath.cs | 4 +- .../Game/Level/NpcActionContent.cs | 4 +- Core/Definitions/Game/Level/NpcInstance.cs | 8 +- .../Game/Level/Scripting/Script.cs | 4 +- .../Game/Level/Scripting/ScriptAction.cs | 4 +- .../Game/Level/Scripting/ScriptCondition.cs | 2 +- .../Game/Level/Scripting/ScriptTrigger.cs | 2 +- Core/Definitions/Game/Level/SpeechLine.cs | 4 +- Core/Definitions/Game/Level/TrileGroup.cs | 4 +- Core/Definitions/Game/Level/Volume.cs | 2 +- .../Game/Level/VolumeActorSettings.cs | 2 +- Core/Definitions/Game/MapTree/MapNode.cs | 4 +- .../Game/MapTree/MapNodeConnection.cs | 2 +- Core/Definitions/Game/MapTree/MapTree.cs | 2 +- .../Definitions/Game/MapTree/WinConditions.cs | 2 +- .../Game/NpcMetadata/NpcMetadata.cs | 4 +- Core/Definitions/Game/Sky/Sky.cs | 20 +-- Core/Definitions/Game/Sky/SkyLayer.cs | 2 +- Core/Definitions/Game/TrackedSong/Loop.cs | 14 +- .../Game/TrackedSong/TrackedSong.cs | 20 ++- Core/Definitions/Game/TrileSet/Trile.cs | 9 +- Core/Definitions/Game/TrileSet/TrileSet.cs | 4 +- Core/Definitions/Json/MapTreeJsonModel.cs | 5 +- Tests/TestPacking.cs | 12 +- Tests/TestUtils.cs | 37 +++-- Tests/TypeContractDefaultsTests.cs | 124 ++++++++++++++++ Tests/TypeContractNullabilityTests.cs | 131 +++++++++++++++++ Tests/TypeContractXnbTests.cs | 133 ++++++++++++++++++ 35 files changed, 518 insertions(+), 99 deletions(-) create mode 100644 Tests/TypeContractDefaultsTests.cs create mode 100644 Tests/TypeContractNullabilityTests.cs create mode 100644 Tests/TypeContractXnbTests.cs diff --git a/Core/Definitions/Game/ArtObject/ArtObject.cs b/Core/Definitions/Game/ArtObject/ArtObject.cs index d9b78d2..ed364c4 100644 --- a/Core/Definitions/Game/ArtObject/ArtObject.cs +++ b/Core/Definitions/Game/ArtObject/ArtObject.cs @@ -16,14 +16,14 @@ public class ArtObject [JsonIgnore] [XnbProperty(UseConverter = true)] - public Texture2D Cubemap { get; set; } = new(); + public Texture2D? Cubemap { get; set; } [XnbProperty] public Vector3 Size { get; set; } [JsonIgnore] [XnbProperty(UseConverter = true)] - public IndexedPrimitives Geometry { get; set; } = new(); + public IndexedPrimitives? Geometry { get; set; } [XnbProperty(UseConverter = true)] public ActorType ActorType { get; set; } diff --git a/Core/Definitions/Game/Graphics/IndexedPrimitives.cs b/Core/Definitions/Game/Graphics/IndexedPrimitives.cs index 2b7ca53..58cb255 100644 --- a/Core/Definitions/Game/Graphics/IndexedPrimitives.cs +++ b/Core/Definitions/Game/Graphics/IndexedPrimitives.cs @@ -11,15 +11,9 @@ public class IndexedPrimitives public PrimitiveType PrimitiveType { get; set; } [XnbProperty(UseConverter = true)] - public TemplateType[] Vertices { get; set; } + public TemplateType[]? Vertices { get; set; } [XnbProperty(UseConverter = true)] - public ushort[] Indices { get; set; } - - public IndexedPrimitives() - { - Vertices = new TemplateType[0]; - Indices = new ushort[0]; - } + public ushort[]? Indices { get; set; } } } diff --git a/Core/Definitions/Game/Level/AmbienceTrack.cs b/Core/Definitions/Game/Level/AmbienceTrack.cs index 0995ceb..6812e8c 100644 --- a/Core/Definitions/Game/Level/AmbienceTrack.cs +++ b/Core/Definitions/Game/Level/AmbienceTrack.cs @@ -5,7 +5,7 @@ public class AmbienceTrack { [XnbProperty(UseConverter = true)] - public string Name { get; set; } = ""; + public string? Name { get; set; } [XnbProperty] public bool Day { get; set; } diff --git a/Core/Definitions/Game/Level/ArtObjectActorSettings.cs b/Core/Definitions/Game/Level/ArtObjectActorSettings.cs index f1dc68b..1bd0453 100644 --- a/Core/Definitions/Game/Level/ArtObjectActorSettings.cs +++ b/Core/Definitions/Game/Level/ArtObjectActorSettings.cs @@ -50,7 +50,7 @@ public class ArtObjectActorSettings public string? TreasureMapName { get; set; } [XnbProperty(UseConverter = true)] - public FaceOrientation[] InvisibleSides { get; set; } = { }; + public FaceOrientation[]? InvisibleSides { get; set; } = { }; [XnbProperty] public float TimeswitchWindBackSpeed { get; set; } diff --git a/Core/Definitions/Game/Level/ArtObjectInstance.cs b/Core/Definitions/Game/Level/ArtObjectInstance.cs index 7734533..9b0c0f7 100644 --- a/Core/Definitions/Game/Level/ArtObjectInstance.cs +++ b/Core/Definitions/Game/Level/ArtObjectInstance.cs @@ -13,12 +13,12 @@ public class ArtObjectInstance public Vector3 Position { get; set; } [XnbProperty] - public Quaternion Rotation { get; set; } + public Quaternion Rotation { get; set; } = Quaternion.Identity; [XnbProperty] - public Vector3 Scale { get; set; } + public Vector3 Scale { get; set; } = Vector3.One; [XnbProperty(UseConverter = true)] - public ArtObjectActorSettings? ActorSettings { get; set; } + public ArtObjectActorSettings? ActorSettings { get; set; } = new(); } } diff --git a/Core/Definitions/Game/Level/DotDialogueLine.cs b/Core/Definitions/Game/Level/DotDialogueLine.cs index 34a0812..be6b782 100644 --- a/Core/Definitions/Game/Level/DotDialogueLine.cs +++ b/Core/Definitions/Game/Level/DotDialogueLine.cs @@ -5,7 +5,7 @@ public class DotDialogueLine { [XnbProperty(UseConverter = true)] - public string ResourceText { get; set; } = ""; + public string? ResourceText { get; set; } [XnbProperty] public bool Grouped { get; set; } diff --git a/Core/Definitions/Game/Level/Level.cs b/Core/Definitions/Game/Level/Level.cs index 2ba7048..1d5d4cb 100644 --- a/Core/Definitions/Game/Level/Level.cs +++ b/Core/Definitions/Game/Level/Level.cs @@ -10,7 +10,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level public class Level { [XnbProperty(UseConverter = true)] - public string Name { get; set; } = ""; + public string? Name { get; set; } [XnbProperty] public Vector3 Size { get; set; } @@ -37,7 +37,7 @@ public class Level public string? GomezHaloName { get; set; } [XnbProperty] - public bool HaloFiltering { get; set; } + public bool HaloFiltering { get; set; } = true; [XnbProperty] public bool BlinkingAlpha { get; set; } @@ -55,13 +55,13 @@ public class Level public string SkyName { get; set; } = ""; [XnbProperty(UseConverter = true)] - public string TrileSetName { get; set; } = ""; + public string? TrileSetName { get; set; } [XnbProperty(UseConverter = true)] - public IDictionary Volumes { get; set; } = new OrderedDictionary(); + public IDictionary? Volumes { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public IDictionary Scripts { get; set; } = new OrderedDictionary(); + public IDictionary? Scripts { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] public string? SongName { get; set; } @@ -73,22 +73,22 @@ public class Level public int FAPFadeOutLength { get; set; } [XnbProperty(UseConverter = true)] - public IDictionary Triles { get; set; } = new OrderedDictionary(); + public IDictionary? Triles { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public IDictionary ArtObjects { get; set; } = new OrderedDictionary(); + public IDictionary? ArtObjects { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public IDictionary BackgroundPlanes { get; set; } = new OrderedDictionary(); + public IDictionary? BackgroundPlanes { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public IDictionary Groups { get; set; } = new OrderedDictionary(); + public IDictionary? Groups { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public IDictionary NonPlayerCharacters { get; set; } = new OrderedDictionary(); + public IDictionary? NonPlayerCharacters { get; set; } = new OrderedDictionary(); [XnbProperty(UseConverter = true)] - public IDictionary Paths { get; set; } = new OrderedDictionary(); + public IDictionary? Paths { get; set; } = new OrderedDictionary(); [XnbProperty] public bool Descending { get; set; } @@ -100,10 +100,10 @@ public class Level public bool LowPass { get; set; } [XnbProperty(UseConverter = true)] - public List MutedLoops { get; set; } = new(); + public List? MutedLoops { get; set; } = new(); [XnbProperty(UseConverter = true)] - public List AmbienceTracks { get; set; } = new(); + public List? AmbienceTracks { get; set; } = new(); [XnbProperty(UseConverter = true)] public LevelNodeType NodeType { get; set; } diff --git a/Core/Definitions/Game/Level/MovementPath.cs b/Core/Definitions/Game/Level/MovementPath.cs index fdd4388..9cd0e06 100644 --- a/Core/Definitions/Game/Level/MovementPath.cs +++ b/Core/Definitions/Game/Level/MovementPath.cs @@ -5,7 +5,7 @@ public class MovementPath { [XnbProperty(UseConverter = true)] - public List Segments { get; set; } = new(); + public List? Segments { get; set; } = new(); [XnbProperty] public bool NeedsTrigger { get; set; } @@ -25,4 +25,4 @@ public class MovementPath [XnbProperty] public bool SaveTrigger { get; set; } } -} \ No newline at end of file +} diff --git a/Core/Definitions/Game/Level/NpcActionContent.cs b/Core/Definitions/Game/Level/NpcActionContent.cs index 37e4005..4673eff 100644 --- a/Core/Definitions/Game/Level/NpcActionContent.cs +++ b/Core/Definitions/Game/Level/NpcActionContent.cs @@ -5,9 +5,9 @@ public class NpcActionContent { [XnbProperty(UseConverter = true)] - public string AnimationName { get; set; } = ""; + public string? AnimationName { get; set; } [XnbProperty(UseConverter = true)] - public string SoundName { get; set; } = ""; + public string? SoundName { get; set; } } } diff --git a/Core/Definitions/Game/Level/NpcInstance.cs b/Core/Definitions/Game/Level/NpcInstance.cs index 0202b60..82b21ac 100644 --- a/Core/Definitions/Game/Level/NpcInstance.cs +++ b/Core/Definitions/Game/Level/NpcInstance.cs @@ -17,7 +17,7 @@ public class NpcInstance public Vector3 DestinationOffset { get; set; } [XnbProperty] - public float WalkSpeed { get; set; } + public float WalkSpeed { get; set; } = 1.5f; [XnbProperty] public bool RandomizeSpeech { get; set; } @@ -32,9 +32,9 @@ public class NpcInstance public ActorType ActorType { get; set; } [XnbProperty(UseConverter = true)] - public List Speech { get; set; } = null!; + public List? Speech { get; set; } = new(); [XnbProperty(UseConverter = true)] - public IDictionary Actions { get; set; } = null!; + public IDictionary? Actions { get; set; } = new FEZRepacker.Core.Helpers.OrderedDictionary(); } -} \ No newline at end of file +} diff --git a/Core/Definitions/Game/Level/Scripting/Script.cs b/Core/Definitions/Game/Level/Scripting/Script.cs index bc44d50..7f2afc0 100644 --- a/Core/Definitions/Game/Level/Scripting/Script.cs +++ b/Core/Definitions/Game/Level/Scripting/Script.cs @@ -11,13 +11,13 @@ public class Script public TimeSpan? Timeout { get; set; } [XnbProperty(UseConverter = true)] - public List? Triggers { get; set; } + public List? Triggers { get; set; } = new(); [XnbProperty(UseConverter = true)] public List? Conditions { get; set; } [XnbProperty(UseConverter = true)] - public List? Actions { get; set; } + public List? Actions { get; set; } = new(); [XnbProperty] public bool OneTime { get; set; } diff --git a/Core/Definitions/Game/Level/Scripting/ScriptAction.cs b/Core/Definitions/Game/Level/Scripting/ScriptAction.cs index aa0b064..dca7f9c 100644 --- a/Core/Definitions/Game/Level/Scripting/ScriptAction.cs +++ b/Core/Definitions/Game/Level/Scripting/ScriptAction.cs @@ -5,13 +5,13 @@ public class ScriptAction { [XnbProperty(UseConverter = true)] - public Entity Object { get; set; } = new(); + public Entity? Object { get; set; } = new(); [XnbProperty] public string Operation { get; set; } = ""; [XnbProperty(UseConverter = true)] - public string[] Arguments { get; set; } = { }; + public string[]? Arguments { get; set; } = { }; [XnbProperty] public bool Killswitch { get; set; } // whether action should kill this script diff --git a/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs b/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs index b138e36..336c7fe 100644 --- a/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs +++ b/Core/Definitions/Game/Level/Scripting/ScriptCondition.cs @@ -5,7 +5,7 @@ public class ScriptCondition { [XnbProperty(UseConverter = true)] - public Entity Object { get; set; } = new(); + public Entity? Object { get; set; } = new(); [XnbProperty(UseConverter = true)] public ComparisonOperator Operator { get; set; } = ComparisonOperator.None; diff --git a/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs b/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs index 3cfd45d..39786d5 100644 --- a/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs +++ b/Core/Definitions/Game/Level/Scripting/ScriptTrigger.cs @@ -5,7 +5,7 @@ public class ScriptTrigger { [XnbProperty(UseConverter = true)] - public Entity Object { get; set; } = new(); + public Entity? Object { get; set; } = new(); [XnbProperty] public string Event { get; set; } = ""; diff --git a/Core/Definitions/Game/Level/SpeechLine.cs b/Core/Definitions/Game/Level/SpeechLine.cs index 5fe6013..db03e86 100644 --- a/Core/Definitions/Game/Level/SpeechLine.cs +++ b/Core/Definitions/Game/Level/SpeechLine.cs @@ -5,9 +5,9 @@ public class SpeechLine { [XnbProperty(UseConverter = true)] - public string Text { get; set; } = ""; + public string? Text { get; set; } [XnbProperty(UseConverter = true)] public NpcActionContent? OverrideContent { get; set; } } -} \ No newline at end of file +} diff --git a/Core/Definitions/Game/Level/TrileGroup.cs b/Core/Definitions/Game/Level/TrileGroup.cs index fa7d4d6..b5e520a 100644 --- a/Core/Definitions/Game/Level/TrileGroup.cs +++ b/Core/Definitions/Game/Level/TrileGroup.cs @@ -8,7 +8,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level public class TrileGroup { [XnbProperty(UseConverter = true)] - public List Triles { get; set; } = new(); + public List? Triles { get; set; } = new(); [XnbProperty(UseConverter = true)] public MovementPath? Path { get; set; } = null; @@ -55,4 +55,4 @@ public class TrileGroup [XnbProperty(UseConverter = true)] public string? AssociatedSound { get; set; } } -} \ No newline at end of file +} diff --git a/Core/Definitions/Game/Level/Volume.cs b/Core/Definitions/Game/Level/Volume.cs index 6f0e1b6..89b0978 100644 --- a/Core/Definitions/Game/Level/Volume.cs +++ b/Core/Definitions/Game/Level/Volume.cs @@ -8,7 +8,7 @@ namespace FEZRepacker.Core.Definitions.Game.Level public class Volume { [XnbProperty(UseConverter = true)] - public FaceOrientation[] Orientations { get; set; } = { }; + public FaceOrientation[]? Orientations { get; set; } = { }; [XnbProperty] public Vector3 From { get; set; } diff --git a/Core/Definitions/Game/Level/VolumeActorSettings.cs b/Core/Definitions/Game/Level/VolumeActorSettings.cs index 8b434af..267d769 100644 --- a/Core/Definitions/Game/Level/VolumeActorSettings.cs +++ b/Core/Definitions/Game/Level/VolumeActorSettings.cs @@ -13,7 +13,7 @@ public class VolumeActorSettings public bool IsPointOfInterest { get; set; } [XnbProperty(UseConverter = true)] - public List DotDialogue { get; set; } = new(); + public List? DotDialogue { get; set; } = new(); [XnbProperty] public bool WaterLocked { get; set; } diff --git a/Core/Definitions/Game/MapTree/MapNode.cs b/Core/Definitions/Game/MapTree/MapNode.cs index 0891238..04e5c62 100644 --- a/Core/Definitions/Game/MapTree/MapNode.cs +++ b/Core/Definitions/Game/MapTree/MapNode.cs @@ -10,13 +10,13 @@ public class MapNode public string LevelName { get; set; } = ""; [XnbProperty(UseConverter = true)] - public List Connections { get; set; } = new(); + public List? Connections { get; set; } = new(); [XnbProperty(UseConverter = true)] public LevelNodeType NodeType { get; set; } [XnbProperty(UseConverter = true)] - public WinConditions Conditions { get; set; } = new(); + public WinConditions? Conditions { get; set; } = new(); [XnbProperty] public bool HasLesserGate { get; set; } diff --git a/Core/Definitions/Game/MapTree/MapNodeConnection.cs b/Core/Definitions/Game/MapTree/MapNodeConnection.cs index 52aa3f3..4b184cc 100644 --- a/Core/Definitions/Game/MapTree/MapNodeConnection.cs +++ b/Core/Definitions/Game/MapTree/MapNodeConnection.cs @@ -10,7 +10,7 @@ public class MapNodeConnection public FaceOrientation Face { get; set; } [XnbProperty(UseConverter = true)] - public MapNode Node { get; set; } = new(); + public MapNode? Node { get; set; } = new(); [XnbProperty] public float BranchOversize { get;set; } diff --git a/Core/Definitions/Game/MapTree/MapTree.cs b/Core/Definitions/Game/MapTree/MapTree.cs index da60a45..16909d4 100644 --- a/Core/Definitions/Game/MapTree/MapTree.cs +++ b/Core/Definitions/Game/MapTree/MapTree.cs @@ -5,6 +5,6 @@ public class MapTree { [XnbProperty(UseConverter = true)] - public MapNode Root { get; set; } = new(); + public MapNode? Root { get; set; } } } diff --git a/Core/Definitions/Game/MapTree/WinConditions.cs b/Core/Definitions/Game/MapTree/WinConditions.cs index d6bc8f8..002ba01 100644 --- a/Core/Definitions/Game/MapTree/WinConditions.cs +++ b/Core/Definitions/Game/MapTree/WinConditions.cs @@ -14,7 +14,7 @@ public class WinConditions public int UnlockedDoorCount { get; set; } [XnbProperty(UseConverter = true)] - public List ScriptIds { get; set; } = new(); + public List? ScriptIds { get; set; } = new(); [XnbProperty] public int CubeShardCount { get; set; } diff --git a/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs b/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs index 3826a03..387c852 100644 --- a/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs +++ b/Core/Definitions/Game/NpcMetadata/NpcMetadata.cs @@ -7,7 +7,7 @@ namespace FEZRepacker.Core.Definitions.Game.NpcMetadata public class NpcMetadata { [XnbProperty] - public float WalkSpeed { get; set; } + public float WalkSpeed { get; set; } = 1.5f; [XnbProperty] public bool AvoidsGomez { get;set; } @@ -16,6 +16,6 @@ public class NpcMetadata public string? SoundPath { get; set; } [XnbProperty(UseConverter = true)] - public List SoundActions { get; set; } = new(); + public List? SoundActions { get; set; } = new(); } } diff --git a/Core/Definitions/Game/Sky/Sky.cs b/Core/Definitions/Game/Sky/Sky.cs index 74cc505..ba2e238 100644 --- a/Core/Definitions/Game/Sky/Sky.cs +++ b/Core/Definitions/Game/Sky/Sky.cs @@ -5,25 +5,25 @@ public class Sky { [XnbProperty] - public string Name { get; set; } = ""; + public string Name { get; set; } = "Default"; [XnbProperty] - public string Background { get; set; } = ""; + public string Background { get; set; } = "SkyBack"; [XnbProperty] - public float WindSpeed { get; set; } + public float WindSpeed { get; set; } = 1f; [XnbProperty] - public float Density { get; set; } + public float Density { get; set; } = 1f; [XnbProperty] - public float FogDensity { get; set; } + public float FogDensity { get; set; } = 0.02f; [XnbProperty(UseConverter = true)] - public List Layers { get; set; } = new(); + public List? Layers { get; set; } = new(); [XnbProperty(UseConverter = true)] - public List Clouds { get; set; } = new(); + public List? Clouds { get; set; } = new(); [XnbProperty(UseConverter = true)] public string? Shadows { get; set; } @@ -41,7 +41,7 @@ public class Sky public bool HorizontalScrolling { get; set; } [XnbProperty] - public float LayerBaseHeight { get; set; } + public float LayerBaseHeight { get; set; } = 0.5f; [XnbProperty] public float InterLayerVerticalDistance { get; set; } @@ -65,10 +65,10 @@ public class Sky public float WindDistance { get; set; } [XnbProperty] - public float CloudsParallax { get; set; } + public float CloudsParallax { get; set; } = 1f; [XnbProperty] - public float ShadowOpacity { get; set; } + public float ShadowOpacity { get; set; } = 0.7f; [XnbProperty] public bool FoliageShadows { get; set; } diff --git a/Core/Definitions/Game/Sky/SkyLayer.cs b/Core/Definitions/Game/Sky/SkyLayer.cs index f6b7e85..aef13f2 100644 --- a/Core/Definitions/Game/Sky/SkyLayer.cs +++ b/Core/Definitions/Game/Sky/SkyLayer.cs @@ -11,7 +11,7 @@ public class SkyLayer public bool InFront { get; set; } [XnbProperty] - public float Opacity { get; set; } + public float Opacity { get; set; } = 1f; [XnbProperty] public float FogTint { get; set; } diff --git a/Core/Definitions/Game/TrackedSong/Loop.cs b/Core/Definitions/Game/TrackedSong/Loop.cs index 8f13b21..2b1023d 100644 --- a/Core/Definitions/Game/TrackedSong/Loop.cs +++ b/Core/Definitions/Game/TrackedSong/Loop.cs @@ -5,13 +5,13 @@ public class Loop { [XnbProperty] - public int Duration { get; set; } + public int Duration { get; set; } = 1; [XnbProperty] - public int LoopTimesFrom { get; set; } + public int LoopTimesFrom { get; set; } = 1; [XnbProperty] - public int LoopTimesTo { get; set; } + public int LoopTimesTo { get; set; } = 1; [XnbProperty] public string Name { get; set; } = ""; @@ -26,16 +26,16 @@ public class Loop public int Delay { get; set; } [XnbProperty] - public bool Night { get; set; } + public bool Night { get; set; } = true; [XnbProperty] - public bool Day { get; set; } + public bool Day { get; set; } = true; [XnbProperty] - public bool Dusk { get; set; } + public bool Dusk { get; set; } = true; [XnbProperty] - public bool Dawn { get; set; } + public bool Dawn { get; set; } = true; [XnbProperty] public bool FractionalTime { get; set; } diff --git a/Core/Definitions/Game/TrackedSong/TrackedSong.cs b/Core/Definitions/Game/TrackedSong/TrackedSong.cs index 5e1f50a..cdd9cdd 100644 --- a/Core/Definitions/Game/TrackedSong/TrackedSong.cs +++ b/Core/Definitions/Game/TrackedSong/TrackedSong.cs @@ -5,19 +5,29 @@ public class TrackedSong { [XnbProperty(UseConverter = true)] - public List Loops { get; set; } = new(); + public List? Loops { get; set; } = new(); [XnbProperty] - public string Name { get; set; } = ""; + public string Name { get; set; } = "Untitled"; [XnbProperty] - public int Tempo { get; set; } + public int Tempo { get; set; } = 60; [XnbProperty] - public int TimeSignature { get; set; } + public int TimeSignature { get; set; } = 4; [XnbProperty(UseConverter = true)] - public ShardNotes[] Notes { get; set; } = { }; + public ShardNotes[]? Notes { get; set; } = new[] + { + ShardNotes.C2, + ShardNotes.D2, + ShardNotes.E2, + ShardNotes.F2, + ShardNotes.G2, + ShardNotes.A2, + ShardNotes.B2, + ShardNotes.C3 + }; [XnbProperty(UseConverter = true)] public AssembleChords AssembleChord { get; set; } diff --git a/Core/Definitions/Game/TrileSet/Trile.cs b/Core/Definitions/Game/TrileSet/Trile.cs index 8765d37..914b10f 100644 --- a/Core/Definitions/Game/TrileSet/Trile.cs +++ b/Core/Definitions/Game/TrileSet/Trile.cs @@ -4,6 +4,7 @@ using FEZRepacker.Core.Definitions.Game.Common; using FEZRepacker.Core.Definitions.Game.Graphics; using FEZRepacker.Core.Definitions.Game.XNA; +using FEZRepacker.Core.Helpers; namespace FEZRepacker.Core.Definitions.Game.TrileSet { @@ -12,13 +13,13 @@ namespace FEZRepacker.Core.Definitions.Game.TrileSet public class Trile { [XnbProperty] - public string Name { get; set; } = ""; + public string Name { get; set; } = "Untitled"; [XnbProperty] public string CubemapPath { get; set; } = ""; [XnbProperty] - public Vector3 Size { get; set; } + public Vector3 Size { get; set; } = Vector3.One; [XnbProperty] public Vector3 Offset { get; set; } @@ -36,11 +37,11 @@ public class Trile public bool ForceHugging { get; set; } [XnbProperty(UseConverter = true)] - public IDictionary Faces { get; set; } = null!; + public IDictionary? Faces { get; set; } = new OrderedDictionary(); [JsonIgnore] [XnbProperty(UseConverter = true)] - public IndexedPrimitives Geometry { get; set; } = null!; + public IndexedPrimitives? Geometry { get; set; } [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 0673a8f..83108e8 100644 --- a/Core/Definitions/Game/TrileSet/TrileSet.cs +++ b/Core/Definitions/Game/TrileSet/TrileSet.cs @@ -13,10 +13,10 @@ public class TrileSet public string Name { get; set; } = ""; [XnbProperty(UseConverter = true)] - public IDictionary Triles { get; set; } = new OrderedDictionary(); + public IDictionary? Triles { get; set; } = new OrderedDictionary(); [JsonIgnore] [XnbProperty(UseConverter = true)] - public Texture2D TextureAtlas { get; set; } = new(); + public Texture2D? TextureAtlas { get; set; } } } diff --git a/Core/Definitions/Json/MapTreeJsonModel.cs b/Core/Definitions/Json/MapTreeJsonModel.cs index 8d05763..1dc786a 100644 --- a/Core/Definitions/Json/MapTreeJsonModel.cs +++ b/Core/Definitions/Json/MapTreeJsonModel.cs @@ -7,7 +7,10 @@ public class MapTreeJsonModel : Dictionary, JsonModel() { diff --git a/Tests/TestPacking.cs b/Tests/TestPacking.cs index af3a88a..b08153d 100644 --- a/Tests/TestPacking.cs +++ b/Tests/TestPacking.cs @@ -3,13 +3,21 @@ namespace FEZRepacker.Tests { + // Verifies package data is byte-identical after unpacking and repacking [TestClass] public class TestPacking { + // Rebuilds each configured FEZ package and compares its complete payload [TestMethod] [DynamicData(nameof(TestUtils.PackagePathsTestData), typeof(TestUtils))] - public void RepackAndComparePackage(string packagePath) + public void RepackAndComparePackage(string? packagePath) { + if (packagePath == null) + { + Assert.Inconclusive("Set FEZContentDirPath in a runsettings file to run package integration tests."); + return; + } + var pakData = File.ReadAllBytes(packagePath); using var pakStream = new MemoryStream(pakData); @@ -29,4 +37,4 @@ public void RepackAndComparePackage(string packagePath) Assert.IsTrue(repackData.SequenceEqual(pakData)); } } -} \ No newline at end of file +} diff --git a/Tests/TestUtils.cs b/Tests/TestUtils.cs index 4631472..03a585b 100644 --- a/Tests/TestUtils.cs +++ b/Tests/TestUtils.cs @@ -2,32 +2,47 @@ namespace FEZRepacker.Tests { + // Supplies optional FEZ asset paths to package integration tests [TestClass] public class TestUtils { - public static TestContext Context; + private static TestContext Context { get; set; } = null!; + // Captures the MSTest context without requiring optional integration-test data [AssemblyInitialize] public static void SetupTestContext(TestContext testContext) { Context = testContext; - Assert.IsTrue( - Directory.Exists(GetGameAssetsDirectory()), - "You forgot to put FEZ's Content path into .runsettings file" - ); } - public static IEnumerable PackagePathsTestData => - GetPathsToPackages().Select(name => new object[] { name }); + public static IEnumerable PackagePathsTestData + { + get + { + var assetsDirectory = GetGameAssetsDirectory(); + if (assetsDirectory == null) + { + yield return new object?[] { null }; + yield break; + } + + foreach (var packagePath in GetPathsToPackages(assetsDirectory)) + { + yield return new object?[] { packagePath }; + } + } + } - public static string GetGameAssetsDirectory() + private static string? GetGameAssetsDirectory() { - return Context.Properties["FEZContentDirPath"].ToString(); + var configuredPath = Context.Properties["FEZContentDirPath"]?.ToString(); + return string.IsNullOrWhiteSpace(configuredPath) ? null : + Directory.Exists(configuredPath) ? configuredPath : null; } - public static IEnumerable GetPathsToPackages() + private static IEnumerable GetPathsToPackages(string assetsDirectory) { - return Directory.EnumerateFiles(GetGameAssetsDirectory(), "*.pak", SearchOption.AllDirectories); + return Directory.EnumerateFiles(assetsDirectory, "*.pak", SearchOption.AllDirectories); } } } diff --git a/Tests/TypeContractDefaultsTests.cs b/Tests/TypeContractDefaultsTests.cs new file mode 100644 index 0000000..2e357c2 --- /dev/null +++ b/Tests/TypeContractDefaultsTests.cs @@ -0,0 +1,124 @@ +using FEZRepacker.Core.Definitions.Game.Level; +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; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FEZRepacker.Tests +{ + // Verifies newly constructed repacker models use the same defaults as FEZ + [TestClass] + public class TypeContractDefaultsTests + { + // Constructs a level and checks the FEZ filtering default + [TestMethod] + public void LevelUsesFezDefaults() + { + var level = new Level(); + + Assert.IsTrue(level.HaloFiltering); + } + + // Constructs an art-object instance and checks its transform and settings defaults + [TestMethod] + public void ArtObjectInstanceUsesFezDefaults() + { + var instance = new ArtObjectInstance(); + + Assert.AreEqual(Quaternion.Identity, instance.Rotation); + Assert.AreEqual(Vector3.One, instance.Scale); + Assert.IsNotNull(instance.ActorSettings); + } + + // Constructs NPC models and checks their movement and collection defaults + [TestMethod] + public void NpcModelsUseFezDefaults() + { + var instance = new NpcInstance(); + var metadata = new NpcMetadata(); + + Assert.AreEqual(1.5f, instance.WalkSpeed); + Assert.IsNotNull(instance.Speech); + Assert.AreEqual(0, instance.Speech.Count); + Assert.IsNotNull(instance.Actions); + Assert.AreEqual(0, instance.Actions.Count); + Assert.AreEqual(1.5f, metadata.WalkSpeed); + Assert.IsNotNull(metadata.SoundActions); + Assert.AreEqual(0, metadata.SoundActions.Count); + } + + // Constructs song models and checks FEZ timing, note, and loop defaults + [TestMethod] + public void TrackedSongModelsUseFezDefaults() + { + var song = new TrackedSong(); + var loop = new Loop(); + var expectedNotes = new[] + { + ShardNotes.C2, ShardNotes.D2, ShardNotes.E2, ShardNotes.F2, ShardNotes.G2, ShardNotes.A2, + ShardNotes.B2, ShardNotes.C3 + }; + + Assert.AreEqual("Untitled", song.Name); + Assert.AreEqual(60, song.Tempo); + Assert.AreEqual(4, song.TimeSignature); + CollectionAssert.AreEqual(expectedNotes, song.Notes); + Assert.IsNotNull(song.Loops); + Assert.AreEqual(0, song.Loops.Count); + Assert.AreEqual(1, loop.Duration); + Assert.AreEqual(1, loop.LoopTimesFrom); + Assert.AreEqual(1, loop.LoopTimesTo); + Assert.IsTrue(loop.Day); + Assert.IsTrue(loop.Night); + Assert.IsTrue(loop.Dawn); + Assert.IsTrue(loop.Dusk); + } + + // Constructs sky models and checks their rendering defaults + [TestMethod] + public void SkyModelsUseFezDefaults() + { + var sky = new Sky(); + var layer = new SkyLayer(); + + Assert.AreEqual("Default", sky.Name); + Assert.AreEqual("SkyBack", sky.Background); + Assert.AreEqual(1f, sky.WindSpeed); + Assert.AreEqual(1f, sky.Density); + Assert.AreEqual(0.02f, sky.FogDensity); + Assert.AreEqual(0.5f, sky.LayerBaseHeight); + Assert.AreEqual(1f, sky.CloudsParallax); + Assert.AreEqual(0.7f, sky.ShadowOpacity); + Assert.IsNotNull(sky.Layers); + Assert.IsNotNull(sky.Clouds); + Assert.AreEqual(1f, layer.Opacity); + } + + // Constructs a trile and checks its resource, size, and face defaults + [TestMethod] + public void TrileUsesFezDefaults() + { + var trile = new Trile(); + + Assert.AreEqual("Untitled", trile.Name); + Assert.AreEqual(Vector3.One, trile.Size); + Assert.IsNotNull(trile.Faces); + Assert.AreEqual(0, trile.Faces.Count); + Assert.IsNull(trile.Geometry); + } + + // Deconverts a map model and verifies the converter creates its required root + [TestMethod] + public void MapTreeJsonModelCreatesRoot() + { + var model = new MapTreeJsonModel { [0] = new MapNodeJsonModel() }; + var mapTree = model.Deserialize(); + + Assert.IsNotNull(mapTree.Root); + } + } +} \ No newline at end of file diff --git a/Tests/TypeContractNullabilityTests.cs b/Tests/TypeContractNullabilityTests.cs new file mode 100644 index 0000000..7a008c4 --- /dev/null +++ b/Tests/TypeContractNullabilityTests.cs @@ -0,0 +1,131 @@ +using System.Reflection; +using System.Text.Json; + +using FEZRepacker.Core.Definitions.Game.ArtObject; +using FEZRepacker.Core.Definitions.Game.Common; +using FEZRepacker.Core.Definitions.Game.Graphics; +using FEZRepacker.Core.Definitions.Game.Level; +using FEZRepacker.Core.Definitions.Game.Level.Scripting; +using FEZRepacker.Core.Definitions.Game.MapTree; +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 Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FEZRepacker.Tests +{ + // Verifies object-encoded references are nullable and explicit JSON nulls survive + [TestClass] + public class TypeContractNullabilityTests + { + // Checks nullable metadata for every corrected object-encoded property + [TestMethod] + public void ObjectEncodedPropertiesAreNullable() + { + AssertNullable(nameof(Level.Name)); + AssertNullable(nameof(Level.TrileSetName)); + AssertNullable(nameof(Level.Volumes)); + AssertNullable(nameof(Level.Scripts)); + AssertNullable(nameof(Level.Triles)); + AssertNullable(nameof(Level.ArtObjects)); + AssertNullable(nameof(Level.BackgroundPlanes)); + AssertNullable(nameof(Level.Groups)); + AssertNullable(nameof(Level.NonPlayerCharacters)); + AssertNullable(nameof(Level.Paths)); + AssertNullable(nameof(Level.MutedLoops)); + AssertNullable(nameof(Level.AmbienceTracks)); + + AssertNullable(nameof(NpcActionContent.AnimationName)); + AssertNullable(nameof(NpcActionContent.SoundName)); + AssertNullable(nameof(NpcInstance.Speech)); + AssertNullable(nameof(NpcInstance.Actions)); + AssertNullable(nameof(SpeechLine.Text)); + AssertNullable(nameof(AmbienceTrack.Name)); + AssertNullable(nameof(DotDialogueLine.ResourceText)); + + AssertNullable(nameof(MovementPath.Segments)); + AssertNullable(nameof(TrileGroup.Triles)); + AssertNullable(nameof(TrileInstance.OverlappedTriles)); + AssertNullable(nameof(Volume.Orientations)); + AssertNullable(nameof(VolumeActorSettings.DotDialogue)); + AssertNullable(nameof(ArtObjectActorSettings.InvisibleSides)); + + AssertNullable