12public static class SceneDefinitionSerializer
14 private static readonly JsonSerializerSettings Settings =
new()
16 Formatting = Formatting.Indented,
17 NullValueHandling = NullValueHandling.Ignore,
18 DefaultValueHandling = DefaultValueHandling.Include,
19 MissingMemberHandling = MissingMemberHandling.Ignore
29 if (
string.IsNullOrWhiteSpace(filePath))
30 throw new ArgumentException(
"GSCN file path must be a non-empty string.", nameof(filePath));
32 var fullPath = Path.GetFullPath(filePath);
33 if (!File.Exists(fullPath))
34 throw new FileNotFoundException($
"GSCN file not found: {fullPath}", fullPath);
38 var definition = FromJson(File.ReadAllText(fullPath), fullPath);
39 ApplyDefaultSource(definition, SceneDefinitionSource.LooseDefinitionFile(fullPath));
40 definition.LoadStamp =
new(definition, filePath: fullPath);
43 catch (JsonException ex)
45 throw new InvalidDataException($
"Failed to deserialize GSCN file: {fullPath}", ex);
56 ArgumentNullException.ThrowIfNull(stream);
59 throw new ArgumentException(
"Stream must be readable.", nameof(stream));
61 using var reader =
new StreamReader(stream, leaveOpen:
true);
62 return FromJson(reader.ReadToEnd());
71 public static SceneDefinition Load(AssetsFile assetsFile,
string entryName)
73 ArgumentNullException.ThrowIfNull(assetsFile);
75 if (
string.IsNullOrWhiteSpace(entryName))
76 throw new ArgumentException(
"GSCN asset entry name must be a non-empty string.", nameof(entryName));
78 using var stream = assetsFile.
Get(
AssetTypes.SceneDefinition, entryName)
79 ??
throw new FileNotFoundException($
"GSCN asset entry not found: {entryName}", entryName);
81 var definition = Load(stream);
82 definition.Source = SceneDefinitionSource.None();
83 if (!
string.IsNullOrWhiteSpace(assetsFile.
SourcePath))
85 definition.Source = SceneDefinitionSource.PackedDefinitionFile(
90 definition.LoadStamp =
new(definition, assetsFilePath: assetsFile.
SourcePath, entryName: entryName);
101 if (
string.IsNullOrWhiteSpace(filePath))
102 throw new ArgumentException(
"GSCN file path must be a non-empty string.", nameof(filePath));
104 ArgumentNullException.ThrowIfNull(definition);
106 var fullPath = Path.GetFullPath(filePath);
107 var directory = Path.GetDirectoryName(fullPath);
108 if (!
string.IsNullOrWhiteSpace(directory))
109 Directory.CreateDirectory(directory);
111 var definitionToSave = CreateDefinitionForSaving(definition, fullPath);
112 File.WriteAllText(fullPath, ToJson(definitionToSave));
120 public static void Save(
string filePath, Scene scene)
122 ArgumentNullException.ThrowIfNull(scene);
123 Save(filePath, FromScene(scene));
132 FromJson(json, sourceDescription:
null);
141 ArgumentNullException.ThrowIfNull(definition);
142 return JsonConvert.SerializeObject(definition, Settings);
152 ArgumentNullException.ThrowIfNull(scene);
157 CollisionGroups = scene.CollisionGroups.GetGroupNames().ToList(),
158 CollisionProfiles = scene.CollisionProfiles.GetProfileNames()
161 var profile = scene.CollisionProfiles.Get(name);
162 return new SceneCollisionProfileDefinition
165 CollisionGroup = profile.CollisionGroup,
166 CollidesWith = profile.CollidesWith.ToList(),
167 CollidesWithAll = profile.CollidesWithAll
171 Source = SceneDefinitionSource.Generated()
174 foreach (var layer
in scene.SceneLayers)
175 definition.Layers.Add(CreateLayerDefinition(layer));
185 public static string ToJson(Scene scene) => ToJson(FromScene(scene));
196 ArgumentNullException.ThrowIfNull(definition);
198 var errors = SceneDefinitionValidator.Validate(definition);
199 if (errors.Count != 0)
201 throw new InvalidDataException(
202 "GSCN definition is invalid:" + Environment.NewLine +
203 string.Join(Environment.NewLine, errors.Select(error =>
"- " + error)));
206 var scene =
new Scene();
210 if (!
string.IsNullOrWhiteSpace(definition.ID))
211 scene.ID = definition.ID;
213 foreach (var group
in definition.CollisionGroups ?? [])
214 scene.CollisionGroups.Define(group);
216 foreach (var profile
in definition.CollisionProfiles ?? [])
218 scene.CollisionProfiles.Define(
220 profile.CollisionGroup,
221 profile.CollidesWith,
222 profile.CollidesWithAll);
225 foreach (var layerDefinition
in definition.Layers ?? [])
226 MaterializeLayer(scene, layerDefinition);
229 if (definition.ID == scene.ID && (definition.Layers ?? []).All(layer => !
string.IsNullOrWhiteSpace(layer.ID)))
230 scene.DefinitionProvenance = definition.LoadStamp?.Materialized(definition, () => FromScene(scene));
245 public static Scene LoadScene(
string filePath) => ToScene(Load(filePath));
253 public static Scene LoadScene(AssetsFile assetsFile,
string entryName) =>
254 ToScene(Load(assetsFile, entryName));
256 private static SceneDefinition FromJson(
string json,
string? sourceDescription)
258 if (
string.IsNullOrWhiteSpace(json))
259 throw new ArgumentException(
"GSCN JSON content must be a non-empty string.", nameof(json));
263 var definition = JsonConvert.DeserializeObject<
SceneDefinition>(json, Settings);
264 if (definition is
null)
266 var source =
string.IsNullOrWhiteSpace(sourceDescription)
267 ?
"GSCN JSON content"
270 throw new InvalidDataException($
"Failed to deserialize {source}. Result was null.");
273 definition.CollisionGroups ??= [];
274 definition.CollisionProfiles ??= [];
275 definition.TilesheetSources ??= [];
276 definition.AnimationSources ??= [];
277 definition.Layers ??= [];
279 for (
int i = 0; i < definition.TilesheetSources.Count; i++)
281 if (definition.TilesheetSources[i] is
null)
282 throw new InvalidDataException($
"GSCN TilesheetSources cannot contain a null entry at index {i}.");
285 for (
int i = 0; i < definition.AnimationSources.Count; i++)
287 if (definition.AnimationSources[i] is
null)
288 throw new InvalidDataException($
"GSCN AnimationSources cannot contain a null entry at index {i}.");
291 foreach (var profile
in definition.CollisionProfiles)
294 throw new InvalidDataException(
"GSCN CollisionProfiles cannot contain null entries.");
295 profile.CollidesWith ??= [];
298 foreach (var layer
in definition.Layers)
301 throw new InvalidDataException(
"GSCN Layers cannot contain null entries.");
307 catch (JsonException ex)
309 var source =
string.IsNullOrWhiteSpace(sourceDescription)
310 ?
"GSCN JSON content"
313 throw new InvalidDataException($
"Failed to deserialize {source}.", ex);
317 private static SceneLayerDefinition CreateLayerDefinition(SceneLayer layer)
319 var definition =
new SceneLayerDefinition
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
338 for (
int x = 0; x < layer.GridColumnCount; x++)
340 for (
int y = 0; y < layer.GridRowCount; y++)
342 var tile = layer[x, y]!;
343 definition.Tiles.Add(CreateTileDefinition(tile, x, y));
350 private static SceneLayerTileDefinition CreateTileDefinition(
355 SceneFrameDefinition? frame =
null;
356 if (tile.CurrentFrame.Tilesheet is not
null)
358 frame =
new SceneFrameDefinition
360 Tilesheet = tile.CurrentFrame.Tilesheet.Name,
361 RegionName = tile.CurrentFrame.RegionName,
362 XTile = tile.CurrentFrame.XTile,
363 YTile = tile.CurrentFrame.YTile
367 return new SceneLayerTileDefinition
372 Nickname = tile.Nickname,
373 Visible = tile.Visible,
374 Transform = tile.Transform,
376 EnableAnimator = tile.EnableAnimator,
377 AnimationKey = tile.EnableAnimator
378 ? tile.TileAnimator.CurrentCycle?.CycleKey
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
391 private static void MaterializeLayer(
393 SceneLayerDefinition definition)
395 var layer = scene.AddLayer(
398 definition.TileWidth,
399 definition.TileHeight,
402 definition.CoordinateSystemType);
404 if (!
string.IsNullOrWhiteSpace(definition.ID))
405 layer.ID = definition.ID;
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;
415 foreach (var tileDefinition
in definition.Tiles ?? [])
417 var tile = layer[tileDefinition.X, tileDefinition.Y]!;
418 MaterializeTile(tile, tileDefinition);
422 private static void MaterializeTile(
424 SceneLayerTileDefinition definition)
426 if (definition.Id != Guid.Empty)
427 tile.Id = definition.Id;
429 tile.Nickname = definition.Nickname;
431 if (definition.Frame is { } frame)
432 tile.CurrentFrame = ResolveFrame(frame);
434 tile.Visible = definition.Visible;
435 tile.Transform = definition.Transform;
436 tile.EnableFog = definition.EnableFog;
440 tile.AdjustCollisionArea = definition.AdjustCollisionArea;
441 tile.CollisionType = definition.CollisionType;
443 if (!
string.IsNullOrWhiteSpace(definition.CollisionProfileName))
444 tile.SetCollisionProfile(definition.CollisionProfileName);
446 tile.AdjustCollisionAreaByFrame = definition.AdjustCollisionAreaByFrame;
447 tile.CollisionTypeByFrame = definition.CollisionTypeByFrame;
449 var animationKey =
string.IsNullOrWhiteSpace(definition.AnimationKey)
451 : definition.AnimationKey.Trim();
453 tile.EnableAnimator =
454 definition.EnableAnimator ||
455 animationKey is not
null;
457 if (animationKey is not
null)
459 var cycle = Cycle.GetAnimationCycle(animationKey)
460 ??
throw new InvalidDataException(
461 $
"GSCN references animation '{animationKey}', but no matching GANI/cycle is registered.");
463 tile.TileAnimator.CurrentCycle = cycle;
465 if (definition.StartAnimation)
466 tile.TileAnimator.StartAnimation();
470 private static Frame ResolveFrame(SceneFrameDefinition definition)
472 var tilesheet = TilesheetRegistry.Instance.GetOrNull(definition.Tilesheet)
473 ??
throw new InvalidDataException(
474 $
"GSCN references tilesheet '{definition.Tilesheet}', but it is not registered.");
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.");
480 if (definition.XTile < 0 ||
481 definition.YTile < 0 ||
482 definition.XTile >= region.Columns ||
483 definition.YTile >= region.Rows)
485 throw new InvalidDataException(
486 $
"GSCN frame ({definition.XTile}, {definition.YTile}) is outside region '{definition.RegionName}' on tilesheet '{definition.Tilesheet}'.");
489 return tilesheet.GetFrame(
490 definition.RegionName,
495 private static void ApplyDefaultSource(
497 SceneDefinitionSource source)
500 definition.Source = source;
510 var clone = FromJson(ToJson(definition));
511 clone.Source = SceneDefinitionSource.LooseDefinitionFile(fullPath);
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.
@ SceneDefinition
Represents a scene definition file (.gscn) for Gondwana.Scenes.GSCN.SceneDefinition.
SceneDefinitionSourceKind
Indicates where a scene definition came from.