diff --git a/src/Murder.Editor/Assets/SpriteEventData.cs b/src/Murder.Editor/Assets/SpriteEventData.cs new file mode 100644 index 000000000..e16c44d77 --- /dev/null +++ b/src/Murder.Editor/Assets/SpriteEventData.cs @@ -0,0 +1,95 @@ +namespace Murder.Editor.Assets; + +public class SpriteEventData +{ + public readonly Dictionary> Events = new(); + + public readonly Dictionary> DeletedEvents = new(); + + /// + /// Apply custom messages to an existing dictionary of events. + /// + public void FilterEventsForAnimation(string animation, ref Dictionary events) + { + if (Events.TryGetValue(animation, out var customEvents)) + { + foreach ((int frame, string message) in customEvents) + { + events[frame] = message; + } + } + + if (DeletedEvents.TryGetValue(animation, out var deletedEvents)) + { + foreach (int frame in deletedEvents) + { + events.Remove(frame); + } + } + } + + public Dictionary GetEventsForAnimation(string animation) + { + if (!Events.TryGetValue(animation, out var dictionary)) + { + dictionary = new(); + Events[animation] = dictionary; + } + + return dictionary; + } + + public void AddEvent(string animation, int frame, string message) + { + Dictionary dict = GetEventsForAnimation(animation); + dict[frame] = message; + + RemoveFromDeletedEvents(animation, frame); + } + + public void RemoveEvent(string animation, int frame) + { + if (!Events.TryGetValue(animation, out var dictionary)) + { + AddToDeletedEvent(animation, frame); + return; + } + + bool hadValueDefined = dictionary.Remove(frame); + if (!hadValueDefined) + { + // Value was previously not tracked here. It is likely added by aseprite. + // So let's override that! + AddToDeletedEvent(animation, frame); + } + } + + private void AddToDeletedEvent(string animation, int frame) + { + if (!DeletedEvents.TryGetValue(animation, out var dictionary)) + { + dictionary = new(); + DeletedEvents[animation] = dictionary; + } + + dictionary.Add(frame); + } + + private bool RemoveFromDeletedEvents(string animation, int frame) + { + if (!DeletedEvents.TryGetValue(animation, out var dictionary)) + { + return false; + } + + _ = dictionary.Remove(frame); + if (dictionary.Count == 0) + { + DeletedEvents.Remove(animation); + } + + return true; + } + + public SpriteEventData() { } +} \ No newline at end of file diff --git a/src/Murder.Editor/Assets/SpriteEventDataManagerAsset.cs b/src/Murder.Editor/Assets/SpriteEventDataManagerAsset.cs index 9f0730795..0849b8401 100644 --- a/src/Murder.Editor/Assets/SpriteEventDataManagerAsset.cs +++ b/src/Murder.Editor/Assets/SpriteEventDataManagerAsset.cs @@ -1,75 +1,10 @@ using Murder.Assets; using Murder.Diagnostics; using Murder.Utilities.Attributes; +using Newtonsoft.Json; using System.Collections.Immutable; -namespace Murder.Editor.Assets.Graphics; - -public class SpriteEventData -{ - public readonly Dictionary> Events = new(); - - public readonly Dictionary> DeletedEvents = new(); - - public Dictionary GetEventsForAnimation(string animation) - { - if (!Events.TryGetValue(animation, out var dictionary)) - { - dictionary = new(); - Events[animation] = dictionary; - } - - return dictionary; - } - - public void AddEvent(string animation, int frame, string message) - { - Dictionary dict = GetEventsForAnimation(animation); - dict[frame] = message; - - RemoveFromDeletedEvents(animation, frame); - } - - public void RemoveEvent(string animation, int frame) - { - if (!Events.TryGetValue(animation, out var dictionary)) - { - AddToDeletedEvent(animation, frame); - return; - } - - bool hadValueDefined = dictionary.Remove(frame); - if (!hadValueDefined) - { - // Value was previously not tracked here. It is likely added by aseprite. - // So let's override that! - AddToDeletedEvent(animation, frame); - } - } - - private void AddToDeletedEvent(string animation, int frame) - { - if (!DeletedEvents.TryGetValue(animation, out var dictionary)) - { - dictionary = new(); - DeletedEvents[animation] = dictionary; - } - - dictionary.Add(frame); - } - - private bool RemoveFromDeletedEvents(string animation, int frame) - { - if (!DeletedEvents.TryGetValue(animation, out var dictionary)) - { - return false; - } - - return dictionary.Remove(frame); - } - - public SpriteEventData() { } -} +namespace Murder.Editor.Assets; [RuntimeOnly] internal class SpriteEventDataManagerAsset : GameAsset @@ -79,6 +14,7 @@ internal class SpriteEventDataManagerAsset : GameAsset /// public override string EditorFolder => "_Hidden"; + [JsonProperty] public ImmutableDictionary Events { get; private set; } = ImmutableDictionary.Empty; @@ -95,7 +31,7 @@ public bool DeleteSprite(Guid spriteId) return false; } - + public SpriteEventData GetOrCreate(Guid spriteId) { if (Events.TryGetValue(spriteId, out SpriteEventData? spriteEventData)) @@ -119,7 +55,7 @@ public SpriteEventData GetOrCreate(Guid spriteId) } ImmutableDictionary assets = Architect.EditorData.FilterAllAssets(typeof(SpriteEventDataManagerAsset)); - if (assets.Count >= 1) + if (assets.Count > 1) { GameLogger.Warning("How did we end up with more than one manager assets?"); } diff --git a/src/Murder.Editor/CustomEditors/SpriteEditor.cs b/src/Murder.Editor/CustomEditors/SpriteEditor.cs index a79487ba1..a331b1eb8 100644 --- a/src/Murder.Editor/CustomEditors/SpriteEditor.cs +++ b/src/Murder.Editor/CustomEditors/SpriteEditor.cs @@ -7,7 +7,7 @@ using Murder.Core.Graphics; using Murder.Core.Sounds; using Murder.Diagnostics; -using Murder.Editor.Assets.Graphics; +using Murder.Editor.Assets; using Murder.Editor.Attributes; using Murder.Editor.CustomFields; using Murder.Editor.ImGuiExtended; @@ -140,6 +140,9 @@ private void AddMessage(string animation, int frame, string message) // Also, let the sprite know that this is a thing now. _sprite.AddMessageToAnimationFrame(animation, frame, message); + _sprite.TrackAssetOnSave(manager.Guid); + + manager.FileChanged = true; } private void DeleteMessage(string animation, int frame) @@ -152,6 +155,9 @@ private void DeleteMessage(string animation, int frame) data.RemoveEvent(animation, frame); _sprite.RemoveMessageFromAnimationFrame(animation, frame); + _sprite.TrackAssetOnSave(manager.Guid); + + manager.FileChanged = true; } private void DrawFirstColumn(SpriteInformation info) diff --git a/src/Murder.Editor/CustomFields/LocalizedStringField.cs b/src/Murder.Editor/CustomFields/LocalizedStringField.cs index 2bdee9bb2..73b99b8a2 100644 --- a/src/Murder.Editor/CustomFields/LocalizedStringField.cs +++ b/src/Murder.Editor/CustomFields/LocalizedStringField.cs @@ -54,7 +54,8 @@ public override (bool modified, object? result) ProcessInput(EditorMember member localization.SetResource(data); modified = true; - Architect.EditorData.SaveAsset(localization); + EditorServices.SaveAssetWhenSelectedAssetIsSaved(localization.Guid); + localization.FileChanged = true; } return (modified, localizedString); diff --git a/src/Murder.Editor/Data/Dialogs/GumToMurderConverter.cs b/src/Murder.Editor/Data/Dialogs/GumToMurderConverter.cs index d39dc7962..a2591a308 100644 --- a/src/Murder.Editor/Data/Dialogs/GumToMurderConverter.cs +++ b/src/Murder.Editor/Data/Dialogs/GumToMurderConverter.cs @@ -78,6 +78,7 @@ public void ReloadDialogWith(GumData.CharacterScript script, CharacterAsset asse asset.RemoveCustomComponents(_components.Keys.Where(t => !_matchedComponents.Contains(t))); localizationAsset.SetResourcesForDialogue(asset.Guid, _localizedStrings.ToImmutable()); + Architect.EditorData.SaveAsset(localizationAsset); } private Dictionary FetchResourcesForAsset(LocalizationAsset localizationAsset, Guid characterGuid) diff --git a/src/Murder.Editor/Data/EditorDataManager.cs b/src/Murder.Editor/Data/EditorDataManager.cs index aa7518ec8..b667bf807 100644 --- a/src/Murder.Editor/Data/EditorDataManager.cs +++ b/src/Murder.Editor/Data/EditorDataManager.cs @@ -478,6 +478,22 @@ public void SaveAsset(T asset) where T : GameAsset FileHelper.CreateDirectoryPathIfNotExists(binPath); FileHelper.SaveSerialized(asset, binPath); } + + // Also save any extra assets at this point. + List? saveAssetsOnSave = asset.AssetsToBeSaved(); + if (saveAssetsOnSave is not null) + { + foreach (Guid g in saveAssetsOnSave) + { + GameAsset? a = TryGetAsset(g); + if (a is null) + { + continue; + } + + SaveAsset(a); + } + } } private GameAsset? GetAssetByName(string name) diff --git a/src/Murder.Editor/Data/EditorDataManager_Sprites.cs b/src/Murder.Editor/Data/EditorDataManager_Sprites.cs index befeaf9db..e424d03ce 100644 --- a/src/Murder.Editor/Data/EditorDataManager_Sprites.cs +++ b/src/Murder.Editor/Data/EditorDataManager_Sprites.cs @@ -127,6 +127,8 @@ private void FetchResourcesForImporters(bool reload, bool skipIfNoChangesFound) return; } + bool foundChanges = false; + DateTime lastTimeFetched = reload ? EditorSettings.LastHotReloadImport : EditorSettings.LastImported; string rawResourcesPath = FileHelper.GetPath(EditorSettings.RawResourcesPath); @@ -178,12 +180,20 @@ private void FetchResourcesForImporters(bool reload, bool skipIfNoChangesFound) !Directory.Exists(importer.GetSourcePackedPath()) || !Directory.Exists(importer.GetSourceResourcesPath()); + bool changed = hasInitializedAtlas || File.GetLastWriteTime(file) > lastTimeFetched; + foundChanges |= changed; + // If everything is good so far, put it on stage and check for changes - importer.StageFile(file, hasInitializedAtlas || File.GetLastWriteTime(file) > lastTimeFetched); + importer.StageFile(file, changed); break; } } + if (!foundChanges) + { + return; + } + EditorSettings.LastHotReloadImport = DateTime.Now; if (!reload) @@ -193,6 +203,5 @@ private void FetchResourcesForImporters(bool reload, bool skipIfNoChangesFound) SaveAsset(Architect.EditorSettings); } - } } \ No newline at end of file diff --git a/src/Murder.Editor/Data/Graphics/AsepriteImporter.cs b/src/Murder.Editor/Data/Graphics/AsepriteImporter.cs index 4d308a47f..3f0d5fee1 100644 --- a/src/Murder.Editor/Data/Graphics/AsepriteImporter.cs +++ b/src/Murder.Editor/Data/Graphics/AsepriteImporter.cs @@ -3,14 +3,13 @@ using Murder.Core.Graphics; using Murder.Data; using Murder.Diagnostics; +using Murder.Editor.Assets; using Murder.Serialization; using Murder.Utilities; using System.Collections.Immutable; using System.IO.Compression; using System.Security.Cryptography; using System.Text; -using static Murder.Editor.Data.Graphics.Aseprite; -using static Murder.Editor.Data.Graphics.Aseprite.Layer; using Color = Microsoft.Xna.Framework.Color; // Gist from: @@ -748,6 +747,11 @@ private SpriteAsset CreateAsset(int layer, int sliceIndex, AtlasId atlas) slice = Slices[sliceIndex]; } + (bool baked, Guid guid) = GetGuid(layer, sliceIndex); + + SpriteEventData? spriteEventData = null; + SpriteEventDataManagerAsset.TryGet()?.Events.TryGetValue(guid, out spriteEventData); + // Create an empty animation with all frames { int[] frames; @@ -769,11 +773,17 @@ private SpriteAsset CreateAsset(int layer, int sliceIndex, AtlasId atlas) } else { - frames = new int[] { 0 }; - durations = new float[] { Frames[0].Duration }; + frames = [0]; + durations = [Frames[0].Duration]; } dictBuilder[string.Empty] = new Animation(frames, durations, events, null); + + // Now that we have a guid, make sure if there are any extra events we need to pull. + if (spriteEventData is not null) + { + spriteEventData.FilterEventsForAnimation(string.Empty, ref events); + } } for (int i = 0; i < Tags.Count; i++) // Create individual animations for tags @@ -831,15 +841,20 @@ private SpriteAsset CreateAsset(int layer, int sliceIndex, AtlasId atlas) } break; } - } else { - frames = new int[] { 0 }; - durations = new float[] { Frames[0].Duration }; + frames = [0]; + durations = [Frames[0].Duration]; FindEventsInframe(events, 0, 0); } + // Now that we have a guid, make sure if there are any extra events we need to pull. + if (spriteEventData is not null) + { + spriteEventData.FilterEventsForAnimation(tag.Name, ref events); + } + dictBuilder[tag.Name] = new Animation(frames, durations, events, AnimationSequence.CreateIfPossible(tag.UserData.Text)); } @@ -853,8 +868,6 @@ private SpriteAsset CreateAsset(int layer, int sliceIndex, AtlasId atlas) framesBuilder.Add($"{source}"); Point pivot = slice?.Pivot != null ? new Point(slice.Pivot.Value.X, slice.Pivot.Value.Y) : Point.Zero; - (bool baked, Guid guid) = GetGuid(layer, sliceIndex); - // No slice or just get the first SpriteAsset asset = new( guid: guid, diff --git a/src/Murder.Editor/EditorScene_Selector.cs b/src/Murder.Editor/EditorScene_Selector.cs index 2c0a1bf79..b43414b50 100644 --- a/src/Murder.Editor/EditorScene_Selector.cs +++ b/src/Murder.Editor/EditorScene_Selector.cs @@ -126,7 +126,7 @@ private void DrawAssetFolder(string folderName, Vector4 color, Type? createType, private void DrawAssetFolder(string folderName, Vector4 color, Type? createType, IEnumerable assets, int depth, string folderRootPath, bool unfoldAll) { - if (folderName.StartsWith(GameDataManager.SKIP_CHAR)) + if (folderName.StartsWith(GameDataManager.SKIP_CHAR) || folderName.StartsWith("_")) { // Skip folders that start with "_". return; diff --git a/src/Murder.Editor/Services/EditorLocalizationServices.cs b/src/Murder.Editor/Services/EditorLocalizationServices.cs index b2adfc6de..a529a3b49 100644 --- a/src/Murder.Editor/Services/EditorLocalizationServices.cs +++ b/src/Murder.Editor/Services/EditorLocalizationServices.cs @@ -7,33 +7,6 @@ namespace Murder.Editor.Services; internal static class EditorLocalizationServices { - public static LocalizedString? AddNewResource() - { - LocalizedString result = new(Guid.NewGuid()); - - LocalizationAsset asset = Game.Data.Localization; - asset.AddResource(result.Id); - - Architect.EditorData.SaveAsset(asset); - return result; - } - - public static void AddExistingResource(Guid g) - { - LocalizationAsset asset = Game.Data.Localization; - asset.AddResource(g); - - Architect.EditorData.SaveAsset(asset); - } - - public static void RemoveResource(Guid id) - { - LocalizationAsset asset = Game.Data.Localization; - asset.RemoveResource(id); - - Architect.EditorData.SaveAsset(asset); - } - public static LocalizedString? SearchLocalizedString() { LocalizationAsset localization = LocalizationServices.GetCurrentLocalization(); @@ -89,4 +62,26 @@ public static void RemoveResource(Guid id) return null; } + + private static LocalizedString? AddNewResource() + { + LocalizedString result = new(Guid.NewGuid()); + + LocalizationAsset asset = Game.Data.Localization; + asset.AddResource(result.Id); + + EditorServices.SaveAssetWhenSelectedAssetIsSaved(asset.Guid); + asset.FileChanged = true; + + return result; + } + + private static void AddExistingResource(Guid g) + { + LocalizationAsset asset = Game.Data.Localization; + asset.AddResource(g); + + EditorServices.SaveAssetWhenSelectedAssetIsSaved(asset.Guid); + asset.FileChanged = true; + } } diff --git a/src/Murder.Editor/Services/EditorServices.cs b/src/Murder.Editor/Services/EditorServices.cs index 39734eff1..f20f2be69 100644 --- a/src/Murder.Editor/Services/EditorServices.cs +++ b/src/Murder.Editor/Services/EditorServices.cs @@ -465,5 +465,17 @@ public static bool DrawPolygonHandles(Polygon polygon, RenderContext render, Vec return modified; } + + public static bool SaveAssetWhenSelectedAssetIsSaved(Guid guidToTrackOnSelectedAssetSaved) + { + if (Architect.Instance.ActiveScene is EditorScene editorScene) + { + editorScene.CurrentAsset?.TrackAssetOnSave(guidToTrackOnSelectedAssetSaved); + + return true; + } + + return false; + } } } \ No newline at end of file diff --git a/src/Murder/Assets/GameAsset.cs b/src/Murder/Assets/GameAsset.cs index 237929766..13dd72d9b 100644 --- a/src/Murder/Assets/GameAsset.cs +++ b/src/Murder/Assets/GameAsset.cs @@ -102,6 +102,12 @@ public bool Rename [JsonIgnore] public bool TaggedForDeletion = false; + /// + /// If set, this has a list of assets which should also be saved whenever this asset has been saved. + /// For example: localization resources or sprite event manager. + /// + private List? _saveAssetsOnSave = null; + public virtual void AfterDeserialized() { } public void MakeGuid() @@ -127,5 +133,28 @@ public GameAsset Duplicate(string name) /// This notifies it that it has been modified (usually by an editor). /// protected virtual void OnModified() { } + + /// + /// Track an asset such that is also saved once this asset is saved. + /// + public void TrackAssetOnSave(Guid g) + { + _saveAssetsOnSave ??= new(); + _saveAssetsOnSave.Add(g); + } + + /// + /// Return the assets which will be saved with this (). + /// Also clear the pending list. + /// + public List? AssetsToBeSaved() + { + if (_saveAssetsOnSave is null) return null; + + List result = _saveAssetsOnSave; + _saveAssetsOnSave = null; + + return result; + } } } \ No newline at end of file diff --git a/src/Murder/Assets/Localization/LocalizationAsset.cs b/src/Murder/Assets/Localization/LocalizationAsset.cs index d31f5f1be..98546b27e 100644 --- a/src/Murder/Assets/Localization/LocalizationAsset.cs +++ b/src/Murder/Assets/Localization/LocalizationAsset.cs @@ -129,6 +129,14 @@ public void UpdateOrSetResource(Guid id, string translated, string? notes) _resources = _resources.SetItem(index, value); } + /// + /// Used when setting data from a reference data. + /// + public void SetAllDialogueResources(ImmutableArray resources) + { + _dialogueResources = resources; + } + public LocalizedStringData? TryGetResource(Guid id) { if (!GuidToResourceIndex.TryGetValue(id, out int index)) @@ -141,14 +149,6 @@ public void UpdateOrSetResource(Guid id, string translated, string? notes) public bool HasResource(Guid id) => GuidToResourceIndex.ContainsKey(id); - /// - /// Used when setting data from a reference data. - /// - public void SetAllDialogueResources(ImmutableArray resources) - { - _dialogueResources = resources; - } - public void SetResourcesForDialogue(Guid guid, ImmutableArray resources) { for (int i = 0; i < _dialogueResources.Length; ++i)