'Add page' for compressed sprites/palettes

Existing images sometimes include multiple sprites/palettes within the same compressed data. Example: Castform. Make it possible to add extra pages through the tool.
This commit is contained in:
Benjamin Popp 2020-06-22 22:36:54 -05:00
parent fa7d3d70a8
commit 58344f4631
10 changed files with 176 additions and 9 deletions

View File

@ -7,3 +7,8 @@ indent_size = 3
trim_trailing_whitespace = true
insert_final_newline = true
csharp_new_line_before_open_brace = none
[*.xaml]
indent_style = space
indent_size = 3
trim_trailing_whitespace = true

View File

@ -81,5 +81,25 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites {
model.ObserveRunWritten(token, newRun);
return newRun;
}
public LzPaletteRun AppendPage(ModelDelta token) {
var data = Decompress(Model, Start);
var lastPage = Pages - 1;
var pageLength = (int)Math.Pow(2, PaletteFormat.Bits) * 2;
var newData = new byte[data.Length + pageLength];
Array.Copy(data, newData, data.Length);
Array.Copy(data, lastPage * pageLength, newData, data.Length, pageLength);
var newModelData = Compress(newData, 0, newData.Length);
var newRun = (LzPaletteRun)Model.RelocateForExpansion(token, this, newModelData.Count);
for (int i = 0; i < newModelData.Count; i++) token.ChangeData(Model, newRun.Start + i, newModelData[i]);
newRun = new LzPaletteRun(PaletteFormat, Model, newRun.Start, newRun.PointerSources);
Model.ObserveRunWritten(token, newRun);
return newRun;
}
public LzPaletteRun DeletePage(int page, ModelDelta token) {
throw new NotImplementedException();
}
}
}

View File

@ -68,5 +68,26 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites {
model.ObserveRunWritten(token, newRun);
return newRun;
}
public LzSpriteRun AppendPage(ModelDelta token) {
var data = Decompress(Model, Start);
var lastPage = Pages - 1;
var pageLength = SpriteFormat.TileWidth * SpriteFormat.TileHeight * 8 * SpriteFormat.BitsPerPixel;
var newData = new byte[data.Length + pageLength];
Array.Copy(data, newData, data.Length);
Array.Copy(data, lastPage * pageLength, newData, data.Length, pageLength);
var newModelData = Compress(newData, 0, newData.Length);
var newRun = (LzSpriteRun)Model.RelocateForExpansion(token, this, newModelData.Count);
for (int i = 0; i < newModelData.Count; i++) token.ChangeData(Model, newRun.Start + i, newModelData[i]);
newRun = new LzSpriteRun(SpriteFormat, Model, newRun.Start, newRun.PointerSources);
Model.ObserveRunWritten(token, newRun);
return newRun;
}
public LzSpriteRun DeletePage(int page, ModelDelta token) {
// TODO
throw new NotImplementedException();
}
}
}

View File

@ -149,7 +149,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs.Sprites {
foreach (var relatedTable in model.GetRelatedArrays(spriteTable)) {
results.AddRange(model.GetPointedChildren<IPaletteRun>(relatedTable, offset.ElementIndex));
}
results.AddRange(model.GetPointedChildren<IPaletteRun>(spriteTable, offset.ElementIndex));
}
return results;
}

View File

@ -109,10 +109,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
}
public interface IPagedViewModel : IStreamArrayElementViewModel {
bool HasMultiplePages { get; }
bool ShowPageControls { get; }
int Pages { get; }
int CurrentPage { get; set; }
ICommand PreviousPage { get; }
ICommand NextPage { get; }
ICommand AddPage { get; }
ICommand DeletePage { get; }
}
}

View File

