10public static class AnimationDefinitionSerializer
12 private static readonly JsonSerializerSettings Settings =
new()
14 Formatting = Formatting.Indented,
15 NullValueHandling = NullValueHandling.Ignore,
16 DefaultValueHandling = DefaultValueHandling.Include,
17 MissingMemberHandling = MissingMemberHandling.Ignore
27 if (
string.IsNullOrWhiteSpace(filePath))
28 throw new ArgumentException(
"GANI file path must be a non-empty string.", nameof(filePath));
30 var fullPath = Path.GetFullPath(filePath);
31 if (!File.Exists(fullPath))
32 throw new FileNotFoundException($
"GANI file not found: {fullPath}", fullPath);
36 var definition = FromJson(File.ReadAllText(fullPath), fullPath);
39 AnimationDefinitionSource.LooseDefinitionFile(fullPath));
40 definition.LoadStamp =
new(definition, filePath: fullPath);
43 catch (JsonException ex)
45 throw new InvalidDataException($
"Failed to deserialize GANI 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());
73 ArgumentNullException.ThrowIfNull(assetsFile);
75 if (
string.IsNullOrWhiteSpace(entryName))
76 throw new ArgumentException(
"GANI asset entry name must be a non-empty string.", nameof(entryName));
78 using var stream = assetsFile.
Get(
AssetTypes.AnimationDefinition, entryName)
79 ??
throw new FileNotFoundException($
"GANI asset entry not found: {entryName}", entryName);
81 var definition = Load(stream);
82 definition.Source = AnimationDefinitionSource.None();
83 if (!
string.IsNullOrWhiteSpace(assetsFile.
SourcePath))
85 definition.Source = AnimationDefinitionSource.PackedDefinitionFile(
90 definition.LoadStamp =
new(definition, assetsFilePath: assetsFile.
SourcePath, entryName: entryName);
101 if (
string.IsNullOrWhiteSpace(filePath))
102 throw new ArgumentException(
"GANI 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, Cycle cycle)
122 ArgumentNullException.ThrowIfNull(cycle);
123 Save(filePath, FromCycle(cycle));
132 FromJson(json, sourceDescription:
null);
141 ArgumentNullException.ThrowIfNull(definition);
142 return JsonConvert.SerializeObject(definition, Settings);
152 ArgumentNullException.ThrowIfNull(cycle);
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) =>
164 var definition = CreateFrameDefinition(frame);
165 definition.DurationSeconds = cycle.Sequence.GetDurationSeconds(index);
169 Source = AnimationDefinitionSource.Generated()
178 public static string ToJson(Cycle cycle) => ToJson(FromCycle(cycle));
190 var previous = Cycle._cycles.TryGetValue(definition.Key, out var existing)
193 var cycle = MaterializeCycle(definition);
197 ApplyNextCycle(cycle, definition);
202 if (Cycle._cycles.TryGetValue(cycle.CycleKey, out var registered) &&
203 ReferenceEquals(registered, cycle))
206 if (previous is not
null)
207 Cycle._cycles[definition.Key] = previous;
219 public static Cycle LoadCycle(
string filePath) => ToCycle(Load(filePath));
227 public static Cycle LoadCycle(AssetsFile assetsFile,
string entryName) =>
228 ToCycle(Load(assetsFile, entryName));
232 ArgumentNullException.ThrowIfNull(definition);
234 var errors = AnimationDefinitionValidator.Validate(definition);
235 if (errors.Count != 0)
237 throw new InvalidDataException(
238 "GANI definition is invalid:" + Environment.NewLine +
239 string.Join(Environment.NewLine, errors.Select(error =>
"- " + error)));
242 var frames = definition.Frames
243 .Select(ResolveFrame)
246 var sequence =
new FrameSequence(frames)
248 SequenceCycleType = definition.CycleType
251 sequence.SetDurations(definition.Frames.Select(frame => frame.DurationSeconds));
255 definition.ThrottleTime,
257 definition.HideTileOnCycleEnd);
260 internal static void ApplyNextCycle(
264 ArgumentNullException.ThrowIfNull(cycle);
265 ArgumentNullException.ThrowIfNull(definition);
267 if (
string.IsNullOrWhiteSpace(definition.NextCycleKey) ||
269 definition.NextCycleKey,
271 StringComparison.Ordinal))
273 cycle.NextCycle = cycle;
274 CaptureProvenance(cycle, definition);
278 if (!Cycle._cycles.TryGetValue(definition.NextCycleKey, out var nextCycle))
280 throw new InvalidDataException(
281 $
"GANI animation '{definition.Key}' references next cycle '{definition.NextCycleKey}', but it is not registered.");
284 cycle.NextCycle = nextCycle;
285 CaptureProvenance(cycle, definition);
289 cycle.DefinitionProvenance = definition.LoadStamp?.Materialized(definition,
290 () => FromCycle(cycle));
294 string? sourceDescription)
296 if (
string.IsNullOrWhiteSpace(json))
297 throw new ArgumentException(
"GANI JSON content must be a non-empty string.", nameof(json));
302 if (definition is
null)
304 var source =
string.IsNullOrWhiteSpace(sourceDescription)
305 ?
"GANI JSON content"
308 throw new InvalidDataException($
"Failed to deserialize {source}. Result was null.");
311 definition.TilesheetSources ??= [];
312 for (
int i = 0; i < definition.TilesheetSources.Count; i++)
314 if (definition.TilesheetSources[i] is
null)
316 throw new InvalidDataException(
317 $
"GANI TilesheetSources cannot contain a null entry at index {i}.");
321 definition.Frames ??= [];
322 for (
int i = 0; i < definition.Frames.Count; i++)
324 if (definition.Frames[i] is
null)
325 throw new InvalidDataException($
"GANI Frames cannot contain a null entry at index {i}.");
330 catch (JsonException ex)
332 var source =
string.IsNullOrWhiteSpace(sourceDescription)
333 ?
"GANI JSON content"
336 throw new InvalidDataException($
"Failed to deserialize {source}.", ex);
340 private static AnimationFrameDefinition CreateFrameDefinition(Frame frame)
344 throw new InvalidOperationException(
345 "A GANI animation frame cannot be created from an unassigned runtime Frame.");
348 return new AnimationFrameDefinition
357 private static Frame ResolveFrame(AnimationFrameDefinition definition)
359 var tilesheet = TilesheetRegistry.Instance.GetOrNull(definition.Tilesheet)
360 ??
throw new InvalidDataException(
361 $
"GANI references tilesheet '{definition.Tilesheet}', but it is not registered.");
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.");
367 if (definition.XTile < 0 ||
368 definition.YTile < 0 ||
369 definition.XTile >= region.Columns ||
370 definition.YTile >= region.Rows)
372 throw new InvalidDataException(
373 $
"GANI frame ({definition.XTile}, {definition.YTile}) is outside region '{definition.RegionName}' on tilesheet '{definition.Tilesheet}'.");
376 return tilesheet.GetFrame(
377 definition.RegionName,
382 private static void ApplyDefaultSource(
384 AnimationDefinitionSource source)
387 definition.Source = source;
397 var clone = FromJson(ToJson(definition));
398 clone.Source = AnimationDefinitionSource.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.
string Name
Gets the name.
AssetTypes
Defines the types of assets that can be stored and managed by the engine.
@ AnimationDefinition
Represents an animation definition file (.gani) for Gondwana.Drawing.Animation.GANI....
AnimationDefinitionSourceKind
Identifies where a GANI animation definition originated.
CycleType
Simple is self-terminating; the other two are repeating.
readonly Tilesheet Tilesheet
The tilesheet that contains the source bitmap for this frame.
readonly string RegionName
The tilesheet region that contains the source bitmap for this frame.
readonly int XTile
The horizontal tile coordinate (column index) within the tilesheet.