8public static class SceneDefinitionValidator
10 private static readonly
string[] StandardGroups =
11 [
"WorldStatic",
"Actors",
"Projectiles",
"Triggers"];
13 private static readonly
string[] StandardProfiles =
14 [CollisionProfileNames.World, CollisionProfileNames.Actor, CollisionProfileNames.Projectile, CollisionProfileNames.Sensor];
21 public static IReadOnlyList<string> Validate(SceneDefinition definition)
23 ArgumentNullException.ThrowIfNull(definition);
25 var errors =
new List<string>();
26 ValidateTilesheetSources(definition, errors);
27 ValidateAnimationSources(definition, errors);
29 var knownGroups =
new HashSet<string>(StandardGroups, StringComparer.OrdinalIgnoreCase);
30 var definedGroups =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
32 foreach (var group
in definition.CollisionGroups ?? [])
34 if (
string.IsNullOrWhiteSpace(group))
36 errors.Add(
"CollisionGroups contains an empty name.");
40 if (!definedGroups.Add(group))
41 errors.Add($
"Collision group '{group}' is duplicated.");
43 knownGroups.Add(group);
46 var knownProfiles =
new HashSet<string>(StandardProfiles, StringComparer.OrdinalIgnoreCase);
47 var definedProfiles =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
49 foreach (var profile
in definition.CollisionProfiles ?? [])
53 errors.Add(
"CollisionProfiles contains a null entry.");
57 if (
string.IsNullOrWhiteSpace(profile.Name))
58 errors.Add(
"CollisionProfiles contains a profile with an empty name.");
61 if (!definedProfiles.Add(profile.Name))
62 errors.Add($
"Collision profile '{profile.Name}' is duplicated.");
63 knownProfiles.Add(profile.Name);
66 if (
string.IsNullOrWhiteSpace(profile.CollisionGroup))
67 errors.Add($
"Collision profile '{profile.Name}' has an empty CollisionGroup.");
68 else if (!knownGroups.Contains(profile.CollisionGroup))
69 errors.Add($
"Collision profile '{profile.Name}' references unknown group '{profile.CollisionGroup}'.");
71 foreach (var group
in profile.CollidesWith ?? [])
73 if (!knownGroups.Contains(group))
74 errors.Add($
"Collision profile '{profile.Name}' references unknown CollidesWith group '{group}'.");
78 var layerIds =
new HashSet<string>(StringComparer.Ordinal);
80 foreach (var layer
in definition.Layers ?? [])
84 errors.Add($
"Layers contains a null entry at index {layerIndex++}.");
88 var label =
string.IsNullOrWhiteSpace(layer.ID)
89 ? $
"Layer #{layerIndex}"
90 : $
"Layer '{layer.ID}'";
92 if (!
string.IsNullOrWhiteSpace(layer.ID) && !layerIds.Add(layer.ID))
93 errors.Add($
"{label}: duplicate ID.");
95 if (layer.Columns < 0 || layer.Rows < 0)
96 errors.Add($
"{label}: Columns and Rows cannot be negative.");
98 if (layer.TileWidth <= 0 || layer.TileHeight <= 0)
99 errors.Add($
"{label}: TileWidth and TileHeight must be positive.");
101 if (!
float.IsFinite(layer.Parallax))
102 errors.Add($
"{label}: Parallax must be finite.");
104 if (!Enum.IsDefined(layer.CoordinateSystemType))
105 errors.Add($
"{label}: unknown coordinate system type '{layer.CoordinateSystemType}'.");
107 if (
string.IsNullOrWhiteSpace(layer.DefaultTileCollisionProfile))
108 errors.Add($
"{label}: DefaultTileCollisionProfile cannot be empty.");
109 else if (!knownProfiles.Contains(layer.DefaultTileCollisionProfile))
110 errors.Add($
"{label}: unknown default collision profile '{layer.DefaultTileCollisionProfile}'.");
112 var coordinates =
new HashSet<(int X, int Y)>();
113 foreach (var tile
in layer.Tiles ?? [])
117 errors.Add($
"{label}: Tiles contains a null entry.");
121 var tileLabel = $
"{label}, tile ({tile.X}, {tile.Y})";
122 if (!coordinates.Add((tile.X, tile.Y)))
123 errors.Add($
"{tileLabel}: duplicate tile entry.");
125 if (tile.X < 0 || tile.Y < 0 || tile.X >= layer.Columns || tile.Y >= layer.Rows)
126 errors.Add($
"{tileLabel}: coordinates are outside the layer grid.");
128 if (!Enum.IsDefined(tile.Transform))
129 errors.Add($
"{tileLabel}: unknown tile transform '{tile.Transform}'.");
131 if (!Enum.IsDefined(tile.CollisionType))
132 errors.Add($
"{tileLabel}: unknown collision type '{tile.CollisionType}'.");
134 if (!
string.IsNullOrWhiteSpace(tile.CollisionProfileName) &&
135 !knownProfiles.Contains(tile.CollisionProfileName))
137 errors.Add($
"{tileLabel}: unknown collision profile '{tile.CollisionProfileName}'.");
140 if (tile.Frame is { } frame)
142 if (
string.IsNullOrWhiteSpace(frame.Tilesheet))
143 errors.Add($
"{tileLabel}: frame Tilesheet cannot be empty.");
145 if (
string.IsNullOrWhiteSpace(frame.RegionName))
146 errors.Add($
"{tileLabel}: frame RegionName cannot be empty.");
148 if (frame.XTile < 0 || frame.YTile < 0)
149 errors.Add($
"{tileLabel}: frame coordinates cannot be negative.");
152 bool hasAnimationKey =
153 !
string.IsNullOrWhiteSpace(tile.AnimationKey);
155 if (tile.AnimationKey is not
null && !hasAnimationKey)
156 errors.Add($
"{tileLabel}: AnimationKey cannot be empty.");
158 if (tile.StartAnimation && !hasAnimationKey)
159 errors.Add($
"{tileLabel}: StartAnimation requires an AnimationKey.");
168 private static void ValidateTilesheetSources(
169 SceneDefinition definition,
172 definition.TilesheetSources ??= [];
173 var names =
new HashSet<string>(StringComparer.Ordinal);
175 for (
int i = 0; i < definition.TilesheetSources.Count; i++)
177 var source = definition.TilesheetSources[i];
178 var label = $
"Tilesheet source {i}";
182 errors.Add($
"{label}: source definition is null.");
186 if (
string.IsNullOrWhiteSpace(source.Tilesheet))
187 errors.Add($
"{label}: tilesheet name is empty.");
188 else if (!names.Add(source.Tilesheet))
189 errors.Add($
"{label}: duplicate source for tilesheet '{source.Tilesheet}'.");
191 if (!Enum.IsDefined(source.Kind))
193 errors.Add($
"{label}: unknown source kind '{source.Kind}'.");
200 if (
string.IsNullOrWhiteSpace(source.GtsPath))
201 errors.Add($
"{label}: loose GTS source path is empty.");
205 if (
string.IsNullOrWhiteSpace(source.AssetsFilePath))
206 errors.Add($
"{label}: assets file path is empty.");
207 if (
string.IsNullOrWhiteSpace(source.AssetEntryName))
208 errors.Add($
"{label}: packed GTS entry name is empty.");
214 private static void ValidateAnimationSources(
215 SceneDefinition definition,
218 definition.AnimationSources ??= [];
219 var keys =
new HashSet<string>(StringComparer.Ordinal);
221 for (
int i = 0; i < definition.AnimationSources.Count; i++)
223 var source = definition.AnimationSources[i];
224 var label = $
"Animation source {i}";
228 errors.Add($
"{label}: source definition is null.");
232 if (
string.IsNullOrWhiteSpace(source.AnimationKey))
233 errors.Add($
"{label}: animation key is empty.");
234 else if (!keys.Add(source.AnimationKey))
235 errors.Add($
"{label}: duplicate source for animation '{source.AnimationKey}'.");
237 if (!Enum.IsDefined(source.Kind))
239 errors.Add($
"{label}: unknown source kind '{source.Kind}'.");
246 if (
string.IsNullOrWhiteSpace(source.GaniPath))
247 errors.Add($
"{label}: loose GANI source path is empty.");
251 if (
string.IsNullOrWhiteSpace(source.AssetsFilePath))
252 errors.Add($
"{label}: assets file path is empty.");
253 if (
string.IsNullOrWhiteSpace(source.AssetEntryName))
254 errors.Add($
"{label}: packed GANI entry name is empty.");
SceneAnimationSourceKind
Identifies how a GSCN definition can locate one of its logical GANI dependencies for authoring and to...
SceneTilesheetSourceKind
Identifies how a GSCN definition can locate one of its logical GTS dependencies for authoring and too...