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
AnimationDefinitionSerializer.cs
3using Newtonsoft.Json;
4
6
10public static class AnimationDefinitionSerializer
11{
12 private static readonly JsonSerializerSettings Settings = new()
13 {
14 Formatting = Formatting.Indented,
15 NullValueHandling = NullValueHandling.Ignore,
16 DefaultValueHandling = DefaultValueHandling.Include,
17 MissingMemberHandling = MissingMemberHandling.Ignore
18 };
19
25 public static AnimationDefinition Load(string filePath)
26 {
27 if (string.IsNullOrWhiteSpace(filePath))
28 throw new ArgumentException("GANI file path must be a non-empty string.", nameof(filePath));
29
30 var fullPath = Path.GetFullPath(filePath);
31 if (!File.Exists(fullPath))
32 throw new FileNotFoundException($"GANI file not found: {fullPath}", fullPath);
33
34 try
35 {
36 var definition = FromJson(File.ReadAllText(fullPath), fullPath);
37 ApplyDefaultSource(
38 definition,
39 AnimationDefinitionSource.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 GANI file: {fullPath}", ex);
46 }
47 }
48
54 public static AnimationDefinition 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 AnimationDefinition Load(AssetsFile assetsFile, string entryName)
72 {
73 ArgumentNullException.ThrowIfNull(assetsFile);
74
75 if (string.IsNullOrWhiteSpace(entryName))
76 throw new ArgumentException("GANI asset entry name must be a non-empty string.", nameof(entryName));
77
78 using var stream = assetsFile.Get(AssetTypes.AnimationDefinition, entryName)
79 ?? throw new FileNotFoundException($"GANI asset entry not found: {entryName}", entryName);
80
81 var definition = Load(stream);
82 definition.Source = AnimationDefinitionSource.None();
83 if (!string.IsNullOrWhiteSpace(assetsFile.SourcePath))
84 {
85 definition.Source = AnimationDefinitionSource.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, AnimationDefinition definition)
100 {
101 if (string.IsNullOrWhiteSpace(filePath))
102 throw new ArgumentException("GANI 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, Cycle cycle)
121 {
122 ArgumentNullException.ThrowIfNull(cycle);
123 Save(filePath, FromCycle(cycle));
124 }
125
131 public static AnimationDefinition FromJson(string json) =>
132 FromJson(json, sourceDescription: null);
133
139 public static string ToJson(AnimationDefinition definition)
140 {
141 ArgumentNullException.ThrowIfNull(definition);
142 return JsonConvert.SerializeObject(definition, Settings);
143 }
144
150 public static AnimationDefinition FromCycle(Cycle cycle)
151 {
152 ArgumentNullException.ThrowIfNull(cycle);
153
154 return new AnimationDefinition
155 {
156 Key = cycle.CycleKey,
157 ThrottleTime = cycle.ThrottleTime,
158 CycleType = cycle.Sequence.SequenceCycleType,
159 HideTileOnCycleEnd = cycle.HideTileOnCycleEnd,
160 NextCycleKey = cycle.NextCycle?.CycleKey,
161 Frames = cycle.Sequence.FrameList
162 .Select((frame, index) =>
163 {
164 var definition = CreateFrameDefinition(frame);
165 definition.DurationSeconds = cycle.Sequence.GetDurationSeconds(index);
166 return definition;
167 })
168 .ToList(),
169 Source = AnimationDefinitionSource.Generated()
170 };
171 }
172
178 public static string ToJson(Cycle cycle) => ToJson(FromCycle(cycle));
179
188 public static Cycle ToCycle(AnimationDefinition definition)
189 {
190 var previous = Cycle._cycles.TryGetValue(definition.Key, out var existing)
191 ? existing
192 : null;
193 var cycle = MaterializeCycle(definition);
194
195 try
196 {
197 ApplyNextCycle(cycle, definition);
198 return cycle;
199 }
200 catch
201 {
202 if (Cycle._cycles.TryGetValue(cycle.CycleKey, out var registered) &&
203 ReferenceEquals(registered, cycle))
204 {
205 cycle.Dispose();
206 if (previous is not null)
207 Cycle._cycles[definition.Key] = previous;
208 }
209
210 throw;
211 }
212 }
213
219 public static Cycle LoadCycle(string filePath) => ToCycle(Load(filePath));
220
227 public static Cycle LoadCycle(AssetsFile assetsFile, string entryName) =>
228 ToCycle(Load(assetsFile, entryName));
229
230 internal static Cycle MaterializeCycle(AnimationDefinition definition)
231 {
232 ArgumentNullException.ThrowIfNull(definition);
233
234 var errors = AnimationDefinitionValidator.Validate(definition);
235 if (errors.Count != 0)
236 {
237 throw new InvalidDataException(
238 "GANI definition is invalid:" + Environment.NewLine +
239 string.Join(Environment.NewLine, errors.Select(error => "- " + error)));
240 }
241
242 var frames = definition.Frames
243 .Select(ResolveFrame)
244 .ToList();
245
246 var sequence = new FrameSequence(frames)
247 {
248 SequenceCycleType = definition.CycleType
249 };
250
251 sequence.SetDurations(definition.Frames.Select(frame => frame.DurationSeconds));
252
253 return new Cycle(
254 sequence,
255 definition.ThrottleTime,
256 definition.Key,
257 definition.HideTileOnCycleEnd);
258 }
259
260 internal static void ApplyNextCycle(
261 Cycle cycle,
262 AnimationDefinition definition)
263 {
264 ArgumentNullException.ThrowIfNull(cycle);
265 ArgumentNullException.ThrowIfNull(definition);
266
267 if (string.IsNullOrWhiteSpace(definition.NextCycleKey) ||
268 string.Equals(
269 definition.NextCycleKey,
270 cycle.CycleKey,
271 StringComparison.Ordinal))
272 {
273 cycle.NextCycle = cycle;
274 CaptureProvenance(cycle, definition);
275 return;
276 }
277
278 if (!Cycle._cycles.TryGetValue(definition.NextCycleKey, out var nextCycle))
279 {
280 throw new InvalidDataException(
281 $"GANI animation '{definition.Key}' references next cycle '{definition.NextCycleKey}', but it is not registered.");
282 }
283
284 cycle.NextCycle = nextCycle;
285 CaptureProvenance(cycle, definition);
286 }
287
288 private static void CaptureProvenance(Cycle cycle, AnimationDefinition definition) =>
289 cycle.DefinitionProvenance = definition.LoadStamp?.Materialized(definition,
290 () => FromCycle(cycle));
291
292 private static AnimationDefinition FromJson(
293 string json,
294 string? sourceDescription)
295 {
296 if (string.IsNullOrWhiteSpace(json))
297 throw new ArgumentException("GANI JSON content must be a non-empty string.", nameof(json));
298
299 try
300 {
301 var definition = JsonConvert.DeserializeObject<AnimationDefinition>(json, Settings);
302 if (definition is null)
303 {
304 var source = string.IsNullOrWhiteSpace(sourceDescription)
305 ? "GANI JSON content"
306 : sourceDescription;
307
308 throw new InvalidDataException($"Failed to deserialize {source}. Result was null.");
309 }
310
311 definition.TilesheetSources ??= [];
312 for (int i = 0; i < definition.TilesheetSources.Count; i++)
313 {
314 if (definition.TilesheetSources[i] is null)
315 {
316 throw new InvalidDataException(
317 $"GANI TilesheetSources cannot contain a null entry at index {i}.");
318 }
319 }
320
321 definition.Frames ??= [];
322 for (int i = 0; i < definition.Frames.Count; i++)
323 {
324 if (definition.Frames[i] is null)
325 throw new InvalidDataException($"GANI Frames cannot contain a null entry at index {i}.");
326 }
327
328 return definition;
329 }
330 catch (JsonException ex)
331 {
332 var source = string.IsNullOrWhiteSpace(sourceDescription)
333 ? "GANI JSON content"
334 : sourceDescription;
335
336 throw new InvalidDataException($"Failed to deserialize {source}.", ex);
337 }
338 }
339
340 private static AnimationFrameDefinition CreateFrameDefinition(Frame frame)
341 {
342 if (frame.Tilesheet is null)
343 {
344 throw new InvalidOperationException(
345 "A GANI animation frame cannot be created from an unassigned runtime Frame.");
346 }
347
348 return new AnimationFrameDefinition
349 {
350 Tilesheet = frame.Tilesheet.Name,
351 RegionName = frame.RegionName,
352 XTile = frame.XTile,
353 YTile = frame.YTile
354 };
355 }
356
357 private static Frame ResolveFrame(AnimationFrameDefinition definition)
358 {
359 var tilesheet = TilesheetRegistry.Instance.GetOrNull(definition.Tilesheet)
360 ?? throw new InvalidDataException(
361 $"GANI references tilesheet '{definition.Tilesheet}', but it is not registered.");
362
363 var region = tilesheet.GetRegion(definition.RegionName)
364 ?? throw new InvalidDataException(
365 $"GANI references region '{definition.RegionName}' on tilesheet '{definition.Tilesheet}', but it does not exist.");
366
367 if (definition.XTile < 0 ||
368 definition.YTile < 0 ||
369 definition.XTile >= region.Columns ||
370 definition.YTile >= region.Rows)
371 {
372 throw new InvalidDataException(
373 $"GANI frame ({definition.XTile}, {definition.YTile}) is outside region '{definition.RegionName}' on tilesheet '{definition.Tilesheet}'.");
374 }
375
376 return tilesheet.GetFrame(
377 definition.RegionName,
378 definition.XTile,
379 definition.YTile);
380 }
381
382 private static void ApplyDefaultSource(
383 AnimationDefinition definition,
384 AnimationDefinitionSource source)
385 {
386 if (definition.Source.Kind == AnimationDefinitionSourceKind.None)
387 definition.Source = source;
388 }
389
390 private static AnimationDefinition CreateDefinitionForSaving(
391 AnimationDefinition definition,
392 string fullPath)
393 {
394 if (definition.Source.Kind != AnimationDefinitionSourceKind.None)
395 return definition;
396
397 var clone = FromJson(ToJson(definition));
398 clone.Source = AnimationDefinitionSource.LooseDefinitionFile(fullPath);
399 return clone;
400 }
401}
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
@ AnimationDefinition
Represents an animation definition file (.gani) for Gondwana.Drawing.Animation.GANI....
Definition AssetTypes.cs:57
AnimationDefinitionSourceKind
Identifies where a GANI animation definition originated.
CycleType
Simple is self-terminating; the other two are repeating.
Definition CycleType.cs:11
readonly Tilesheet Tilesheet
The tilesheet that contains the source bitmap for this frame.
Definition Frame.cs:16
readonly string RegionName
The tilesheet region that contains the source bitmap for this frame.
Definition Frame.cs:21
readonly int XTile
The horizontal tile coordinate (column index) within the tilesheet.
Definition Frame.cs:26