@ -5,7 +5,7 @@ using System.Windows.Input;
namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
public abstract class PagedElementViewModel : StreamElementViewModel, IPagedViewModel {
public bool HasMultiplePages => Pages > 1;
public bool ShowPageControls => Pages > 1 || CanExecuteAddPage();
private int currentPage;
public int CurrentPage {
@ -15,18 +15,43 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
previousPage.CanExecuteChanged.Invoke(previousPage, EventArgs.Empty);
nextPage.CanExecuteChanged.Invoke(nextPage, EventArgs.Empty);
UpdateOtherPagedViewModels();
addPage?.CanExecuteChanged.Invoke(addPage, EventArgs.Empty);
PageChanged();
}
}
public int Pages { get; protected set; }
#region Commands
private readonly StubCommand previousPage = new StubCommand();
public ICommand PreviousPage => previousPage;
private readonly StubCommand nextPage = new StubCommand();
public ICommand NextPage => nextPage;
private StubCommand addPage, deletePage;
public ICommand AddPage => StubCommand(ref addPage, ExecuteAddPage, CanExecuteAddPage);
public ICommand DeletePage => StubCommand(ref deletePage, ExecuteDeletePage, CanExecuteDeletePage);
protected abstract bool CanExecuteAddPage();
protected abstract bool CanExecuteDeletePage();
protected virtual void ExecuteAddPage() {
foreach (var child in ViewPort.Tools.TableTool.Children) {
if (!(child is PagedElementViewModel pvm)) continue;
if (pvm.Pages == Pages - 1) pvm.ExecuteAddPage();
}
deletePage?.CanExecuteChanged.Invoke(deletePage, EventArgs.Empty);
}
protected virtual void ExecuteDeletePage() {
foreach (var child in ViewPort.Tools.TableTool.Children) {
if (!(child is PagedElementViewModel pvm)) continue;
if (pvm.Pages == Pages + 1) pvm.ExecuteDeletePage();
}
deletePage?.CanExecuteChanged.Invoke(deletePage, EventArgs.Empty);
}
#endregion
public PagedElementViewModel(ViewPort viewPort, int start) : base(viewPort, start) {
Pages = 1;
@ -54,7 +79,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
currentPage = that.currentPage;
Pages = that.Pages;
NotifyPropertyChanged(nameof(Pages));
NotifyPropertyChanged(nameof(HasMultiplePages));
NotifyPropertyChanged(nameof(ShowPageControls));
addPage?.CanExecuteChanged.Invoke(addPage, EventArgs.Empty);
deletePage?.CanExecuteChanged.Invoke(deletePage, EventArgs.Empty);
NotifyPropertyChanged(nameof(CurrentPage));
previousPage.CanExecuteChanged.Invoke(previousPage, EventArgs.Empty);
nextPage.CanExecuteChanged.Invoke(nextPage, EventArgs.Empty);

View File

@ -1,10 +1,10 @@
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using System;
using System.Linq;
namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
public class PaletteElementViewModel : PagedElementViewModel, IPagedViewModel {
public class PaletteElementViewModel : PagedElementViewModel {
private PaletteFormat format;
public string TableName { get; private set; }
@ -40,6 +40,35 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
public void Activate() => UpdateSprites(TableName);
protected override bool CanExecuteAddPage() {
var destination = ViewPort.Model.ReadPointer(Start);
var run = ViewPort.Model.GetNextRun(destination) as IPaletteRun;
return run is LzPaletteRun && CurrentPage == run.Pages - 1 && run.FindDependentSprites(Model).All(sprite => sprite.Pages == run.Pages && sprite is LzSpriteRun);
}
protected override void ExecuteAddPage() {
var destination = ViewPort.Model.ReadPointer(Start);
if (!(ViewPort.Model.GetNextRun(destination) is LzPaletteRun run)) return;
var newRun = run.AppendPage(ViewPort.CurrentChange);
if (newRun.Start != run.Start) {
ViewPort.RaiseMessage($"Palette moved from {run.Start:X6} to {newRun.Start:X6}. Pointers were updated.");
}
Pages = newRun.Pages;
CurrentPage = newRun.Pages - 1;
base.ExecuteAddPage();
}
protected override bool CanExecuteDeletePage() {
var destination = ViewPort.Model.ReadPointer(Start);
var run = ViewPort.Model.GetNextRun(destination) as IPaletteRun;
return run is LzPaletteRun && Pages > 1 && run.FindDependentSprites(Model).All(sprite => sprite.Pages == run.Pages && sprite is LzSpriteRun);
}
protected override void ExecuteDeletePage() {
// TODO
base.ExecuteDeletePage();
}
private void UpdateSprites(string hint = null) {
foreach (var child in ViewPort.Tools.TableTool.Children) {
if (child == this) break;

View File

@ -1,12 +1,13 @@
using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using System;
using System.Collections.Generic;
using System.Linq;
namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
public class SpriteElementViewModel : PagedElementViewModel, IPagedViewModel, IPixelViewModel {
public class SpriteElementViewModel : PagedElementViewModel, IPixelViewModel {
private SpriteFormat format;
private string paletteHint;
@ -46,6 +47,36 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
UpdateTiles(Start, page, false);
}
protected override bool CanExecuteAddPage() {
var destination = ViewPort.Model.ReadPointer(Start);
var run = ViewPort.Model.GetNextRun(destination) as ISpriteRun;
return run is LzSpriteRun && CurrentPage == run.Pages - 1 && run.FindRelatedPalettes(Model).All(pal => pal.Pages == run.Pages && pal is LzPaletteRun);
}
protected override void ExecuteAddPage() {
var destination = ViewPort.Model.ReadPointer(Start);
if (!(ViewPort.Model.GetNextRun(destination) is LzSpriteRun run)) return;
var newRun = run.AppendPage(ViewPort.CurrentChange);
if (newRun.Start != run.Start) {
ViewPort.RaiseMessage($"Sprite moved from {run.Start:X6} to {newRun.Start:X6}. Pointers were updated.");
}
Pages = newRun.Pages;
CurrentPage = newRun.Pages - 1;
base.ExecuteAddPage();
UpdateTiles(Start, CurrentPage, true); // update the tiles once all the related palettes have their pages added
}
protected override bool CanExecuteDeletePage() {
var destination = ViewPort.Model.ReadPointer(Start);
var run = ViewPort.Model.GetNextRun(destination) as ISpriteRun;
return run is LzSpriteRun && Pages > 1 && run.FindRelatedPalettes(Model).All(pal => pal.Pages == run.Pages && pal is LzPaletteRun);
}
protected override void ExecuteDeletePage() {
// TODO
base.ExecuteDeletePage();
}
private int[,] lastPixels;
private IReadOnlyList<short> lastColors;
private void UpdateTiles(int start, int page, bool exitPaletteSearchEarly) {

View File

@ -503,6 +503,36 @@ namespace HavenSoft.HexManiac.Tests {
Assert.IsType<SpriteRun>(Model.GetNextRun(0xA0));
}
[Fact]
public void CanAddPageToCompressedSpritesAndPalettes() {
// Arrange: place some sprites and palettes, linked to a name table
// 000-010 : pokenames table
// 010-020 : sprite/palette tables (each is a single pointer)
// 100-130 : front sprites
// 130-160 : back sprites
// 160-190 : normal palettes
// 190-1B0 : shiny palettes
ViewPort.Edit("FF @00 ^pokenames[name\"\"15]1 Castform\"");
ViewPort.Edit("@100 10 20 @130 10 20 @160 10 20 @190 10 20 @10 ");
ViewPort.Edit("^front.sprites[sprite<`lzs4x1x1`>]pokenames <100>");
ViewPort.Edit("^back.sprites[sprite<`lzs4x1x1`>]pokenames <130>");
ViewPort.Edit("^normal.palette[pal<`lzp4`>]pokenames <160>");
ViewPort.Edit("^shiny.palette[pal<`lzp4`>]pokenames <190>");
// Act: expand each by a single page
ViewPort.Goto.Execute("pokenames");
var sevm = (SpriteElementViewModel)ViewPort.Tools.TableTool.Children.First(child => child is SpriteElementViewModel);
sevm.AddPage.Execute();
// Assert: each has 2 pages now
foreach(var child in ViewPort.Tools.TableTool.Children) {
if (child is IPagedViewModel pvm) {
Assert.Equal(2, pvm.Pages);
Assert.Equal(1, pvm.CurrentPage);
}
}
}
private void CreateLzRun(int start, params byte[] data) {
for (int i = 0; i < data.Length; i++) Model[start + i] = data[i];
var run = new LZRun(Model, start);

View File

@ -259,8 +259,9 @@
<StackPanel Visibility="{Binding Visible, Converter={StaticResource BoolToVisibility}}">
<hsg3hv:CommonTableStreamControl />
<hsg3hv:PixelImage SnapsToDevicePixels="True" />
<DockPanel Visibility="{Binding HasMultiplePages, Converter={StaticResource BoolToVisibility}}" LastChildFill="False">
<DockPanel Visibility="{Binding ShowPageControls, Converter={StaticResource BoolToVisibility}}" LastChildFill="False">
<Button Content="Previous" Command="{Binding PreviousPage}" DockPanel.Dock="Left"/>
<Button Content="+" Command="{Binding AddPage}" DockPanel.Dock="Right" ToolTip="Add a new page to this sprite and palette"/>
<Button Content="Next" Command="{Binding NextPage}" DockPanel.Dock="Right"/>
</DockPanel>
</StackPanel>
@ -269,8 +270,9 @@
<StackPanel PreviewMouseDown="ActivatePalette" Visibility="{Binding Visible, Converter={StaticResource BoolToVisibility}}">
<hsg3hv:CommonTableStreamControl />
<hsg3hv:PaletteControl DataContext="{Binding Colors}" />
<DockPanel Visibility="{Binding HasMultiplePages, Converter={StaticResource BoolToVisibility}}" LastChildFill="False">
<DockPanel Visibility="{Binding ShowPageControls, Converter={StaticResource BoolToVisibility}}" LastChildFill="False">
<Button Content="Previous" Command="{Binding PreviousPage}" DockPanel.Dock="Left"/>
<Button Content="+" Command="{Binding AddPage}" DockPanel.Dock="Right" ToolTip="Add a new page to this sprite and palette"/>
<Button Content="Next" Command="{Binding NextPage}" DockPanel.Dock="Right"/>
</DockPanel>
</StackPanel>