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
AudioDefinitionSerializer.cs
2using Newtonsoft.Json;
3
4namespace Gondwana.Audio.GSND;
5
9public static class AudioDefinitionSerializer
10{
11 private static readonly JsonSerializerSettings Settings = new()
12 {
13 Formatting = Formatting.Indented,
14 NullValueHandling = NullValueHandling.Ignore,
15 DefaultValueHandling = DefaultValueHandling.Include,
16 MissingMemberHandling = MissingMemberHandling.Ignore
17 };
18
24 public static AudioDefinition Load(string filePath)
25 {
26 if (string.IsNullOrWhiteSpace(filePath))
27 throw new ArgumentException("GSND file path must be a non-empty string.", nameof(filePath));
28
29 var fullPath = Path.GetFullPath(filePath);
30 if (!File.Exists(fullPath))
31 throw new FileNotFoundException($"GSND file not found: {fullPath}", fullPath);
32
33 try
34 {
35 var definition = FromJson(File.ReadAllText(fullPath), fullPath);
36 ApplyDefaultSource(definition, AudioDefinitionSource.LooseDefinitionFile(fullPath));
37 definition.LoadStamp = new(definition, filePath: fullPath);
38 return definition;
39 }
40 catch (JsonException ex)
41 {
42 throw new InvalidDataException($"Failed to deserialize GSND file: {fullPath}", ex);
43 }
44 }
45
51 public static AudioDefinition Load(Stream stream)
52 {
53 ArgumentNullException.ThrowIfNull(stream);
54 if (!stream.CanRead)
55 throw new ArgumentException("Stream must be readable.", nameof(stream));
56
57 using var reader = new StreamReader(stream, leaveOpen: true);
58 return FromJson(reader.ReadToEnd());
59 }
60
67 public static AudioDefinition Load(AssetsFile assetsFile, string entryName)
68 {
69 ArgumentNullException.ThrowIfNull(assetsFile);
70 ArgumentException.ThrowIfNullOrWhiteSpace(entryName);
71
72 using var stream = assetsFile.Get(AssetTypes.AudioDefinition, entryName)
73 ?? throw new FileNotFoundException($"GSND asset entry not found: {entryName}", entryName);
74
75 var definition = Load(stream);
76 definition.Source = AudioDefinitionSource.None();
77 if (!string.IsNullOrWhiteSpace(assetsFile.SourcePath))
78 {
79 definition.Source = AudioDefinitionSource.PackedDefinitionFile(
80 assetsFile.SourcePath,
81 entryName);
82 }
83
84 definition.LoadStamp = new(definition, assetsFilePath: assetsFile.SourcePath, entryName: entryName);
85 return definition;
86 }
87
93 public static void Save(string filePath, AudioDefinition definition)
94 {
95 if (string.IsNullOrWhiteSpace(filePath))
96 throw new ArgumentException("GSND file path must be a non-empty string.", nameof(filePath));
97
98 ArgumentNullException.ThrowIfNull(definition);
99
100 var fullPath = Path.GetFullPath(filePath);
101 var directory = Path.GetDirectoryName(fullPath) ?? string.Empty;
102 if (!string.IsNullOrWhiteSpace(directory))
103 Directory.CreateDirectory(directory);
104
105 var clone = FromJson(ToJson(definition));
106 RebaseReferences(clone, GetReferenceBaseDirectory(definition.Source), directory);
107 clone.Source = AudioDefinitionSource.LooseDefinitionFile(fullPath);
108
109 File.WriteAllText(fullPath, ToJson(clone));
110 }
111
117 public static void Save(string filePath, AudioResourceManager manager)
118 {
119 ArgumentNullException.ThrowIfNull(manager);
120 Save(filePath, FromManager(manager));
121 }
122
128 public static string ToJson(AudioDefinition definition)
129 {
130 ArgumentNullException.ThrowIfNull(definition);
131 return JsonConvert.SerializeObject(definition, Settings);
132 }
133
139 public static AudioDefinition FromJson(string json) =>
140 FromJson(json, sourceDescription: null);
141
149 public static AudioDefinition FromManager(
150 AudioResourceManager manager,
151 string? baseDirectory = null,
152 bool makePathsRelative = false)
153 {
154 ArgumentNullException.ThrowIfNull(manager);
155
156 var definition = new AudioDefinition
157 {
158 Resources = manager.GetAll()
159 .OrderBy(pair => pair.Key, StringComparer.Ordinal)
160 .Select(pair => FromResource(pair.Value))
161 .ToList(),
162 Source = AudioDefinitionSource.Generated()
163 };
164
165 if (makePathsRelative && !string.IsNullOrWhiteSpace(baseDirectory))
166 RebaseReferences(definition, null, baseDirectory);
167
168 return definition;
169 }
170
176 public static AudioResourceDefinition FromResource(AudioResource resource)
177 {
178 ArgumentNullException.ThrowIfNull(resource);
179
180 var definition = new AudioResourceDefinition
181 {
182 Key = resource.Key,
183 Volume = resource.Volume,
184 Pan = resource.Pan,
185 PlaybackSpeed = resource.PlaybackSpeed,
186 IsLooping = resource.IsLooping,
187 SourceExtension = resource.SourceExtension
188 };
189
190 if (resource.AssetIdentifier is { } asset)
191 {
192 definition.SourceKind = AudioResourceSourceKind.PackedAsset;
193 definition.AssetsFilePath = asset.AssetsFile.SourcePath;
194 definition.AssetEntryName = asset.AssetName;
195 }
196 else if (!string.IsNullOrWhiteSpace(resource.SourceFilePath))
197 {
198 definition.SourceKind = AudioResourceSourceKind.LooseFile;
199 definition.FilePath = resource.SourceFilePath;
200 }
201 else if (!string.IsNullOrWhiteSpace(resource.SourceUri))
202 {
203 definition.SourceKind = AudioResourceSourceKind.Uri;
204 definition.SourceUri = resource.SourceUri;
205 }
206 else
207 {
208 throw new InvalidOperationException(
209 $"AudioResource '{resource.Key}' does not have a persistable file, packed-asset, or URI source.");
210 }
211
212 return definition;
213 }
214
222 public static IReadOnlyList<AudioResource> LoadIntoManager(
223 AudioDefinition definition,
224 bool overwriteExisting = true,
225 string? baseDirectory = null)
226 {
227 ArgumentNullException.ThrowIfNull(definition);
228
229 var errors = AudioDefinitionValidator.Validate(definition);
230 if (errors.Count != 0)
231 {
232 throw new InvalidDataException(
233 "GSND definition is invalid:" + Environment.NewLine +
234 string.Join(Environment.NewLine, errors.Select(error => "- " + error)));
235 }
236
237 baseDirectory ??= GetReferenceBaseDirectory(definition.Source);
238 var manager = AudioResourceManager.Instance;
239 var loaded = new List<AudioResource>();
240 bool skippedExisting = false;
241
242 foreach (var resourceDefinition in definition.Resources)
243 {
244 if (manager.TryGet(resourceDefinition.Key, out var existing) && existing is not null)
245 {
246 if (!overwriteExisting)
247 {
248 skippedExisting = true;
249 ApplySettings(existing, resourceDefinition);
250 loaded.Add(existing);
251 continue;
252 }
253
254 manager.Unload(resourceDefinition.Key);
255 }
256
257 AudioResource resource = resourceDefinition.SourceKind switch
258 {
259 AudioResourceSourceKind.LooseFile =>
260 manager.LoadFromFile(
261 resourceDefinition.Key,
262 ResolveReferencePath(resourceDefinition.FilePath!, baseDirectory),
263 resourceDefinition.Volume,
264 resourceDefinition.Pan,
265 resourceDefinition.PlaybackSpeed),
266
267 AudioResourceSourceKind.Uri =>
268 manager.LoadFromUri(
269 resourceDefinition.Key,
270 resourceDefinition.SourceUri!,
271 resourceDefinition.Volume,
272 resourceDefinition.Pan,
273 resourceDefinition.PlaybackSpeed),
274
275 AudioResourceSourceKind.PackedAsset =>
276 LoadPackedResource(resourceDefinition, baseDirectory, manager),
277
278 _ => throw new InvalidDataException(
279 $"Unknown GSND source kind '{resourceDefinition.SourceKind}'.")
280 };
281
282 resource.IsLooping = resourceDefinition.IsLooping;
283 loaded.Add(resource);
284 }
285
286 manager.DefinitionProvenance = !skippedExisting && manager.GetAll().Count == loaded.Count
287 ? definition.LoadStamp?.Materialized(definition,
288 () => FromManager(manager))
289 : null;
290 return loaded;
291 }
292
299 public static IReadOnlyList<AudioResource> LoadIntoManager(
300 string filePath,
301 bool overwriteExisting = true) =>
302 LoadIntoManager(
303 Load(filePath),
304 overwriteExisting,
305 Path.GetDirectoryName(Path.GetFullPath(filePath)));
306
307 private static AudioResource LoadPackedResource(
308 AudioResourceDefinition definition,
309 string? baseDirectory,
310 AudioResourceManager manager)
311 {
312 var assetsPath = ResolveReferencePath(definition.AssetsFilePath!, baseDirectory);
313 var assetsFile = AssetsFile.AllAssetsFiles.FirstOrDefault(
314 file => !string.IsNullOrWhiteSpace(file.SourcePath) && string.Equals(
315 Path.GetFullPath(file.SourcePath),
316 Path.GetFullPath(assetsPath),
317 StringComparison.OrdinalIgnoreCase))
318 ?? throw new InvalidDataException(
319 $"GSND resource '{definition.Key}' references assets file '{assetsPath}', but it is not loaded.");
320
321 using var stream = assetsFile.Get(AssetTypes.Audio, definition.AssetEntryName!)
322 ?? throw new InvalidDataException(
323 $"GSND resource '{definition.Key}' references missing audio entry '{definition.AssetEntryName}' in '{assetsPath}'.");
324
325 var extension = !string.IsNullOrWhiteSpace(definition.SourceExtension)
326 ? definition.SourceExtension!
327 : Path.GetExtension(definition.AssetEntryName!);
328
329 var resource = manager.LoadFromStream(
330 definition.Key,
331 stream,
332 extension,
333 definition.Volume,
334 definition.Pan,
335 definition.PlaybackSpeed);
336
337 resource.SetAssetIdentifier(
338 new AssetsFileIdentifier(
339 assetsFile,
340 AssetTypes.Audio,
341 definition.AssetEntryName!));
342
343 return resource;
344 }
345
346 private static void ApplySettings(
347 AudioResource resource,
348 AudioResourceDefinition definition)
349 {
350 resource.Volume = definition.Volume;
351 resource.Pan = definition.Pan;
352 resource.PlaybackSpeed = definition.PlaybackSpeed;
353 resource.IsLooping = definition.IsLooping;
354 }
355
356 private static AudioDefinition FromJson(
357 string json,
358 string? sourceDescription)
359 {
360 if (string.IsNullOrWhiteSpace(json))
361 throw new ArgumentException("GSND JSON content must be a non-empty string.", nameof(json));
362
363 try
364 {
365 var definition = JsonConvert.DeserializeObject<AudioDefinition>(json, Settings);
366 if (definition is null)
367 {
368 var source = string.IsNullOrWhiteSpace(sourceDescription)
369 ? "GSND JSON content"
370 : sourceDescription;
371 throw new InvalidDataException($"Failed to deserialize {source}. Result was null.");
372 }
373
374 definition.Resources ??= [];
375 for (int i = 0; i < definition.Resources.Count; i++)
376 {
377 if (definition.Resources[i] is null)
378 throw new InvalidDataException($"GSND Resources cannot contain a null entry at index {i}.");
379 }
380
381 return definition;
382 }
383 catch (JsonException ex)
384 {
385 var source = string.IsNullOrWhiteSpace(sourceDescription)
386 ? "GSND JSON content"
387 : sourceDescription;
388 throw new InvalidDataException($"Failed to deserialize {source}.", ex);
389 }
390 }
391
392 private static void ApplyDefaultSource(
393 AudioDefinition definition,
394 AudioDefinitionSource source)
395 {
396 if (definition.Source.Kind == AudioDefinitionSourceKind.None)
397 definition.Source = source;
398 }
399
400 private static string? GetReferenceBaseDirectory(AudioDefinitionSource source)
401 {
402 return source.Kind switch
403 {
404 AudioDefinitionSourceKind.LooseDefinitionFile
405 when !string.IsNullOrWhiteSpace(source.GsndFilePath) =>
406 Path.GetDirectoryName(Path.GetFullPath(source.GsndFilePath)),
407
408 AudioDefinitionSourceKind.PackedDefinitionFile
409 when !string.IsNullOrWhiteSpace(source.AssetsFilePath) =>
410 Path.GetDirectoryName(Path.GetFullPath(source.AssetsFilePath)),
411
412 _ => null
413 };
414 }
415
416 private static void RebaseReferences(
417 AudioDefinition definition,
418 string? oldBaseDirectory,
419 string newBaseDirectory)
420 {
421 foreach (var resource in definition.Resources)
422 {
423 switch (resource.SourceKind)
424 {
425 case AudioResourceSourceKind.LooseFile
426 when !string.IsNullOrWhiteSpace(resource.FilePath):
427 resource.FilePath = MakeReferencePath(
428 ResolveReferencePath(resource.FilePath, oldBaseDirectory),
429 newBaseDirectory);
430 break;
431
432 case AudioResourceSourceKind.PackedAsset
433 when !string.IsNullOrWhiteSpace(resource.AssetsFilePath):
434 resource.AssetsFilePath = MakeReferencePath(
435 ResolveReferencePath(resource.AssetsFilePath, oldBaseDirectory),
436 newBaseDirectory);
437 break;
438 }
439 }
440 }
441
442 private static string ResolveReferencePath(
443 string path,
444 string? baseDirectory)
445 {
446 if (Path.IsPathRooted(path))
447 return Path.GetFullPath(path);
448
449 return string.IsNullOrWhiteSpace(baseDirectory)
450 ? Path.GetFullPath(path)
451 : Path.GetFullPath(path, baseDirectory);
452 }
453
454 private static string MakeReferencePath(
455 string path,
456 string baseDirectory)
457 {
458 var fullPath = Path.GetFullPath(path);
459 var fullBase = Path.GetFullPath(baseDirectory);
460 if (!string.Equals(
461 Path.GetPathRoot(fullPath),
462 Path.GetPathRoot(fullBase),
463 StringComparison.OrdinalIgnoreCase))
464 {
465 return fullPath;
466 }
467
468 return Path.GetRelativePath(fullBase, fullPath);
469 }
470}
string? SourcePath
Gets the archive's reconstructible origin, including an optional stream origin.
static IReadOnlyList< AssetsFile > AllAssetsFiles
Gets a read-only list of registered AssetsFile instances.
Definition AssetsFile.cs:24
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
@ AudioDefinition
Represents an audio definition file (.gsnd) for Gondwana.Audio.GSND.AudioDefinition.
Definition AssetTypes.cs:62
AudioResourceSourceKind
Identifies how a GSND audio resource locates its underlying media.
AudioDefinitionSourceKind
Identifies the provenance of an audio definition.