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.
This commit is contained in:
Haven1433 2021-12-15 08:27:47 -06:00
parent c9aaa8b4ba
commit 3b22e8a2de
2 changed files with 74 additions and 20 deletions

View File

@ -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<IPaletteRun>(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<ISpriteRun>(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;
}
/// <summary>
/// Returns all related arrays, sorted with the longer anchor names first.
/// This unusual strategy allows 'front' before 'back' and 'normal' before 'shiny'.
/// </summary>
public static List<ArrayRun> 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;
}
}
}

View File

@ -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<ISpriteRun> 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<ISpriteRun> dependentSprites, string imageType, IPaletteRun palette, out IReadOnlyList<int> usablePalettePages) {
private ImportType GetImportType(IFileSystem fileSystem, int dependentPageCount, IReadOnlyList<ISpriteRun> dependentSprites, string imageType, IPaletteRun palette, ImportType type, out IReadOnlyList<int> usablePalettePages) {
usablePalettePages = palette.Pages.Range().ToList();
if (type != ImportType.Unknown) return type;
var palFormat = palette.PaletteFormat;
var imageDetails = new List<object>();
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<ISpriteRun>();
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); }