Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
SceneDefinitionValidator.cs
2
4
8public static class SceneDefinitionValidator
9{
10 private static readonly string[] StandardGroups =
11 ["WorldStatic", "Actors", "Projectiles", "Triggers"];
12
13 private static readonly string[] StandardProfiles =
14 [CollisionProfileNames.World, CollisionProfileNames.Actor, CollisionProfileNames.Projectile, CollisionProfileNames.Sensor];
15
21 public static IReadOnlyList<string> Validate(SceneDefinition definition)
22 {
23 ArgumentNullException.ThrowIfNull(definition);
24
25 var errors = new List<string>();
26 ValidateTilesheetSources(definition, errors);
27 ValidateAnimationSources(definition, errors);
28
29 var knownGroups = new HashSet<string>(StandardGroups, StringComparer.OrdinalIgnoreCase);
30 var definedGroups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
31
32 foreach (var group in definition.CollisionGroups ?? [])
33 {
34 if (string.IsNullOrWhiteSpace(group))
35 {
36 errors.Add("CollisionGroups contains an empty name.");
37 continue;
38 }
39
40 if (!definedGroups.Add(group))
41 errors.Add($"Collision group '{group}' is duplicated.");
42
43 knownGroups.Add(group);
44 }
45
46 var knownProfiles = new HashSet<string>(StandardProfiles, StringComparer.OrdinalIgnoreCase);
47 var definedProfiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
48
49 foreach (var profile in definition.CollisionProfiles ?? [])
50 {
51 if (profile is null)
52 {
53 errors.Add("CollisionProfiles contains a null entry.");
54 continue;
55 }
56
57 if (string.IsNullOrWhiteSpace(profile.Name))
58 errors.Add("CollisionProfiles contains a profile with an empty name.");
59 else
60 {
61 if (!definedProfiles.Add(profile.Name))
62 errors.Add($"Collision profile '{profile.Name}' is duplicated.");
63 knownProfiles.Add(profile.Name);
64 }
65
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}'.");
70
71 foreach (var group in profile.CollidesWith ?? [])
72 {
73 if (!knownGroups.Contains(group))
74 errors.Add($"Collision profile '{profile.Name}' references unknown CollidesWith group '{group}'.");
75 }
76 }
77
78 var layerIds = new HashSet<string>(StringComparer.Ordinal);
79 int layerIndex = 0;
80 foreach (var layer in definition.Layers ?? [])
81 {
82 if (layer is null)
83 {
84 errors.Add($"Layers contains a null entry at index {layerIndex++}.");
85 continue;
86 }
87
88 var label = string.IsNullOrWhiteSpace(layer.ID)
89 ? $"Layer #{layerIndex}"
90 : $"Layer '{layer.ID}'";
91
92 if (!string.IsNullOrWhiteSpace(layer.ID) && !layerIds.Add(layer.ID))
93 errors.Add($"{label}: duplicate ID.");
94
95 if (layer.Columns < 0 || layer.Rows < 0)
96 errors.Add($"{label}: Columns and Rows cannot be negative.");
97
98 if (layer.TileWidth <= 0 || layer.TileHeight <= 0)
99 errors.Add($"{label}: TileWidth and TileHeight must be positive.");
100
101 if (!float.IsFinite(layer.Parallax))
102 errors.Add($"{label}: Parallax must be finite.");
103
104 if (!Enum.IsDefined(layer.CoordinateSystemType))
105 errors.Add($"{label}: unknown coordinate system type '{layer.CoordinateSystemType}'.");
106
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}'.");
111
112 var coordinates = new HashSet<(int X, int Y)>();
113 foreach (var tile in layer.Tiles ?? [])
114 {
115 if (tile is null)
116 {
117 errors.Add($"{label}: Tiles contains a null entry.");
118 continue;
119 }
120
121 var tileLabel = $"{label}, tile ({tile.X}, {tile.Y})";
122 if (!coordinates.Add((tile.X, tile.Y)))
123 errors.Add($"{tileLabel}: duplicate tile entry.");
124
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.");
127
128 if (!Enum.IsDefined(tile.Transform))
129 errors.Add($"{tileLabel}: unknown tile transform '{tile.Transform}'.");
130
131 if (!Enum.IsDefined(tile.CollisionType))
132 errors.Add($"{tileLabel}: unknown collision type '{tile.CollisionType}'.");
133
134 if (!string.IsNullOrWhiteSpace(tile.CollisionProfileName) &&
135 !knownProfiles.Contains(tile.CollisionProfileName))
136 {
137 errors.Add($"{tileLabel}: unknown collision profile '{tile.CollisionProfileName}'.");
138 }
139
140 if (tile.Frame is { } frame)
141 {
142 if (string.IsNullOrWhiteSpace(frame.Tilesheet))
143 errors.Add($"{tileLabel}: frame Tilesheet cannot be empty.");
144
145 if (string.IsNullOrWhiteSpace(frame.RegionName))
146 errors.Add($"{tileLabel}: frame RegionName cannot be empty.");
147
148 if (frame.XTile < 0 || frame.YTile < 0)
149 errors.Add($"{tileLabel}: frame coordinates cannot be negative.");
150 }
151
152 bool hasAnimationKey =
153 !string.IsNullOrWhiteSpace(tile.AnimationKey);
154
155 if (tile.AnimationKey is not null && !hasAnimationKey)
156 errors.Add($"{tileLabel}: AnimationKey cannot be empty.");
157
158 if (tile.StartAnimation && !hasAnimationKey)
159 errors.Add($"{tileLabel}: StartAnimation requires an AnimationKey.");
160 }
161
162 layerIndex++;
163 }
164
165 return errors;
166 }
167
168 private static void ValidateTilesheetSources(
169 SceneDefinition definition,
170 List<string> errors)
171 {
172 definition.TilesheetSources ??= [];
173 var names = new HashSet<string>(StringComparer.Ordinal);
174
175 for (int i = 0; i < definition.TilesheetSources.Count; i++)
176 {
177 var source = definition.TilesheetSources[i];
178 var label = $"Tilesheet source {i}";
179
180 if (source is null)
181 {
182 errors.Add($"{label}: source definition is null.");
183 continue;
184 }
185
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}'.");
190
191 if (!Enum.IsDefined(source.Kind))
192 {
193 errors.Add($"{label}: unknown source kind '{source.Kind}'.");
194 continue;
195 }
196
197 switch (source.Kind)
198 {
199 case SceneTilesheetSourceKind.LooseDefinitionFile:
200 if (string.IsNullOrWhiteSpace(source.GtsPath))
201 errors.Add($"{label}: loose GTS source path is empty.");
202 break;
203
204 case SceneTilesheetSourceKind.PackedDefinitionFile:
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.");
209 break;
210 }
211 }
212 }
213
214 private static void ValidateAnimationSources(
215 SceneDefinition definition,
216 List<string> errors)
217 {
218 definition.AnimationSources ??= [];
219 var keys = new HashSet<string>(StringComparer.Ordinal);
220
221 for (int i = 0; i < definition.AnimationSources.Count; i++)
222 {
223 var source = definition.AnimationSources[i];
224 var label = $"Animation source {i}";
225
226 if (source is null)
227 {
228 errors.Add($"{label}: source definition is null.");
229 continue;
230 }
231
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}'.");
236
237 if (!Enum.IsDefined(source.Kind))
238 {
239 errors.Add($"{label}: unknown source kind '{source.Kind}'.");
240 continue;
241 }
242
243 switch (source.Kind)
244 {
245 case SceneAnimationSourceKind.LooseDefinitionFile:
246 if (string.IsNullOrWhiteSpace(source.GaniPath))
247 errors.Add($"{label}: loose GANI source path is empty.");
248 break;
249
250 case SceneAnimationSourceKind.PackedDefinitionFile:
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.");
255 break;
256 }
257 }
258 }
259}
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...