diff --git a/src/Blocktavius.AppDQB2/Global.cs b/src/Blocktavius.AppDQB2/Global.cs index 548f761..9dcb6cb 100644 --- a/src/Blocktavius.AppDQB2/Global.cs +++ b/src/Blocktavius.AppDQB2/Global.cs @@ -70,6 +70,35 @@ public ItemCollection GetValues() } } + public sealed class SnippetsItemsSource : IItemsSource + { + public ItemCollection GetValues() + { + var list = new ItemCollection(); + if (currentProject != null) + { + foreach (var snippet in currentProject.ExtractedSnippets) + { + list.Add(snippet, snippet.Name); + } + } + return list; + } + } + + public sealed class RotationItemsSource : IItemsSource + { + public ItemCollection GetValues() + { + var list = new ItemCollection(); + list.Add(0); + list.Add(90); + list.Add(180); + list.Add(270); + return list; + } + } + public static IEnumerable LogicalAncestors(this DependencyObject obj) { do diff --git a/src/Blocktavius.AppDQB2/Persistence/ResourceDeserializationContext.cs b/src/Blocktavius.AppDQB2/Persistence/ResourceDeserializationContext.cs new file mode 100644 index 0000000..84327b9 --- /dev/null +++ b/src/Blocktavius.AppDQB2/Persistence/ResourceDeserializationContext.cs @@ -0,0 +1,9 @@ +using Blocktavius.AppDQB2.Resources; + +namespace Blocktavius.AppDQB2.Persistence; + +sealed class ResourceDeserializationContext +{ + public required IAreaManager AreaManager { get; init; } + public required IReadOnlyList Slots { get; init; } +} diff --git a/src/Blocktavius.AppDQB2/Persistence/ScriptDeserializationContext.cs b/src/Blocktavius.AppDQB2/Persistence/ScriptDeserializationContext.cs index 04809d5..b8388e6 100644 --- a/src/Blocktavius.AppDQB2/Persistence/ScriptDeserializationContext.cs +++ b/src/Blocktavius.AppDQB2/Persistence/ScriptDeserializationContext.cs @@ -1,13 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Blocktavius.AppDQB2.Resources; namespace Blocktavius.AppDQB2.Persistence; public sealed class ScriptDeserializationContext { + internal ScriptDeserializationContext(IReadOnlyList snippets) + { + this.Snippets = snippets; + } + public required IAreaManager AreaManager { get; init; } public required IBlockManager BlockManager { get; init; } + internal IReadOnlyList Snippets { get; } } diff --git a/src/Blocktavius.AppDQB2/Persistence/V1/ProjectV1.cs b/src/Blocktavius.AppDQB2/Persistence/V1/ProjectV1.cs index 69cb013..c4e729f 100644 --- a/src/Blocktavius.AppDQB2/Persistence/V1/ProjectV1.cs +++ b/src/Blocktavius.AppDQB2/Persistence/V1/ProjectV1.cs @@ -49,6 +49,13 @@ public required IReadOnlyList? Scripts public required int? SelectedScriptIndex { get; init; } + private IContentEqualityList? _extractedSnippets = null; + public required IReadOnlyList? ExtractedSnippets + { + get => _extractedSnippets; + init => _extractedSnippets = value?.ToContentEqualityList(); + } + public ProjectV1 VerifyProfileHash(ProfileSettings profile) { if (profile.VerificationHash == ProfileVerificationHash) @@ -170,10 +177,25 @@ sealed record RectV1 public required int X1 { get; init; } public required int Z1 { get; init; } - public Rect ToCoreRect() + /// + /// Most of the time the user is entering all 4 values inclusively, but core algorithms + /// want the end to be exclusive. + /// + public Rect ToCoreRectInclusive() { var start = new XZ(Math.Min(X0, X1), Math.Min(Z0, Z1)); - var end = new XZ(Math.Max(X0, X1), Math.Max(Z0, Z1)); + var end = new XZ(Math.Max(X0, X1) + 1, Math.Max(Z0, Z1) + 1); return new Rect(start, end); } } + +sealed record ExtractedSnippetV1 +{ + public required string? PersistentId { get; init; } + public required string? Name { get; init; } + public required SlotReferenceV1? SourceSlot { get; init; } + public required string? SourceStgdatFilename { get; init; } + public required string? AreaPersistentId { get; init; } + public required RectV1? CustomRectArea { get; init; } + public required int? FloorY { get; init; } +} diff --git a/src/Blocktavius.AppDQB2/ProjectControl.xaml b/src/Blocktavius.AppDQB2/ProjectControl.xaml index 152b06f..22dd5e9 100644 --- a/src/Blocktavius.AppDQB2/ProjectControl.xaml +++ b/src/Blocktavius.AppDQB2/ProjectControl.xaml @@ -34,6 +34,12 @@ + + + Resources + + + diff --git a/src/Blocktavius.AppDQB2/ProjectVM.cs b/src/Blocktavius.AppDQB2/ProjectVM.cs index cc2c379..3c731d9 100644 --- a/src/Blocktavius.AppDQB2/ProjectVM.cs +++ b/src/Blocktavius.AppDQB2/ProjectVM.cs @@ -15,6 +15,7 @@ using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; +using Blocktavius.AppDQB2.Persistence; namespace Blocktavius.AppDQB2; @@ -59,6 +60,7 @@ sealed partial class ProjectVM : ViewModelBaseWithCustomTypeDescriptor, IBlockLi // commands public I.Project.CommandEditChunkGrid CommandEditChunkGrid { get; } public ICommand CommandAddScript { get; } + public ICommand CommandAddSnippet { get; } // wrapper properties private ProfileSettings getProfile => xProfile.Value; @@ -113,10 +115,18 @@ public ProjectVM(IServices services, ProfileSettings profile) CommandExportChunkMask = new RelayCommand(_ => chunkGridLayer.ChunkGridImage != null, ExportChunkMask); CommandExportMinimap = new RelayCommand(_ => minimapLayer?.MinimapImage != null, ExportMinimap); CommandAddScript = new RelayCommand(_ => true, AddScript); + CommandAddSnippet = new RelayCommand(_ => true, AddSnippet); ForceUpdateProfile(profile); } + private void AddSnippet(object? _) + { + var newResource = new Resources.ExtractedSnippetResourceVM(); + newResource.Name = $"New Snippet {ExtractedSnippets.Count + 1}"; + ExtractedSnippets.Add(newResource); + } + private void ExportChunkMask(object? arg) => ExportImage(chunkGridLayer.ChunkGridImage, "exported-chunk-mask.png", 1.0); private void ExportMinimap(object? arg) => ExportImage(minimapLayer?.MinimapImage, "exported-minimap.png", 0.5); @@ -248,6 +258,11 @@ public ILayerVM? SelectedLayer public ObservableCollection Scripts { get; } = new(); + public ObservableCollection ExtractedSnippets { get; } = new(); + + public IReadOnlyList AvailableAreas + => Layers.Select(layer => layer.SelfAsAreaVM).WhereNotNull().ToList(); + private ScriptVM? _selectedScript; public ScriptVM? SelectedScript { @@ -302,7 +317,7 @@ private void ExpandChunks(IReadOnlySet expansion) return null; } - var context = new StageRebuildContext(workingStage); + var context = new StageRebuildContext(workingStage, stageLoader); var mutation = this.SelectedScript?.BuildMutation(context); if (mutation != null) { @@ -393,6 +408,7 @@ public ProjectV1 ToPersistModel() ChunkGridVisible = chunkGridLayer.IsVisible, Scripts = this.Scripts.Select(s => s.ToPersistModelConcrete()).ToList(), SelectedScriptIndex = SelectedScript == null ? null : Scripts.IndexOf(SelectedScript), + ExtractedSnippets = this.ExtractedSnippets.Select(s => s.ToPersistModel()).ToList(), }; } @@ -455,7 +471,20 @@ private void Reload(ProjectV1 project, ExternalImageManager imageManager) Layers.Add(chunkGridLayer); SelectedLayer = Layers.FirstOrDefault(); - var scriptContext = new Persistence.ScriptDeserializationContext + var resourceContext = new Persistence.ResourceDeserializationContext + { + AreaManager = this, + Slots = getSourceSlots, + }; + + ExtractedSnippets.Clear(); + foreach (var snippet in project.ExtractedSnippets.EmptyIfNull()) + { + var snippetVM = Resources.ExtractedSnippetResourceVM.Load(snippet, resourceContext); + ExtractedSnippets.Add(snippetVM); + } + + var scriptContext = new Persistence.ScriptDeserializationContext(ExtractedSnippets) { AreaManager = this, BlockManager = this, @@ -469,6 +498,9 @@ private void Reload(ProjectV1 project, ExternalImageManager imageManager) } SelectedScript = Scripts.ElementAtOrDefault(project.SelectedScriptIndex.GetValueOrDefault(-1)); + + + changeset.Complete(); } @@ -490,6 +522,7 @@ private void Reload(ProjectV1 project, ExternalImageManager imageManager) return Blockdata.AllBlockVMs.FirstOrDefault(x => x.PersistentId == persistentId); } + static partial class MyProperty { public sealed class Profile : NotnullOriginProp, I.Project.Profile { } diff --git a/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceEditor.xaml b/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceEditor.xaml new file mode 100644 index 0000000..c3c6c59 --- /dev/null +++ b/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceEditor.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceEditor.xaml.cs b/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceEditor.xaml.cs new file mode 100644 index 0000000..efd4296 --- /dev/null +++ b/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceEditor.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace Blocktavius.AppDQB2.Resources +{ + /// + /// Interaction logic for ExtractedSnippetResourceEditor.xaml + /// + public partial class ExtractedSnippetResourceEditor : UserControl + { + public ExtractedSnippetResourceEditor() + { + InitializeComponent(); + } + } +} diff --git a/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceVM.cs b/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceVM.cs new file mode 100644 index 0000000..09c49af --- /dev/null +++ b/src/Blocktavius.AppDQB2/Resources/ExtractedSnippetResourceVM.cs @@ -0,0 +1,135 @@ +using Blocktavius.AppDQB2.ScriptNodes; +using Blocktavius.AppDQB2.Persistence.V1; +using Blocktavius.AppDQB2.Persistence; +using System.Linq; +using Blocktavius.Core; +using Blocktavius.DQB2; + +namespace Blocktavius.AppDQB2.Resources; + +interface ISnippetVM +{ + Snippet? LoadSnippet(StageRebuildContext context); +} + +sealed class ExtractedSnippetResourceVM : ViewModelBase, ISnippetVM +{ + private string _name = "New Snippet"; + public string Name + { + get => _name; + set => ChangeProperty(ref _name, value); + } + + private int _floorY = 1; + public int FloorY + { + get => _floorY; + set => ChangeProperty(ref _floorY, value); + } + + public string PersistentId { get; private set; } = Guid.NewGuid().ToString(); + + private SlotVM? _sourceSlot; + public SlotVM? SourceSlot + { + get => _sourceSlot; + set + { + if (ChangeProperty(ref _sourceSlot, value)) + { + SourceStage = null; + } + } + } + + private SlotStageVM? _sourceStage; + public SlotStageVM? SourceStage + { + get => _sourceStage; + set => ChangeProperty(ref _sourceStage, value); + } + + public AreaDefinerVM AreaDefiner { get; } = new(); + + public ExtractedSnippetV1 ToPersistModel() + { + return new ExtractedSnippetV1 + { + PersistentId = this.PersistentId, + Name = this.Name, + SourceSlot = this.SourceSlot?.ToPersistModel(), + SourceStgdatFilename = this.SourceStage?.Filename, + AreaPersistentId = this.AreaDefiner.Area?.PersistentId, + CustomRectArea = this.AreaDefiner.RebuildCustomRect(), + FloorY = this.FloorY, + }; + } + + public static ExtractedSnippetResourceVM Load(ExtractedSnippetV1 persistModel, ResourceDeserializationContext context) + { + var vm = new ExtractedSnippetResourceVM + { + PersistentId = persistModel.PersistentId ?? Guid.NewGuid().ToString(), + Name = persistModel.Name ?? "Unnamed Snippet", + FloorY = persistModel.FloorY ?? 1, + }; + + // Lookup Source Slot + if (persistModel.SourceSlot != null) + { + vm.SourceSlot = context.Slots.FirstOrDefault( + s => s.MatchesByNumber(persistModel.SourceSlot) || s.MatchesByName(persistModel.SourceSlot)); + } + + // Lookup Source Stage + if (persistModel.SourceStgdatFilename != null && vm.SourceSlot != null) + { + vm.SourceStage = vm.SourceSlot.Stages.EmptyIfNull().FirstOrDefault( + s => string.Equals(s.Filename, persistModel.SourceStgdatFilename, StringComparison.OrdinalIgnoreCase)); + } + + // AreaDefiner + if (persistModel.AreaPersistentId != null) + { + vm.AreaDefiner.Area = context.AreaManager.FindArea(persistModel.AreaPersistentId); + } + else if (persistModel.CustomRectArea != null) + { + vm.AreaDefiner.Load(persistModel.CustomRectArea); + } + + return vm; + } + + public Snippet? LoadSnippet(StageRebuildContext context) + { + if (SourceStage == null) + { + return null; + } + + var bounds = AreaDefiner.RebuildCustomRect()?.ToCoreRectInclusive(); + IArea? area = null; + if (bounds == null && true == AreaDefiner.Area?.IsArea(context.ImageCoordTranslation, out var areaWrapper)) + { + area = areaWrapper.Area; + bounds = areaWrapper.Area.Bounds; + } + if (bounds == null) + { + return null; + } + + var loadResult = Task.Run(async () => await context.StageLoader.LoadStage(SourceStage.StgdatFile)).GetAwaiter().GetResult(); + if (loadResult?.Stage == null) + { + return null; + } + + // TODO!!! Need to respect area if not null here! + // Probably Snippet.Create should accept an IArea or a Rect + var snippet = Snippet.Create(loadResult.Stage, bounds, floorY: FloorY); + return snippet; + } +} diff --git a/src/Blocktavius.AppDQB2/ResourcesControl.xaml b/src/Blocktavius.AppDQB2/ResourcesControl.xaml new file mode 100644 index 0000000..ac82aec --- /dev/null +++ b/src/Blocktavius.AppDQB2/ResourcesControl.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + +