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
SceneDefinitionSerializer.cs
5using Newtonsoft.Json;
6
8
12public static class SceneDefinitionSerializer
13{
14 private static readonly JsonSerializerSettings Settings = new()
15 {
16 Formatting = Formatting.Indented,
17 NullValueHandling = NullValueHandling.Ignore,
18 DefaultValueHandling = DefaultValueHandling.Include,
19 MissingMemberHandling = MissingMemberHandling.Ignore
20 };
21
27 public static SceneDefinition Load(string filePath)
28 {
29 if (string.IsNullOrWhiteSpace(filePath))
30 throw new ArgumentException("GSCN file path must be a non-empty string.", nameof(filePath));
31
32 var fullPath = Path.GetFullPath(filePath);
33 if (!File.Exists(fullPath))
34 throw new FileNotFoundException($"GSCN file not found: {fullPath}", fullPath);
35
36 try
37 {
38 var definition = FromJson(File.ReadAllText(fullPath), fullPath);
39 ApplyDefaultSource(definition, SceneDefinitionSource.LooseDefinitionFile(fullPath));
40 definition.LoadStamp = new(definition, filePath: fullPath);
41 return definition;
42 }
43 catch (JsonException ex)
44 {
45 throw new InvalidDataException($"Failed to deserialize GSCN file: {fullPath}", ex);
46 }
47 }
48
54 public static SceneDefinition Load(Stream stream)
55 {
56 ArgumentNullException.ThrowIfNull(stream);
57
58 if (!stream.CanRead)
59 throw new ArgumentException("Stream must be readable.", nameof(stream));
60
61 using var reader = new StreamReader(stream, leaveOpen: true);
62 return FromJson(reader.ReadToEnd());
63 }
64
71 public static SceneDefinition Load(AssetsFile assetsFile, string entryName)
72 {
73 ArgumentNullException.ThrowIfNull(assetsFile);
74
75 if (string.IsNullOrWhiteSpace(entryName))
76 throw new ArgumentException("GSCN asset entry name must be a non-empty string.", nameof(entryName));
77
78 using var stream = assetsFile.Get(AssetTypes.SceneDefinition, entryName)
79 ?? throw new FileNotFoundException($"GSCN asset entry not found: {entryName}", entryName);
80
81 var definition = Load(stream);
82 definition.Source = SceneDefinitionSource.None();
83 if (!string.IsNullOrWhiteSpace(assetsFile.SourcePath))
84 {
85 definition.Source = SceneDefinitionSource.PackedDefinitionFile(
86 assetsFile.SourcePath,
87 entryName);
88 }
89
90 definition.LoadStamp = new(definition, assetsFilePath: assetsFile.SourcePath, entryName: entryName);
91 return definition;
92 }
93
99 public static void Save(string filePath, SceneDefinition definition)
100 {
101 if (string.IsNullOrWhiteSpace(filePath))
102 throw new ArgumentException("GSCN file path must be a non-empty string.", nameof(filePath));
103
104 ArgumentNullException.ThrowIfNull(definition);
105
106 var fullPath = Path.GetFullPath(filePath);
107 var directory = Path.GetDirectoryName(fullPath);
108 if (!string.IsNullOrWhiteSpace(directory))
109 Directory.CreateDirectory(directory);
110
111 var definitionToSave = CreateDefinitionForSaving(definition, fullPath);
112 File.WriteAllText(fullPath, ToJson(definitionToSave));
113 }
114
120 public static void Save(string filePath, Scene scene)
121 {
122 ArgumentNullException.ThrowIfNull(scene);
123 Save(filePath, FromScene(scene));
124 }
125
131 public static SceneDefinition FromJson(string json) =>
132 FromJson(json, sourceDescription: null);
133
139 public static string ToJson(SceneDefinition definition)
140 {
141 ArgumentNullException.ThrowIfNull(definition);
142 return JsonConvert.SerializeObject(definition, Settings);
143 }
144
150 public static SceneDefinition FromScene(Scene scene)
151 {
152 ArgumentNullException.ThrowIfNull(scene);
153
154 var definition = new SceneDefinition
155 {
156 ID = scene.ID,
157 CollisionGroups = scene.CollisionGroups.GetGroupNames().ToList(),
158 CollisionProfiles = scene.CollisionProfiles.GetProfileNames()
159 .Select(name =>
160 {
161 var profile = scene.CollisionProfiles.Get(name);
162 return new SceneCollisionProfileDefinition
163 {
164 Name = profile.Name,
165 CollisionGroup = profile.CollisionGroup,
166 CollidesWith = profile.CollidesWith.ToList(),
167 CollidesWithAll = profile.CollidesWithAll
168 };
169 })
170 .ToList(),
171 Source = SceneDefinitionSource.Generated()
172 };
173
174 foreach (var layer in scene.SceneLayers)
175 definition.Layers.Add(CreateLayerDefinition(layer));
176
177 return definition;
178 }
179
185 public static string ToJson(Scene scene) => ToJson(FromScene(scene));
186
194 public static Scene ToScene(SceneDefinition definition)
195 {
196 ArgumentNullException.ThrowIfNull(definition);
197
198 var errors = SceneDefinitionValidator.Validate(definition);
199 if (errors.Count != 0)
200 {
201 throw new InvalidDataException(
202 "GSCN definition is invalid:" + Environment.NewLine +
203 string.Join(Environment.NewLine, errors.Select(error => "- " + error)));
204 }
205
206 var scene = new Scene();
207
208 try
209 {
210 if (!string.IsNullOrWhiteSpace(definition.ID))
211 scene.ID = definition.ID;
212
213 foreach (var group in definition.CollisionGroups ?? [])
214 scene.CollisionGroups.Define(group);
215
216 foreach (var profile in definition.CollisionProfiles ?? [])
217 {
218 scene.CollisionProfiles.Define(
219 profile.Name,
220 profile.CollisionGroup,
221 profile.CollidesWith,
222 profile.CollidesWithAll);
223 }
224
225 foreach (var layerDefinition in definition.Layers ?? [])
226 MaterializeLayer(scene, layerDefinition);
227
228 // Generated scene/layer IDs would change on replay and break external bindings.
229 if (definition.ID == scene.ID && (definition.Layers ?? []).All(layer => !string.IsNullOrWhiteSpace(layer.ID)))
230 scene.DefinitionProvenance = definition.LoadStamp?.Materialized(definition, () => FromScene(scene));
231 return scene;
232 }
233 catch
234 {
235 scene.Dispose();
236 throw;
237 }
238 }
239
245 public static Scene LoadScene(string filePath) => ToScene(Load(filePath));
246
253 public static Scene LoadScene(AssetsFile assetsFile, string entryName) =>
254 ToScene(Load(assetsFile, entryName));
255
256 private static SceneDefinition FromJson(string json, string? sourceDescription)
257 {
258 if (string.IsNullOrWhiteSpace(json))
259 throw new ArgumentException("GSCN JSON content must be a non-empty string.", nameof(json));
260
261 try
262 {
263 var definition = JsonConvert.DeserializeObject<SceneDefinition>(json, Settings);
264 if (definition is null)
265 {
266 var source = string.IsNullOrWhiteSpace(sourceDescription)
267 ? "GSCN JSON content"
268 : sourceDescription;
269
270 throw new InvalidDataException($"Failed to deserialize {source}. Result was null.");
271 }
272
273 definition.CollisionGroups ??= [];
274 definition.CollisionProfiles ??= [];
275 definition.TilesheetSources ??= [];
276 definition.AnimationSources ??= [];
277 definition.Layers ??= [];
278
279 for (int i = 0; i < definition.TilesheetSources.Count; i++)
280 {
281 if (definition.TilesheetSources[i] is null)
282 throw new InvalidDataException($"GSCN TilesheetSources cannot contain a null entry at index {i}.");
283 }
284
285 for (int i = 0; i < definition.AnimationSources.Count; i++)
286 {
287 if (definition.AnimationSources[i] is null)
288 throw new InvalidDataException($"GSCN AnimationSources cannot contain a null entry at index {i}.");
289 }
290
291 foreach (var profile in definition.CollisionProfiles)
292 {
293 if (profile is null)
294 throw new InvalidDataException("GSCN CollisionProfiles cannot contain null entries.");
295 profile.CollidesWith ??= [];
296 }
297
298 foreach (var layer in definition.Layers)
299 {
300 if (layer is null)
301 throw new InvalidDataException("GSCN Layers cannot contain null entries.");
302 layer.Tiles ??= [];
303 }
304
305 return definition;
306 }
307 catch (JsonException ex)
308 {
309 var source = string.IsNullOrWhiteSpace(sourceDescription)
310 ? "GSCN JSON content"
311 : sourceDescription;
312
313 throw new InvalidDataException($"Failed to deserialize {source}.", ex);
314 }
315 }
316
317 private static SceneLayerDefinition CreateLayerDefinition(SceneLayer layer)
318 {
319 var definition = new SceneLayerDefinition
320 {
321 ID = layer.ID,
322 CoordinateSystemType = layer.CoordinateSystemType,
323 Columns = layer.GridColumnCount,
324 Rows = layer.GridRowCount,
325 TileWidth = layer.TileWidth,
326 TileHeight = layer.TileHeight,
327 ZOrder = layer.ZOrder,
328 Parallax = layer.Parallax,
329 Visible = layer.Visible,
330 WrapHorizontally = layer.WrapHorizontally,
331 WrapVertically = layer.WrapVertically,
332 ShowGridLines = layer.ShowGridLines,
333 ShowCollisionBoxes = layer.ShowCollisionBoxes,
334 OriginPx = layer.OriginPx,
335 DefaultTileCollisionProfile = layer.DefaultTileCollisionProfile
336 };
337
338 for (int x = 0; x < layer.GridColumnCount; x++)
339 {
340 for (int y = 0; y < layer.GridRowCount; y++)
341 {
342 var tile = layer[x, y]!;
343 definition.Tiles.Add(CreateTileDefinition(tile, x, y));
344 }
345 }
346
347 return definition;
348 }
349
350 private static SceneLayerTileDefinition CreateTileDefinition(
351 SceneLayerTile tile,
352 int x,
353 int y)
354 {
355 SceneFrameDefinition? frame = null;
356 if (tile.CurrentFrame.Tilesheet is not null)
357 {
358 frame = new SceneFrameDefinition
359 {
360 Tilesheet = tile.CurrentFrame.Tilesheet.Name,
361 RegionName = tile.CurrentFrame.RegionName,
362 XTile = tile.CurrentFrame.XTile,
363 YTile = tile.CurrentFrame.YTile
364 };
365 }
366
367 return new SceneLayerTileDefinition
368 {
369 X = x,
370 Y = y,
371 Id = tile.Id,
372 Nickname = tile.Nickname,
373 Visible = tile.Visible,
374 Transform = tile.Transform,
375 Frame = frame,
376 EnableAnimator = tile.EnableAnimator,
377 AnimationKey = tile.EnableAnimator
378 ? tile.TileAnimator.CurrentCycle?.CycleKey
379 : null,
380 StartAnimation = tile.EnableAnimator &&
381 tile.TileAnimator.IsCycling,
382 EnableFog = tile.EnableFog,
383 AdjustCollisionAreaByFrame = tile.AdjustCollisionAreaByFrame,
384 AdjustCollisionArea = tile.AdjustCollisionArea,
385 CollisionType = tile.CollisionType,
386 CollisionTypeByFrame = tile.CollisionTypeByFrame,
387 CollisionProfileName = tile.CollisionProfileName
388 };
389 }
390
391 private static void MaterializeLayer(
392 Scene scene,
393 SceneLayerDefinition definition)
394 {
395 var layer = scene.AddLayer(
396 definition.Columns,
397 definition.Rows,
398 definition.TileWidth,
399 definition.TileHeight,
400 definition.ZOrder,
401 definition.Parallax,
402 definition.CoordinateSystemType);
403
404 if (!string.IsNullOrWhiteSpace(definition.ID))
405 layer.ID = definition.ID;
406
407 layer.Visible = definition.Visible;
408 layer.WrapHorizontally = definition.WrapHorizontally;
409 layer.WrapVertically = definition.WrapVertically;
410 layer.ShowGridLines = definition.ShowGridLines;
411 layer.ShowCollisionBoxes = definition.ShowCollisionBoxes;
412 layer.OriginPx = definition.OriginPx;
413 layer.DefaultTileCollisionProfile = definition.DefaultTileCollisionProfile;
414
415 foreach (var tileDefinition in definition.Tiles ?? [])
416 {
417 var tile = layer[tileDefinition.X, tileDefinition.Y]!;
418 MaterializeTile(tile, tileDefinition);
419 }
420 }
421
422 private static void MaterializeTile(
423 SceneLayerTile tile,
424 SceneLayerTileDefinition definition)
425 {
426 if (definition.Id != Guid.Empty)
427 tile.Id = definition.Id;
428
429 tile.Nickname = definition.Nickname;
430
431 if (definition.Frame is { } frame)
432 tile.CurrentFrame = ResolveFrame(frame);
433
434 tile.Visible = definition.Visible;
435 tile.Transform = definition.Transform;
436 tile.EnableFog = definition.EnableFog;
437
438 // Apply explicit values first. Enabling a *ByFrame flag immediately
439 // derives the effective value from CurrentFrame, so those flags must be last.
440 tile.AdjustCollisionArea = definition.AdjustCollisionArea;
441 tile.CollisionType = definition.CollisionType;
442
443 if (!string.IsNullOrWhiteSpace(definition.CollisionProfileName))
444 tile.SetCollisionProfile(definition.CollisionProfileName);
445
446 tile.AdjustCollisionAreaByFrame = definition.AdjustCollisionAreaByFrame;
447 tile.CollisionTypeByFrame = definition.CollisionTypeByFrame;
448
449 var animationKey = string.IsNullOrWhiteSpace(definition.AnimationKey)
450 ? null
451 : definition.AnimationKey.Trim();
452
453 tile.EnableAnimator =
454 definition.EnableAnimator ||
455 animationKey is not null;
456
457 if (animationKey is not null)
458 {
459 var cycle = Cycle.GetAnimationCycle(animationKey)
460 ?? throw new InvalidDataException(
461 $"GSCN references animation '{animationKey}', but no matching GANI/cycle is registered.");
462
463 tile.TileAnimator.CurrentCycle = cycle;
464
465 if (definition.StartAnimation)
466 tile.TileAnimator.StartAnimation();
467 }
468 }
469
470 private static Frame ResolveFrame(SceneFrameDefinition definition)
471 {
472 var tilesheet = TilesheetRegistry.Instance.GetOrNull(definition.Tilesheet)
473 ?? throw new InvalidDataException(
474 $"GSCN references tilesheet '{definition.Tilesheet}', but it is not registered.");
475
476 var region = tilesheet.GetRegion(definition.RegionName)
477 ?? throw new InvalidDataException(
478 $"GSCN references region '{definition.RegionName}' on tilesheet '{definition.Tilesheet}', but it does not exist.");
479
480 if (definition.XTile < 0 ||
481 definition.YTile < 0 ||
482 definition.XTile >= region.Columns ||
483 definition.YTile >= region.Rows)
484 {
485 throw new InvalidDataException(
486 $"GSCN frame ({definition.XTile}, {definition.YTile}) is outside region '{definition.RegionName}' on tilesheet '{definition.Tilesheet}'.");
487 }
488
489 return tilesheet.GetFrame(
490 definition.RegionName,
491 definition.XTile,
492 definition.YTile);
493 }
494
495 private static void ApplyDefaultSource(
496 SceneDefinition definition,
497 SceneDefinitionSource source)
498 {
499 if (definition.Source.Kind == SceneDefinitionSourceKind.None)
500 definition.Source = source;
501 }
502
503 private static SceneDefinition CreateDefinitionForSaving(
504 SceneDefinition definition,
505 string fullPath)
506 {
507 if (definition.Source.Kind != SceneDefinitionSourceKind.None)
508 return definition;
509
510 var clone = FromJson(ToJson(definition));
511 clone.Source = SceneDefinitionSource.LooseDefinitionFile(fullPath);
512 return clone;
513 }
514}
string? SourcePath
Gets the archive's reconstructible origin, including an optional stream origin.
Stream? Get(AssetTypes type, string name)
Retrieves a stream for the specified asset type and name.
AssetTypes
Defines the types of assets that can be stored and managed by the engine.
Definition AssetTypes.cs:8
@ SceneDefinition
Represents a scene definition file (.gscn) for Gondwana.Scenes.GSCN.SceneDefinition.
Definition AssetTypes.cs:52
SceneDefinitionSourceKind
Indicates where a scene definition came from.
@ Scene
Displays scene layer, grid-cell, and active tile-animation counts.