250 public IDictionary<string, TilesheetDefinition>
Tilesheets => CaptureTilesheetDefinitions(baseDirectory:
null);
325 internal void Clear()
376 bool compress =
false,
377 bool separateGtsFiles =
false,
379 bool separateGscnFiles =
false,
380 bool separateGaniFiles =
false,
381 bool separateGsndFile =
false,
382 bool separateGsprFile =
false)
399 if (
string.IsNullOrWhiteSpace(path))
400 throw new ArgumentException(
"Engine state path must be a non-empty string.", nameof(path));
402 var fullPath = Path.GetFullPath(path);
403 var baseDirectory = Path.GetDirectoryName(fullPath);
405 ArgumentNullException.ThrowIfNull(options);
406 foreach (var mode
in new[] { options.Tilesheets, options.Cycles, options.Scenes, options.Audio, options.Sprites })
407 if (!Enum.IsDefined(mode))
throw new ArgumentOutOfRangeException(nameof(options));
408 var parts = options.Parts;
413 var snapshot = BuildSnapshot(NormalizeParts(parts), baseDirectory, fullPath, options);
417 if (options.Compress)
419 using var file = File.Create(fullPath);
420 using var zip =
new GZipStream(file, CompressionMode.Compress);
421 using var writer =
new StreamWriter(zip);
426 File.WriteAllText(fullPath, json);
452 if (
string.IsNullOrWhiteSpace(path))
453 throw new ArgumentException(
"Engine state path must be a non-empty string.", nameof(path));
455 var fullPath = Path.GetFullPath(path);
456 var baseDirectory = Path.GetDirectoryName(fullPath);
458 string json = ReadJsonFile(fullPath, compressed);
464 ??
new EngineStateSnapshot();
467 ApplySnapshot(snapshot, clearExisting:
true, overwriteExisting:
true, parts, baseDirectory);
496 if (
string.IsNullOrWhiteSpace(path))
497 throw new ArgumentException(
"Engine state path must be a non-empty string.", nameof(path));
499 var fullPath = Path.GetFullPath(path);
500 var baseDirectory = Path.GetDirectoryName(fullPath);
502 string json = ReadJsonFile(fullPath, compressed);
506 ??
new EngineStateSnapshot();
509 ApplySnapshot(snapshot, clearExisting:
false, overwriteExisting: overwriteExisting, parts, baseDirectory);
512 #region deserialization helpers
514 private sealed class EngineStateSnapshot
519 [JsonProperty(TypeNameHandling = TypeNameHandling.None, ItemTypeNameHandling = TypeNameHandling.None)]
520 public List<AssetsFileStateEntry>?
AssetsFiles {
get;
set; }
524 [JsonProperty]
public Dictionary<string, TilesheetStateEntry>?
Tilesheets {
get;
set; }
528 [JsonProperty]
public Dictionary<string, AnimationStateEntry>? Cycles {
get;
set; }
532 [JsonProperty]
public List<SceneStateEntry>? Scenes {
get;
set; }
536 [JsonProperty]
public SpriteStateEntry? Sprites {
get;
set; }
540 [JsonProperty]
public AudioStateEntry? Audio {
get;
set; }
547 [JsonProperty]
public Dictionary<string, AudioResource>? SoundResources {
get;
set; }
554 if (parts.HasFlag(
EngineStateParts.Sprites) && snapshot?.Sprites?.AssetsFilePath is not
null)
560 if (parts.HasFlag(
EngineStateParts.Scenes) && snapshot?.Scenes?.Any(entry => entry?.AssetsFilePath is not
null) ==
true)
573 (parts.HasFlag(
EngineStateParts.Scenes) && snapshot?.Scenes?.Any(entry => entry?.AssetsFilePath is not
null) ==
true) ||
574 (parts.HasFlag(
EngineStateParts.Sprites) && snapshot?.Sprites?.AssetsFilePath is not
null))
583 string? baseDirectory,
584 string engineStatePath,
585 EngineStateSaveOptions options)
587 return new EngineStateSnapshot
590 ?
AssetsFiles.Where(file => !
string.IsNullOrWhiteSpace(file.SourcePath))
591 .Select(file =>
new AssetsFileStateEntry
593 FilePath = MakeRelativePath(file.SourcePath!, baseDirectory),
594 Password = file.Password,
595 UseEncryption = file.UseEncryption
600 ? CaptureTilesheetEntries(
607 ? CaptureAnimationEntries(
614 ? CaptureSceneEntries(
621 ? CaptureSpriteEntry(baseDirectory, engineStatePath, options.
Sprites)
637 private SpriteStateEntry CaptureSpriteEntry(
string? baseDirectory,
string engineStatePath,
DefinitionPersistence persistence)
639 var definition = SpriteDefinitionSerializer.FromSprites(SpriteManager.Instance.AllSprites);
640 var entry =
new SpriteStateEntry();
641 if (TryPreserve(entry, SpriteManager.Instance.DefinitionProvenance, definition, persistence, baseDirectory, out var loosePath))
643 entry.GsprPath = loosePath;
647 var path = Path.Combine(Path.GetDirectoryName(engineStatePath)!,
648 Path.GetFileNameWithoutExtension(engineStatePath) +
".sprites.gspr");
649 SpriteDefinitionSerializer.Save(path, definition);
650 return new() { GsprPath = MakeRelativePath(path, baseDirectory) };
653 private static AudioStateEntry CaptureAudioEntry(
654 string? baseDirectory,
655 string engineStatePath,
658 var entry =
new AudioStateEntry();
659 var currentAudio = AudioDefinitionSerializer.FromManager(AudioResourceManager.Instance);
660 if (TryPreserve(entry, AudioResourceManager.Instance.DefinitionProvenance,
661 currentAudio, persistence, baseDirectory, out var loosePath))
663 entry.GsndPath = loosePath;
668 var anonymousAudio = currentAudio.Resources.Where(resource =>
670 string.IsNullOrWhiteSpace(resource.AssetsFilePath)).ToList();
671 if (anonymousAudio.Count != 0)
673 var directory = GetAudioStateDirectory(engineStatePath);
674 Directory.CreateDirectory(directory);
675 for (
int i = 0; i < anonymousAudio.Count; i++)
677 var resource = anonymousAudio[i];
678 var runtime = AudioResourceManager.Instance.Get(resource.Key)!;
679 var extension = Path.GetExtension(runtime.SourceExtension ??
".wav");
680 var mediaPath = Path.Combine(directory, $
"{i}-{SanitizeFileName(resource.Key)}{extension}");
681 using (var source = runtime.AssetIdentifier!.Data)
682 using (var output = File.Create(mediaPath))
683 source.CopyTo(output);
685 resource.FilePath = mediaPath;
686 resource.AssetsFilePath =
null;
687 resource.AssetEntryName =
null;
689 var definitionPath = Path.Combine(directory,
"audio.gsnd");
690 AudioDefinitionSerializer.Save(definitionPath, currentAudio);
691 return new AudioStateEntry { GsndPath = MakeRelativePath(definitionPath, baseDirectory) };
695 var gsndDirectory = GetAudioStateDirectory(engineStatePath);
696 Directory.CreateDirectory(gsndDirectory);
698 var gsndFullPath = Path.Combine(gsndDirectory,
"audio.gsnd");
699 AudioDefinitionSerializer.Save(
701 AudioResourceManager.Instance);
703 return new AudioStateEntry
705 GsndPath = MakeRelativePath(gsndFullPath, baseDirectory)
709 return new AudioStateEntry
711 Definition = AudioDefinitionSerializer.FromManager(
712 AudioResourceManager.Instance,
714 makePathsRelative: !
string.IsNullOrWhiteSpace(baseDirectory))
718 private static Dictionary<string, AnimationStateEntry> CaptureAnimationEntries(
719 string? baseDirectory,
720 string engineStatePath,
723 var result =
new Dictionary<string, AnimationStateEntry>(StringComparer.Ordinal);
724 var usedFileNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
728 foreach (var (key, cycle) in Cycle._cycles.ToList())
733 var preserved =
new AnimationStateEntry();
734 if (cycle.DefinitionProvenance is { } provenance &&
735 TryPreserve(preserved, provenance, AnimationDefinitionSerializer.FromCycle(cycle), persistence, baseDirectory, out var loosePath))
737 preserved.GaniPath = loosePath;
738 result[key] = preserved;
744 var ganiDirectory = GetAnimationStateDirectory(engineStatePath);
745 Directory.CreateDirectory(ganiDirectory);
747 var baseFileName = SanitizeFileName(key);
748 var ganiFileName = $
"{baseFileName}.gani";
751 while (!usedFileNames.Add(ganiFileName))
752 ganiFileName = $
"{baseFileName}_{suffix++}.gani";
754 var ganiFullPath = Path.Combine(ganiDirectory, ganiFileName);
758 AnimationDefinitionSerializer.Save(ganiFullPath, cycle);
760 result[key] =
new AnimationStateEntry
762 GaniPath = MakeRelativePath(ganiFullPath, baseDirectory)
767 result[key] =
new AnimationStateEntry
769 Definition = AnimationDefinitionSerializer.FromCycle(cycle)
777 private static List<SceneStateEntry> CaptureSceneEntries(
778 string? baseDirectory,
779 string engineStatePath,
782 var result =
new List<SceneStateEntry>();
783 var usedFileNames =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
787 foreach (var scene
in Scene._allScenes.ToList())
792 var preserved =
new SceneStateEntry();
793 if (scene.DefinitionProvenance is { } provenance &&
794 TryPreserve(preserved, provenance, SceneDefinitionSerializer.FromScene(scene), persistence, baseDirectory, out var loosePath))
796 preserved.GscnPath = loosePath;
797 result.Add(preserved);
803 var gscnDirectory = GetSceneStateDirectory(engineStatePath);
804 Directory.CreateDirectory(gscnDirectory);
806 var baseFileName = SanitizeFileName(scene.ID);
807 var gscnFileName = $
"{baseFileName}.gscn";
810 while (!usedFileNames.Add(gscnFileName))
811 gscnFileName = $
"{baseFileName}_{suffix++}.gscn";
813 var gscnFullPath = Path.Combine(gscnDirectory, gscnFileName);
817 SceneDefinitionSerializer.Save(gscnFullPath, scene);
819 result.Add(
new SceneStateEntry
821 GscnPath = MakeRelativePath(
828 result.Add(
new SceneStateEntry
830 Definition = SceneDefinitionSerializer.FromScene(scene)
838 private static string ReadJsonFile(
string path,
bool compressed)
842 using var file = File.OpenRead(path);
843 using var zip =
new GZipStream(file, CompressionMode.Decompress);
844 using var reader =
new StreamReader(zip);
845 return reader.ReadToEnd();
849 return File.ReadAllText(path);
862 private static void ApplySnapshot(
863 EngineStateSnapshot snapshot,
865 bool overwriteExisting,
867 string? baseDirectory)
869 parts = NormalizeParts(parts, snapshot);
874 DetachLegacySnapshotScenes(snapshot.Scenes);
875 DetachSnapshotSprites(snapshot.Sprites?.LegacySprites);
877 ValidateSelectedSources(snapshot, parts);
881 ClearSelected(parts);
884 LoadAssetsFiles(snapshot.AssetsFiles ?? Enumerable.Empty<AssetsFileStateEntry>(), overwriteExisting, baseDirectory, PackedEntries(snapshot, parts));
888 snapshot.AssetsFiles?.Select(raw => FindAssetsFile(ResolvePath(raw.FilePath, baseDirectory)))
889 .OfType<AssetsFile>().ToList(),
891 snapshot.SoundResources,
896 MergeTilesheets(snapshot.Tilesheets, overwriteExisting, baseDirectory);
899 MergeAnimations(snapshot.Cycles, overwriteExisting, baseDirectory);
902 MergeScenes(snapshot.Scenes, overwriteExisting, baseDirectory);
906 var entry = snapshot.Sprites;
907 if (entry is not
null)
909 var incoming = entry.LegacySprites;
910 if (incoming is
null)
912 var definition = entry.AssetsFilePath is not
null
913 ? LoadPacked(entry, baseDirectory,
AssetTypes.SpriteDefinition, SpriteDefinitionSerializer.Load)
914 : !string.IsNullOrWhiteSpace(entry.GsprPath)
915 ? SpriteDefinitionSerializer.Load(ResolvePath(entry.GsprPath, baseDirectory))
916 : entry.Definition ?? throw new InvalidDataException(
"Sprite state entry requires GsprPath or Definition.");
917 incoming = WithDefinitionContext(entry, baseDirectory,
918 () => SpriteDefinitionSerializer.ToSprites(definition, allowDuplicateNicknames:
true));
919 DetachSnapshotSprites(incoming);
921 MergeSprites(incoming, overwriteExisting);
930 AssetsFile.ClearAll();
931 SvgResourceManager.Instance.Clear();
935 TilesheetRegistry.Instance.Clear();
938 Cycle.ClearAllAnimationCycles();
941 Scene.ClearAllScenes();
945 SpriteManager.Instance.DefinitionProvenance =
null;
947 foreach (var sprite
in SpriteManager.Instance.AllSprites)
949 SpriteManager.Instance._spriteList.Remove(sprite);
950 sprite.DisposeImmediate();
955 AudioResourceManager.Instance.Dispose();
958 private static void LoadAssetsFiles(IEnumerable<AssetsFileStateEntry> resourceFiles,
bool overwriteExisting,
string? baseDirectory, IEnumerable<DefinitionStateEntry> packedEntries)
961 if (resourceFiles.Any())
963 foreach (var raw
in resourceFiles)
967 var path = ResolvePath(raw.FilePath, baseDirectory);
968 var loaded = FindAssetsFile(path);
971 if (!File.Exists(path))
throw new FileNotFoundException($
"EngineState assets file not found: {path}", path);
972 loaded = AssetsFile.LoadOrCreate(path, raw.Password, raw.UseEncryption);
975 if (overwriteExisting)
977 foreach (var entry
in loaded.GetAllEntries().Where(e => e.AssetType ==
AssetTypes.Svg))
978 SvgResourceManager.Instance.Unload(entry.AssetName);
981 SvgResourceManager.Instance.LoadFromEngineAssetsFile(loaded);
985 Engine.Logger.LogError(ex,
"Failed to load resource file '{FilePath}'", raw.FilePath);
986 var dependency = packedEntries.FirstOrDefault(entry =>
string.Equals(
987 ResolvePath(entry.AssetsFilePath!, baseDirectory), ResolvePath(raw.FilePath, baseDirectory),
988 OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
989 if (dependency is not
null)
990 throw new InvalidDataException($
"Cannot load packed definition '{dependency.AssetEntryName}' from GAF '{ResolvePath(raw.FilePath, baseDirectory)}'.", ex);
997 private static void MergeAudio(
998 List<AssetsFile>? assetsFiles,
999 AudioStateEntry? audio,
1000 Dictionary<string, AudioResource>? soundSpecs,
1001 bool overwriteExisting,
1002 string? baseDirectory)
1004 if (audio is not
null)
1008 if (audio.AssetsFilePath is not
null)
1010 definition = LoadPacked(audio, baseDirectory,
AssetTypes.AudioDefinition, AudioDefinitionSerializer.Load);
1012 else if (!
string.IsNullOrWhiteSpace(audio.GsndPath))
1014 definition = AudioDefinitionSerializer.Load(
1015 ResolvePath(audio.GsndPath, baseDirectory));
1017 else if (audio.Definition is not
null)
1019 definition = audio.Definition;
1023 throw new InvalidDataException(
1024 "Audio state entry does not contain a GSND path or inline definition.");
1027 WithDefinitionContext(audio, baseDirectory, () => AudioDefinitionSerializer.LoadIntoManager(
1030 audio.Definition is not
null ? baseDirectory :
1031 audio.GsndPath is not
null ? Path.GetDirectoryName(ResolvePath(audio.GsndPath, baseDirectory)) :
null));
1037 if (assetsFiles is not
null)
1039 foreach (var af
in assetsFiles)
1041 if (overwriteExisting)
1043 foreach (var entry
in af.GetAllEntries())
1046 AudioResourceManager.Instance.Unload(entry.AssetName);
1050 AudioResourceManager.Instance.LoadFromEngineAssetsFile(af);
1055 if (soundSpecs is
null)
1058 foreach (var (key, spec) in soundSpecs)
1060 if (AudioResourceManager.Instance.Contains(key))
1062 if (!overwriteExisting)
1064 var existing = AudioResourceManager.Instance.Get(key);
1065 if (existing is not
null)
1067 existing.Volume = spec.Volume;
1068 existing.Pan = spec.Pan;
1069 existing.PlaybackSpeed = spec.PlaybackSpeed;
1070 existing.IsLooping = spec.IsLooping;
1075 AudioResourceManager.Instance.Unload(key);
1079 spec.ReloadIntoManager();
1083 private static Dictionary<string, TilesheetDefinition> CaptureTilesheetDefinitions(
string? baseDirectory)
1085 return TilesheetRegistry.Instance
1089 kvp => TilesheetDefinitionSerializer.FromTilesheet(
1092 makePathsRelative: !
string.IsNullOrWhiteSpace(baseDirectory)));
1095 private static Dictionary<string, TilesheetStateEntry> CaptureTilesheetEntries(
1096 string? baseDirectory,
1097 string engineStatePath,
1100 var result =
new Dictionary<string, TilesheetStateEntry>(StringComparer.Ordinal);
1102 foreach (var (key, tilesheet) in TilesheetRegistry.Instance.GetAll())
1104 var preserved =
new TilesheetStateEntry();
1105 if (tilesheet.DefinitionProvenance is { } provenance &&
1106 TryPreserve(preserved, provenance, TilesheetDefinitionSerializer.FromTilesheet(tilesheet), persistence, baseDirectory, out var loosePath))
1108 preserved.GtsPath = loosePath;
1109 result[key] = preserved;
1115 string.IsNullOrWhiteSpace(tilesheet.ImageFilePath) &&
1116 string.IsNullOrWhiteSpace(tilesheet.AssetIdentifier?.AssetsFile.SourcePath)))
1118 var gtsDirectory = GetTilesheetStateDirectory(engineStatePath);
1119 Directory.CreateDirectory(gtsDirectory);
1121 var gtsFileName = $
"{SanitizeFileName(key)}.gts";
1122 var gtsFullPath = Path.Combine(gtsDirectory, gtsFileName);
1127 TilesheetDefinitionSerializer.Save(
1130 makePathsRelative:
true);
1132 var gtsPathForState = MakeRelativePath(
1136 result[key] =
new TilesheetStateEntry
1138 GtsPath = gtsPathForState
1143 result[key] =
new TilesheetStateEntry
1145 Definition = TilesheetDefinitionSerializer.FromTilesheet(
1148 makePathsRelative: !
string.IsNullOrWhiteSpace(baseDirectory))
1156 private static string GetTilesheetStateDirectory(
string engineStatePath)
1158 var directory = Path.GetDirectoryName(engineStatePath) ??
string.Empty;
1159 var fileName = Path.GetFileNameWithoutExtension(engineStatePath);
1161 return Path.Combine(directory, $
"{fileName}.tilesheets");
1165 private static string GetAnimationStateDirectory(
string engineStatePath)
1167 var directory = Path.GetDirectoryName(engineStatePath) ??
string.Empty;
1168 var fileName = Path.GetFileNameWithoutExtension(engineStatePath);
1170 return Path.Combine(directory, $
"{fileName}.animations");
1174 private static string GetAudioStateDirectory(
string engineStatePath)
1176 var directory = Path.GetDirectoryName(engineStatePath) ??
string.Empty;
1177 var fileName = Path.GetFileNameWithoutExtension(engineStatePath);
1179 return Path.Combine(directory, $
"{fileName}.audio");
1183 private static string GetSceneStateDirectory(
string engineStatePath)
1185 var directory = Path.GetDirectoryName(engineStatePath) ??
string.Empty;
1186 var fileName = Path.GetFileNameWithoutExtension(engineStatePath);
1188 return Path.Combine(directory, $
"{fileName}.scenes");
1191 private static string MakeRelativePath(
string path,
string? baseDirectory)
1193 if (
string.IsNullOrWhiteSpace(baseDirectory))
1196 return Path.GetRelativePath(
1197 Path.GetFullPath(baseDirectory),
1198 Path.GetFullPath(path));
1201 private static string SanitizeFileName(
string value)
1203 if (
string.IsNullOrWhiteSpace(value))
1204 return Guid.NewGuid().ToString(
"N");
1206 var invalidChars = Path.GetInvalidFileNameChars();
1207 var sanitized =
new string(
1208 value.Select(ch => invalidChars.Contains(ch) ?
'_' : ch).ToArray());
1210 return string.IsNullOrWhiteSpace(sanitized)
1211 ? Guid.NewGuid().ToString(
"N")
1215 private static void MergeTilesheets(
1216 Dictionary<string, TilesheetStateEntry>? tilesheets,
1217 bool overwriteExisting,
1218 string? baseDirectory)
1220 if (tilesheets is
null || tilesheets.Count == 0)
1223 var registry = TilesheetRegistry.Instance.GetAll();
1225 foreach (var (key, entry) in tilesheets)
1230 var existingKey = key;
1232 if (!overwriteExisting && registry.ContainsKey(existingKey))
1237 if (entry.AssetsFilePath is not
null)
1239 rebuilt = LoadPacked(entry, baseDirectory,
AssetTypes.TilesheetDefinition, TilesheetFactory.FromDefinitionAsset);
1241 else if (!
string.IsNullOrWhiteSpace(entry.GtsPath))
1243 var gtsPath = ResolvePath(entry.GtsPath, baseDirectory);
1245 rebuilt = TilesheetFactory.FromDefinitionFile(gtsPath);
1247 else if (entry.Definition is not
null)
1249 if (
string.IsNullOrWhiteSpace(entry.Definition.Name))
1250 entry.Definition.Name = key;
1252 rebuilt = TilesheetFactory.FromDefinition(
1258 throw new InvalidDataException(
1259 $
"Tilesheet state entry '{key}' does not contain a GTS path or inline definition.");
1262 TilesheetRegistry.Instance.Register(
1264 disposeReplaced: overwriteExisting);
1268 private static string ResolvePath(
string path,
string? baseDirectory)
1270 if (Path.IsPathRooted(path))
1273 if (
string.IsNullOrWhiteSpace(baseDirectory))
1276 return Path.GetFullPath(Path.Combine(baseDirectory, path));
1279 private static void MergeAnimations(
1280 Dictionary<string, AnimationStateEntry>? animations,
1281 bool overwriteExisting,
1282 string? baseDirectory)
1284 if (animations is
null || animations.Count == 0)
1287 var definitions =
new List<AnimationDefinition>();
1288 var sourceEntries =
new Dictionary<AnimationDefinition, AnimationStateEntry>();
1290 foreach (var (key, entry) in animations)
1295 if (!overwriteExisting && Cycle._cycles.ContainsKey(key))
1300 if (entry.AssetsFilePath is not
null)
1302 definition = LoadPacked(entry, baseDirectory,
AssetTypes.AnimationDefinition, AnimationDefinitionSerializer.Load);
1304 else if (!
string.IsNullOrWhiteSpace(entry.GaniPath))
1306 var ganiPath = ResolvePath(entry.GaniPath, baseDirectory);
1307 definition = AnimationDefinitionSerializer.Load(ganiPath);
1309 else if (entry.Definition is not
null)
1311 definition = entry.Definition;
1315 throw new InvalidDataException(
1316 $
"Animation state entry '{key}' does not contain a GANI path or inline definition.");
1319 if (
string.IsNullOrWhiteSpace(definition.
Key))
1321 definition.Key = key;
1323 else if (!
string.Equals(definition.
Key, key, StringComparison.Ordinal))
1325 throw DefinitionError(entry, baseDirectory,
new InvalidDataException(
1326 $
"Animation state entry '{key}' contains definition key '{definition.Key}'."));
1329 definitions.Add(definition);
1330 sourceEntries.Add(definition, entry);
1335 var availableKeys =
new HashSet<string>(
1337 StringComparer.Ordinal);
1339 foreach (var definition
in definitions)
1340 availableKeys.Add(definition.Key);
1342 foreach (var definition
in definitions)
1344 if (!
string.IsNullOrWhiteSpace(definition.NextCycleKey) &&
1346 definition.NextCycleKey,
1348 StringComparison.Ordinal) &&
1349 !availableKeys.Contains(definition.NextCycleKey))
1351 throw DefinitionError(sourceEntries[definition], baseDirectory,
new InvalidDataException(
1352 $
"GANI animation '{definition.Key}' references next cycle '{definition.NextCycleKey}', but no matching animation is available."));
1356 var materialized =
new List<(Cycle Cycle, AnimationDefinition Definition)>();
1358 foreach (var definition
in definitions)
1360 var cycle = WithDefinitionContext(sourceEntries[definition], baseDirectory,
1361 () => AnimationDefinitionSerializer.MaterializeCycle(definition));
1362 materialized.Add((cycle, definition));
1365 foreach (var (cycle, definition) in materialized)
1366 WithDefinitionContext(sourceEntries[definition], baseDirectory, () =>
1368 AnimationDefinitionSerializer.ApplyNextCycle(cycle, definition);
1373 private static void DetachLegacySnapshotScenes(List<SceneStateEntry>? scenes)
1378 foreach (var entry
in scenes)
1380 if (entry?.LegacyScene is { } legacyScene)
1381 Scene._allScenes.Remove(legacyScene);
1385 private static void MergeScenes(
1386 List<SceneStateEntry>? scenes,
1387 bool overwriteExisting,
1388 string? baseDirectory)
1390 if (scenes is
null || scenes.Count == 0)
1393 var existingIndexById =
new Dictionary<string, int>(StringComparer.Ordinal);
1394 for (
int i = 0; i <
Scene._allScenes.Count; i++)
1396 var
id =
Scene._allScenes[i].ID;
1397 if (!
string.IsNullOrWhiteSpace(
id) && !existingIndexById.ContainsKey(
id))
1398 existingIndexById.Add(
id, i);
1403 var seenIncoming =
new HashSet<string>(StringComparer.Ordinal);
1405 foreach (var entry
in scenes)
1411 bool materializedFromDefinition =
false;
1413 if (entry.LegacyScene is not
null)
1415 incoming = entry.LegacyScene;
1417 else if (entry.AssetsFilePath is not
null)
1419 incoming = LoadPacked(entry, baseDirectory,
AssetTypes.SceneDefinition, SceneDefinitionSerializer.LoadScene);
1420 materializedFromDefinition =
true;
1422 else if (!
string.IsNullOrWhiteSpace(entry.GscnPath))
1424 var gscnPath = ResolvePath(entry.GscnPath, baseDirectory);
1425 incoming = SceneDefinitionSerializer.LoadScene(gscnPath);
1426 materializedFromDefinition =
true;
1428 else if (entry.Definition is not
null)
1430 incoming = SceneDefinitionSerializer.ToScene(entry.Definition);
1431 materializedFromDefinition =
true;
1435 throw new InvalidDataException(
1436 "Scene state entry does not contain a GSCN path or inline definition.");
1441 if (materializedFromDefinition)
1442 Scene._allScenes.Remove(incoming);
1444 if (
string.IsNullOrWhiteSpace(incoming.
ID))
1445 incoming.ID = Guid.NewGuid().ToString();
1447 bool duplicateIncoming = !seenIncoming.Add(incoming.
ID);
1449 if (existingIndexById.TryGetValue(incoming.
ID, out
int existingIndex))
1451 if (!overwriteExisting && !duplicateIncoming)
1453 if (materializedFromDefinition)
1459 Scene._allScenes[existingIndex] = incoming;
1463 existingIndexById[incoming.
ID] =
Scene._allScenes.Count;
1464 Scene._allScenes.Add(incoming);
1469 private static void RebindSpriteSceneLayer(Sprite sprite)
1471 var layerId = sprite.SerializedSceneLayerIdForBinding;
1472 if (
string.IsNullOrWhiteSpace(layerId))
1477 throw new InvalidDataException(
1478 $
"Sprite '{sprite.Nickname ?? sprite.Id.ToString()}' does not contain a SceneLayer identity.");
1482 var sceneId = sprite.SerializedSceneIdForBinding;
1484 if (!
string.IsNullOrWhiteSpace(sceneId))
1486 var scene =
Scene._allScenes.FirstOrDefault(
1487 candidate =>
string.Equals(candidate.ID, sceneId, StringComparison.Ordinal));
1489 targetLayer = scene?.GetSceneLayerByID(layerId);
1495 var matches = Scene._allScenes
1496 .SelectMany(scene => scene.SceneLayers)
1497 .Where(layer =>
string.Equals(layer.ID, layerId, StringComparison.Ordinal))
1501 if (matches.Count == 1)
1502 targetLayer = matches[0];
1503 else if (matches.Count > 1)
1504 throw new InvalidDataException(
1505 $
"Sprite '{sprite.Nickname ?? sprite.Id.ToString()}' references ambiguous SceneLayer ID '{layerId}'.");
1508 if (targetLayer is
null)
1510 throw new InvalidDataException(
1511 $
"Sprite '{sprite.Nickname ?? sprite.Id.ToString()}' could not resolve SceneLayer '{layerId}'" +
1512 (
string.IsNullOrWhiteSpace(sceneId) ?
"." : $
" in Scene '{sceneId}'."));
1515 sprite.RebindSceneLayerAfterDeserialization(targetLayer);
1518 private static void DetachSnapshotSprites(List<Sprite>? sprites)
1520 if (sprites is
null)
1523 foreach (var sprite
in sprites)
1525 if (sprite is not
null)
1526 SpriteManager.Instance._spriteList.Remove(sprite);
1530 private static void MergeSprites(List<Sprite>? sprites,
bool overwriteExisting)
1532 if (sprites is
null || sprites.Count == 0)
1535 var existingIndexById =
new Dictionary<string, int>(StringComparer.Ordinal);
1536 for (
int i = 0; i < SpriteManager.Instance._spriteList.Count; i++)
1538 var
id = SpriteManager.Instance._spriteList[i].Nickname;
1539 if (!
string.IsNullOrWhiteSpace(
id) && !existingIndexById.ContainsKey(
id))
1540 existingIndexById.Add(
id, i);
1543 var seenIncoming =
new HashSet<string>(StringComparer.Ordinal);
1545 foreach (var incoming
in sprites)
1547 if (incoming is
null)
1550 RebindSpriteSceneLayer(incoming);
1552 if (
string.IsNullOrWhiteSpace(incoming.Nickname))
1553 incoming.Nickname = Guid.NewGuid().ToString();
1555 bool duplicateIncoming = !seenIncoming.Add(incoming.Nickname);
1557 if (existingIndexById.TryGetValue(incoming.Nickname, out
int existingIndex))
1559 if (!overwriteExisting && !duplicateIncoming)
1561 incoming.DisposeImmediate();
1565 SpriteManager.Instance._spriteList[existingIndex].DisposeImmediate();
1566 SpriteManager.Instance._spriteList[existingIndex] = incoming;
1570 existingIndexById[incoming.Nickname] = SpriteManager.Instance._spriteList.Count;
1571 SpriteManager.Instance.AddSprite(incoming);
1576 #endregion deserialization helpers