Skip to content

Commit 77f4d1d

Browse files
authored
Add palette remap (#173)
* start adding palette remap functionality * progress * working implementation * sort swatches * clear selected image preview * null hints
1 parent ca02e0b commit 77f4d1d

File tree

7 files changed

+136
-51
lines changed

7 files changed

+136
-51
lines changed

Dat/PaletteMap.cs

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@
44

55
namespace OpenLoco.Dat
66
{
7+
public enum ColourRemapSwatch
8+
{
9+
Amber,
10+
AvocadoGreen,
11+
Black,
12+
Blue,
13+
Brass,
14+
Bronze,
15+
Brown,
16+
Copper,
17+
GrassGreen,
18+
Green,
19+
Lavender,
20+
Orange,
21+
Purple,
22+
Red,
23+
Rose,
24+
SeaGreen,
25+
Teal,
26+
Yellow,
27+
//MiscGrey,
28+
//MiscYellow,
29+
PrimaryRemap,
30+
SecondaryRemap,
31+
}
32+
733
public class PaletteMap
834
{
935
public PaletteMap(string filename)
@@ -47,10 +73,10 @@ public static (Color Color, byte Index) Transparent
4773
public (Color Color, byte Index)[] TextRendering
4874
=> Palette[1..7];
4975

50-
public (Color Color, byte Index)[] PrimaryRemapColours
76+
public (Color Color, byte Index)[] PrimaryRemap
5177
=> [.. Palette[7..10], .. Palette[246..255]];
5278

53-
public (Color Color, byte Index)[] SecondaryRemapColours
79+
public (Color Color, byte Index)[] SecondaryRemap
5480
=> Palette[202..214];
5581

5682
public (Color Color, byte Index) ChunkedTransparent
@@ -60,7 +86,7 @@ public static (Color Color, byte Index) Transparent
6086
=> [.. Palette[10..202], .. Palette[214..246]];
6187

6288
public (Color Color, byte Index)[] ReservedColours
63-
=> [Transparent, .. TextRendering, .. PrimaryRemapColours, .. SecondaryRemapColours, ChunkedTransparent];
89+
=> [Transparent, .. TextRendering, .. PrimaryRemap, .. SecondaryRemap, ChunkedTransparent];
6490

6591
#region Colour Swatches
6692

@@ -112,7 +138,7 @@ public static (Color Color, byte Index) Transparent
112138

113139
#endregion
114140

115-
public byte[] ConvertRgba32ImageToG1Data(Image<Rgba32> img, G1ElementFlags flags)
141+
public byte[] ConvertRgba32ImageToG1Data(Image<Rgba32> img, G1ElementFlags flags, ColourRemapSwatch primary, ColourRemapSwatch secondary)
116142
{
117143
var pixels = img.Width * img.Height;
118144
var isBgr = flags.HasFlag(G1ElementFlags.IsBgr24);
@@ -141,7 +167,34 @@ public byte[] ConvertRgba32ImageToG1Data(Image<Rgba32> img, G1ElementFlags flags
141167
return bytes;
142168
}
143169

144-
public bool TryConvertG1ToRgba32Bitmap(G1Element32 g1Element, out Image<Rgba32>? image)
170+
public (Color Color, byte Index)[]? GetRemapSwatchFromName(ColourRemapSwatch swatch)
171+
=> swatch switch
172+
{
173+
ColourRemapSwatch.Black => Black,
174+
ColourRemapSwatch.Bronze => Bronze,
175+
ColourRemapSwatch.Copper => Copper,
176+
ColourRemapSwatch.Yellow => Yellow,
177+
ColourRemapSwatch.Rose => Rose,
178+
ColourRemapSwatch.GrassGreen => GrassGreen,
179+
ColourRemapSwatch.AvocadoGreen => AvocadoGreen,
180+
ColourRemapSwatch.Green => Green,
181+
ColourRemapSwatch.Brass => Brass,
182+
ColourRemapSwatch.Lavender => Lavender,
183+
ColourRemapSwatch.Blue => Blue,
184+
ColourRemapSwatch.SeaGreen => SeaGreen,
185+
ColourRemapSwatch.Purple => Purple,
186+
ColourRemapSwatch.Red => Red,
187+
ColourRemapSwatch.Orange => Orange,
188+
ColourRemapSwatch.Teal => Teal,
189+
ColourRemapSwatch.Brown => Brown,
190+
ColourRemapSwatch.Amber => Amber,
191+
ColourRemapSwatch.PrimaryRemap => PrimaryRemap,
192+
ColourRemapSwatch.SecondaryRemap => SecondaryRemap,
193+
_ => default,
194+
};
195+
196+
197+
public bool TryConvertG1ToRgba32Bitmap(G1Element32 g1Element, ColourRemapSwatch primary, ColourRemapSwatch secondary, out Image<Rgba32>? image)
145198
{
146199
image = new Image<Rgba32>(g1Element.Width, g1Element.Height);
147200

@@ -167,10 +220,35 @@ public bool TryConvertG1ToRgba32Bitmap(G1Element32 g1Element, out Image<Rgba32>?
167220
else
168221
{
169222
var paletteIndex = g1Element.ImageData[index];
170-
image[x, y] = paletteIndex == 0 && g1Element.Flags.HasFlag(G1ElementFlags.HasTransparency)
171-
? Transparent.Color
172-
: Palette[paletteIndex].Color;
223+
Color? colour = null;
224+
225+
if (SecondaryRemap.Any(x => x.Index == paletteIndex))
226+
{
227+
//Debugger.Break();
228+
}
229+
230+
if (paletteIndex == 0 && g1Element.Flags.HasFlag(G1ElementFlags.HasTransparency))
231+
{
232+
colour = Transparent.Color;
233+
}
234+
else if (PrimaryRemap.Index().SingleOrDefault(x => x.Item.Index == paletteIndex) is (int, (Color, byte)) itemP && itemP.Index != 0)
235+
{
236+
var swatch = GetRemapSwatchFromName(primary);
237+
if (swatch != null)
238+
{
239+
colour = swatch[itemP.Index].Color;
240+
}
241+
}
242+
else if (SecondaryRemap.Index().SingleOrDefault(x => x.Item.Index == paletteIndex) is (int, (Color, byte)) itemS && itemS.Index != 0)
243+
{
244+
var swatch = GetRemapSwatchFromName(secondary);
245+
if (swatch != null)
246+
{
247+
colour = swatch[itemS.Index].Color;
248+
}
249+
}
173250

251+
image[x, y] = colour ?? Palette[paletteIndex].Color;
174252
index++;
175253
}
176254
}

Gui/Models/ObjectEditorModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ public bool TryLoadObject(FileSystemItem filesystemItem, out UiDatLocoFile? uiLo
244244
{
245245
foreach (var i in locoObject.G1Elements)
246246
{
247-
if (PaletteMap.TryConvertG1ToRgba32Bitmap(i, out var image))
247+
if (PaletteMap.TryConvertG1ToRgba32Bitmap(i, ColourRemapSwatch.PrimaryRemap, ColourRemapSwatch.SecondaryRemap, out var image))
248248
{
249249
images.Add(image!);
250250
}
@@ -275,7 +275,7 @@ public bool TryLoadObject(FileSystemItem filesystemItem, out UiDatLocoFile? uiLo
275275
{
276276
foreach (var i in locoObject.G1Elements)
277277
{
278-
if (PaletteMap.TryConvertG1ToRgba32Bitmap(i, out var image))
278+
if (PaletteMap.TryConvertG1ToRgba32Bitmap(i, ColourRemapSwatch.PrimaryRemap, ColourRemapSwatch.SecondaryRemap, out var image))
279279
{
280280
images.Add(image!);
281281
}

Gui/ViewModels/DatTypes/G1ViewModel.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using OpenLoco.Dat;
12
using OpenLoco.Dat.FileParsing;
23
using OpenLoco.Gui.Models;
34
using ReactiveUI.Fody.Helpers;
@@ -36,7 +37,7 @@ public override void Load()
3637
{
3738
try
3839
{
39-
if (Model.PaletteMap.TryConvertG1ToRgba32Bitmap(e, out var image))
40+
if (Model.PaletteMap.TryConvertG1ToRgba32Bitmap(e, ColourRemapSwatch.PrimaryRemap, ColourRemapSwatch.SecondaryRemap, out var image))
4041
{
4142
images.Add(image!);
4243
}

Gui/ViewModels/SubObjectTypes/ImageTableViewModel.cs

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,6 @@
2626

2727
namespace OpenLoco.Gui.ViewModels
2828
{
29-
public enum ColourSwatches
30-
{
31-
Black,
32-
Bronze,
33-
Copper,
34-
Yellow,
35-
Rose,
36-
GrassGreen,
37-
AvocadoGreen,
38-
Green,
39-
Brass,
40-
Lavender,
41-
Blue,
42-
SeaGreen,
43-
Purple,
44-
Red,
45-
Orange,
46-
Teal,
47-
Brown,
48-
Amber,
49-
MiscGrey,
50-
MiscYellow,
51-
PrimaryRemap,
52-
SecondaryRemap,
53-
}
54-
5529
public record SpriteOffset(
5630
[property: JsonPropertyName("path")] string Path,
5731
[property: JsonPropertyName("x")] int16_t X,
@@ -66,10 +40,13 @@ public class ImageTableViewModel : ReactiveObject, IExtraContentViewModel
6640
readonly IImageTableNameProvider NameProvider;
6741
readonly ILogger Logger;
6842

69-
public ColourSwatches[] ColourSwatchesArr { get; } = (ColourSwatches[])Enum.GetValues(typeof(ColourSwatches));
43+
public ColourRemapSwatch[] ColourSwatchesArr { get; } = Enum.GetValues<ColourRemapSwatch>();
7044

7145
[Reactive]
72-
public ColourSwatches SelectedColourSwatch { get; set; } = ColourSwatches.PrimaryRemap;
46+
public ColourRemapSwatch SelectedPrimarySwatch { get; set; } = ColourRemapSwatch.PrimaryRemap;
47+
48+
[Reactive]
49+
public ColourRemapSwatch SelectedSecondarySwatch { get; set; } = ColourRemapSwatch.SecondaryRemap;
7350

7451
readonly DispatcherTimer animationTimer;
7552
int currentFrameIndex;
@@ -100,9 +77,6 @@ public Avalonia.Size SelectedBitmapPreviewBorder
10077
[Reactive]
10178
public ICommand CropAllImagesCommand { get; set; }
10279

103-
[Reactive]
104-
public int Zoom { get; set; } = 1;
105-
10680
// where the actual image data is stored
10781
[Reactive]
10882
public IList<Image<Rgba32>> Images { get; set; }
@@ -118,7 +92,7 @@ public Avalonia.Size SelectedBitmapPreviewBorder
11892
public SelectionModel<Bitmap> SelectionModel { get; set; }
11993

12094
public UIG1Element32? SelectedG1Element
121-
=> SelectedImageIndex == -1 || G1Provider.G1Elements.Count == 0
95+
=> SelectedImageIndex == -1 || G1Provider.G1Elements.Count == 0 || SelectedImageIndex >= G1Provider.G1Elements.Count
12296
? null
12397
: new UIG1Element32(SelectedImageIndex, GetImageName(NameProvider, SelectedImageIndex), G1Provider.G1Elements[SelectedImageIndex]);
12498

@@ -143,10 +117,12 @@ public ImageTableViewModel(IHasG1Elements g1ElementProvider, IImageTableNameProv
143117
.Subscribe(_ => this.RaisePropertyChanged(nameof(Images)));
144118
_ = this.WhenAnyValue(o => o.PaletteMap)
145119
.Subscribe(_ => this.RaisePropertyChanged(nameof(Images)));
146-
_ = this.WhenAnyValue(o => o.Zoom)
147-
.Subscribe(_ => this.RaisePropertyChanged(nameof(Images)));
120+
_ = this.WhenAnyValue(o => o.SelectedPrimarySwatch).Skip(1)
121+
.Subscribe(_ => RecalcImages());
122+
_ = this.WhenAnyValue(o => o.SelectedSecondarySwatch).Skip(1)
123+
.Subscribe(_ => RecalcImages());
148124
_ = this.WhenAnyValue(o => o.Images)
149-
.Subscribe(_ => Bitmaps = new ObservableCollection<Bitmap?>(G1ImageConversion.CreateAvaloniaImages(Images)));
125+
.Subscribe(_ => Bitmaps = [.. G1ImageConversion.CreateAvaloniaImages(Images)]);
150126
_ = this.WhenAnyValue(o => o.SelectedImageIndex)
151127
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1Element)));
152128
_ = this.WhenAnyValue(o => o.SelectedG1Element)
@@ -323,7 +299,7 @@ void LoadSprite(string filename, uint imageOffset, short xOffset, short yOffset,
323299

324300
var newElement = new G1Element32(imageOffset, (int16_t)img.Width, (int16_t)img.Height, xOffset, yOffset, flags, zoomOffset)
325301
{
326-
ImageData = PaletteMap.ConvertRgba32ImageToG1Data(img, flags)
302+
ImageData = PaletteMap.ConvertRgba32ImageToG1Data(img, flags, SelectedPrimarySwatch, SelectedSecondarySwatch)
327303
};
328304

329305
G1Provider.G1Elements.Add(newElement);
@@ -332,6 +308,34 @@ void LoadSprite(string filename, uint imageOffset, short xOffset, short yOffset,
332308
}
333309
}
334310

311+
public void RecalcImages()
312+
{
313+
// unfortunately no way to "copy" the selection from old to new
314+
SelectionModel.Clear();
315+
316+
// clear existing images
317+
Logger.Info("Clearing current G1Element32s and existing object images");
318+
Images.Clear();
319+
Bitmaps.Clear();
320+
321+
foreach (var g1 in G1Provider.G1Elements)
322+
{
323+
if (PaletteMap.TryConvertG1ToRgba32Bitmap(g1, SelectedPrimarySwatch, SelectedSecondarySwatch, out var img) && img != null)
324+
{
325+
Images.Add(img);
326+
Bitmaps.Add(G1ImageConversion.CreateAvaloniaImage(img));
327+
}
328+
else
329+
{
330+
Logger.Error("Unable to convert G1 to image");
331+
}
332+
}
333+
334+
this.RaisePropertyChanged(nameof(Bitmaps));
335+
this.RaisePropertyChanged(nameof(Images));
336+
SelectedImageIndex = -1;
337+
}
338+
335339
// todo: second half should be in model
336340
public async Task ExportImages()
337341
{
@@ -458,7 +462,7 @@ void UpdateImage(Image<Rgba32> img, int index, short? xOffset, short? yOffset)
458462
Width = (int16_t)img.Width,
459463
Height = (int16_t)img.Height,
460464
Flags = currG1.Flags,
461-
ImageData = PaletteMap.ConvertRgba32ImageToG1Data(img, currG1.Flags),
465+
ImageData = PaletteMap.ConvertRgba32ImageToG1Data(img, currG1.Flags, SelectedPrimarySwatch, SelectedSecondarySwatch),
462466
XOffset = xOffset ?? currG1.XOffset,
463467
YOffset = yOffset ?? currG1.YOffset,
464468
};

Gui/Views/ImageTableView.axaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
</Flyout>
4242
</Button.Flyout>
4343
</Button>
44+
<ComboBox ItemsSource="{Binding ColourSwatchesArr}" SelectedItem="{Binding SelectedPrimarySwatch}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Primary remap swatch" />
45+
<ComboBox ItemsSource="{Binding ColourSwatchesArr}" SelectedItem="{Binding SelectedSecondarySwatch}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Secondary remap swatch" />
4446
<Button Command="{Binding CropAllImagesCommand}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Crops all images and adjusts offsets">
4547
<DockPanel>
4648
<materialIcons:MaterialIcon Kind="Crop" Width="24" Height="24" Margin="2" />

ObjectService/Server.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public async Task<IResult> GetObjectImages(int uniqueObjectId, LocoDb db, [FromS
172172
var count = 0;
173173
foreach (var g1 in locoObj!.Value!.LocoObject!.G1Elements)
174174
{
175-
if (!PaletteMap.TryConvertG1ToRgba32Bitmap(g1, out var image))
175+
if (!PaletteMap.TryConvertG1ToRgba32Bitmap(g1, ColourRemapSwatch.PrimaryRemap, ColourRemapSwatch.SecondaryRemap, out var image))
176176
{
177177
continue;
178178
}

Tests/ImagePaletteConversionTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ public void G1ElementToPNGAndBack(string objectSource)
6969
var i = 0;
7070
foreach (var element in g1Elements)
7171
{
72-
if (paletteMap.TryConvertG1ToRgba32Bitmap(element, out var image0))
72+
if (paletteMap.TryConvertG1ToRgba32Bitmap(element, ColourRemapSwatch.PrimaryRemap, ColourRemapSwatch.SecondaryRemap, out var image0))
7373
{
74-
var g1Bytes = paletteMap.ConvertRgba32ImageToG1Data(image0!, element.Flags);
74+
var g1Bytes = paletteMap.ConvertRgba32ImageToG1Data(image0!, element.Flags, ColourRemapSwatch.PrimaryRemap, ColourRemapSwatch.SecondaryRemap);
7575
Assert.That(g1Bytes, Is.EqualTo(element.ImageData), $"[{i++}]");
7676
}
7777
}

0 commit comments

Comments
 (0)