From 3b22e8a2decc8b7e82cdde2b8ac8aee91bb83a1e Mon Sep 17 00:00:00 2001 From: Haven1433 Date: Wed, 15 Dec 2021 08:27:47 -0600 Subject: [PATCH] Allow pokemon sprite import to import normal/shiny/front/back sets There's a lot of resources available that use a 4x wide sprite to render the pokemon normal and shiny, front and back, all as a single image. People want to be able to import this all at once. Now they can. As a side effect, pokemon will always show their normal palette as their first palette now, with the shiny palette always being second. This only really works with single-frame sprites for now, so Ruby/FireRed, not Emerald. --- .../Models/Runs/Sprites/Sprites.cs | 17 +++- .../ViewModels/Tools/SpriteTool.cs | 77 ++++++++++++++----- 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/Sprites/Sprites.cs b/src/HexManiac.Core/Models/Runs/Sprites/Sprites.cs index 8d3e8272..d83c2030 100644 --- a/src/HexManiac.Core/Models/Runs/Sprites/Sprites.cs +++ b/src/HexManiac.Core/Models/Runs/Sprites/Sprites.cs @@ -161,7 +161,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites { } else if (spriteTable != null) { // option 5: this sprite is in an array, so get all palettes in all related arrays if (spriteTable is ArrayRun arrayRun) { - foreach (var relatedTable in model.GetRelatedArrays(arrayRun)) { + foreach (var relatedTable in GetLengthSortedRelatedArrays(model, arrayRun)) { if (relatedTable == spriteTable) continue; // skip self, I'll handle my own relation later results.AddRange(model.GetPointedChildren(relatedTable, offset.ElementIndex)); } @@ -201,7 +201,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites { // find all sprites within tables of the same length that reference this table or reference nothing at all if (tableRun is ArrayRun arrayRun) { - foreach (var table in model.GetRelatedArrays(arrayRun)) { + foreach (var table in GetLengthSortedRelatedArrays(model, arrayRun)) { var elementOffset = table.ElementLength * offset.ElementIndex; foreach (var spriteRun in model.GetPointedChildren(table, offset.ElementIndex)) { if (spriteRun is LzTilemapRun) continue; // don't count tilemaps @@ -315,5 +315,18 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites { return results; } + + /// + /// Returns all related arrays, sorted with the longer anchor names first. + /// This unusual strategy allows 'front' before 'back' and 'normal' before 'shiny'. + /// + public static List GetLengthSortedRelatedArrays(IDataModel model, ArrayRun arrayRun) { + var relatedArrays = model.GetRelatedArrays(arrayRun).ToList(); + + // sort by name length + relatedArrays.Sort((a, b) => model.GetAnchorFromAddress(-1, b.Start).Length - model.GetAnchorFromAddress(-1, a.Start).Length); + + return relatedArrays; + } } } diff --git a/src/HexManiac.Core/ViewModels/Tools/SpriteTool.cs b/src/HexManiac.Core/ViewModels/Tools/SpriteTool.cs index 15c1c2d9..71a608bf 100644 --- a/src/HexManiac.Core/ViewModels/Tools/SpriteTool.cs +++ b/src/HexManiac.Core/ViewModels/Tools/SpriteTool.cs @@ -673,12 +673,31 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (image == null) return; if (!TryValidate(image, out var spriteRun, out var paletteRun)) return; int height = image.Length / width; + var relatedSprites = paletteRun.FindDependentSprites(model); + var relatedPalettes = spriteRun.FindRelatedPalettes(model); + int relatedImageCount = relatedSprites.Count * relatedPalettes.Count; if (width == PixelWidth && height == PixelHeight) { ImportSinglePageSpriteAndPalette(fileSystem, image, spriteRun, paletteRun); } else if (width == PixelWidth * spritePages && height == PixelHeight) { ImportWideSpriteAndPalette(fileSystem, image, spriteRun, paletteRun); } else if (width == PixelWidth && height == PixelHeight * spritePages) { ImportTallSpriteAndPalette(fileSystem, image, spriteRun, paletteRun); + } else if (width == PixelWidth * relatedImageCount && height == PixelHeight) { + var images = SplitHorizontally(image, width, relatedImageCount); + var desiredImportType = ImportType.Greedy; + for (int i = 0; i < relatedSprites.Count; i++) { + for (int j = 0; j < relatedPalettes.Count; j++) { + var imageIndex = relatedPalettes.Count * i + j; + ImportSinglePageSpriteAndPalette(fileSystem, images[imageIndex], relatedSprites[i], relatedPalettes[j], desiredImportType); + + // import may have repointed sprites/palettes: refresh the related sprites/palettes cached locations. + spriteRun = model.GetNextRun(spriteAddress) as ISpriteRun; + paletteRun = model.GetNextRun(paletteAddress) as IPaletteRun; + relatedSprites = paletteRun.FindDependentSprites(model); + relatedPalettes = spriteRun.FindRelatedPalettes(model); + } + desiredImportType = ImportType.Cautious; + } } else { var displayWidth = PixelWidth; var displayHeight = PixelHeight; @@ -688,6 +707,19 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { } } + private short[][] SplitHorizontally(short[] image, int width, int count) { + var images = new short[count][]; + var newWidth = width / count; + for (var i = 0; i < count; i++) images[i] = new short[image.Length / count]; + for (int i = 0; i < image.Length; i++) { + var j = (i / newWidth) % count; + var y = i / width; + var x = i % newWidth; + images[j][newWidth * y + x] = image[i]; + } + return images; + } + public static (int index, int page) GetTarget(IReadOnlyList sprites, int target) { var (totalCount, initialTarget) = (0, target); for (int i = 0; i < sprites.Count; i++) { @@ -699,7 +731,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { throw new IndexOutOfRangeException($"Looking for page {initialTarget}, but there were only {totalCount} available pages among {sprites.Count} sprites!"); } - private int GetImportType(IFileSystem fileSystem, int dependentPageCount, IReadOnlyList dependentSprites, string imageType, IPaletteRun palette, out IReadOnlyList usablePalettePages) { + private ImportType GetImportType(IFileSystem fileSystem, int dependentPageCount, IReadOnlyList dependentSprites, string imageType, IPaletteRun palette, ImportType type, out IReadOnlyList usablePalettePages) { + usablePalettePages = palette.Pages.Range().ToList(); + if (type != ImportType.Unknown) return type; var palFormat = palette.PaletteFormat; var imageDetails = new List(); var detailsLength = Math.Min(3, dependentPageCount); @@ -723,26 +757,26 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { $"This palette is used by {dependentPageCount} {imageType}. How do you want to handle the import?", allDetails.ToArray(), new VisualOption { - Option = "Smart", Index = 0, + Option = "Smart", Index = (int)ImportType.Smart, ShortDescription = "Fix Incompatibilites Automatically", Description = "Balance the look of the new image with other images already using this palette.", }, new VisualOption { - Option = "Greedy", Index = 1, + Option = "Greedy", Index = (int)ImportType.Greedy, ShortDescription = "Ignore other sprites", Description = "Ignore other images that use this palette. They'll probably look broken and that's ok.", }, new VisualOption { - Option = "Cautious", Index = 2, + Option = "Cautious", Index = (int)ImportType.Cautious, ShortDescription = "Don't change palette", Description = "Match the new image to the existing palette as closely as possible.", }); usablePalettePages = palDetails.Count.Range().Where(i => palDetails[i].IsSet).ToList(); - return chosenOption; + return (ImportType)chosenOption; } - private void ImportSinglePageSpriteAndPalette(IFileSystem fileSystem, short[] image, ISpriteRun spriteRun, IPaletteRun paletteRun) { + private void ImportSinglePageSpriteAndPalette(IFileSystem fileSystem, short[] image, ISpriteRun spriteRun, IPaletteRun paletteRun, ImportType importType = ImportType.Unknown) { var dependentSprites = paletteRun?.FindDependentSprites(model) ?? new List(); if (dependentSprites.Count == 0 || // no sprites are associated with this palette. So just use the currently loaded sprite. (dependentSprites.Count == 1 && dependentSprites[0].Start == spriteRun.Start && (spriteRun.Pages == 1 || spriteRun.Pages == paletteRun.Pages)) || // 'I am the only sprite' case @@ -761,14 +795,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (paletteRun.Pages > 1) dependentPageCount = dependentSprites.Count; // for multi-page palettes, only count each sprite once. string imageType = "images"; if (dependentSprites.Any(ds => ds is ITilesetRun)) imageType = "tilesets"; - var choice = GetImportType(fileSystem, dependentPageCount, dependentSprites, imageType, paletteRun, out var usablePalPages); + var choice = GetImportType(fileSystem, dependentPageCount, dependentSprites, imageType, paletteRun, importType, out var usablePalPages); - if (choice == 0) { // Smart + if (choice == ImportType.Smart) { var otherSprites = dependentSprites.Except(new[] { spriteRun }).ToList(); WriteSpriteAndBalancePalette(spriteRun, paletteRun, otherSprites, image, usablePalPages); - } else if (choice == 1) { // Greedy + } else if (choice == ImportType.Greedy) { WriteSpriteAndPalette(spriteRun, paletteRun, image, usablePalPages); - } else if (choice == 2) { // Cautious + } else if (choice == ImportType.Cautious) { WriteSpriteWithoutPalette(spriteRun, paletteRun, image, usablePalPages); } @@ -804,14 +838,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (paletteRun.Pages > 1) dependentPageCount = dependentSprites.Count; // for multi-page palettes, only count each sprite once. string imageType = "images"; if (dependentSprites.Any(ds => ds is ITilesetRun)) imageType = "tilesets"; - var choice = GetImportType(fileSystem, dependentPageCount, dependentSprites, imageType, paletteRun, out var usablePalPages); + var choice = GetImportType(fileSystem, dependentPageCount, dependentSprites, imageType, paletteRun, ImportType.Unknown, out var usablePalPages); - if (choice == 0) { // Smart + if (choice == ImportType.Smart) { var otherSprites = dependentSprites.Except(new[] { spriteRun }).ToList(); WriteSpritesAndBalancePalette(spriteRun, paletteRun, otherSprites, imageForPage, usablePalPages); - } else if (choice == 1) { // Greedy + } else if (choice == ImportType.Greedy) { WriteSpritesAndPalette(spriteRun, paletteRun, imageForPage, usablePalPages); - } else if (choice == 2) { // Cautious + } else if (choice == ImportType.Cautious) { WriteSpritesWithoutPalette(spriteRun, paletteRun, imageForPage, usablePalPages); } @@ -844,14 +878,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (paletteRun.Pages > 1) dependentPageCount = dependentSprites.Count; // for multi-page palettes, only count each sprite once. string imageType = "images"; if (dependentSprites.Any(ds => ds is ITilesetRun)) imageType = "tilesets"; - var choice = GetImportType(fileSystem, dependentPageCount, dependentSprites, imageType, paletteRun, out var usablePalPages); + var choice = GetImportType(fileSystem, dependentPageCount, dependentSprites, imageType, paletteRun, ImportType.Unknown, out var usablePalPages); - if (choice == 0) { // Smart + if (choice == ImportType.Smart) { var otherSprites = dependentSprites.Except(new[] { spriteRun }).ToList(); WriteSpritesAndBalancePalette(spriteRun, paletteRun, otherSprites, imageForPage, usablePalPages); - } else if (choice == 1) { // Greedy + } else if (choice == ImportType.Greedy) { WriteSpritesAndPalette(spriteRun, paletteRun, imageForPage, usablePalPages); - } else if (choice == 2) { // Cautious + } else if (choice == ImportType.Cautious) { WriteSpritesWithoutPalette(spriteRun, paletteRun, imageForPage, usablePalPages); } @@ -1414,6 +1448,13 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { } } + public enum ImportType { + Unknown = -1, + Smart = 0, + Greedy = 1, + Cautious = 2, + } + public class FlagViewModel : ViewModelCore { private string name; public string Name { get => name; set => Set(ref name, value); }