From b39f99990c9318f757f62aa1929bc41a47d8e271 Mon Sep 17 00:00:00 2001
From: Gwilym Inzani <email@gwilym.dev>
Date: Tue, 29 Aug 2023 14:52:20 +0100
Subject: [PATCH] Use deduplication in hyperspace roll

---
 agb-image-converter/src/deduplicator.rs    | 152 +++++++++++++++++++++
 examples/hyperspace-roll/src/background.rs |  42 ++----
 2 files changed, 166 insertions(+), 28 deletions(-)
 create mode 100644 agb-image-converter/src/deduplicator.rs

diff --git a/agb-image-converter/src/deduplicator.rs b/agb-image-converter/src/deduplicator.rs
new file mode 100644
index 00000000..cb20608b
--- /dev/null
+++ b/agb-image-converter/src/deduplicator.rs
@@ -0,0 +1,152 @@
+use std::{collections::HashMap, hash::BuildHasher};
+
+use crate::{colour::Colour, image_loader::Image};
+
+pub struct Transformation {
+    pub vflip: bool,
+    pub hflip: bool,
+}
+
+impl Transformation {
+    pub fn none() -> Self {
+        Self {
+            vflip: false,
+            hflip: false,
+        }
+    }
+
+    pub fn vflipped() -> Self {
+        Self {
+            vflip: true,
+            hflip: false,
+        }
+    }
+
+    pub fn hflipped() -> Self {
+        Self {
+            vflip: false,
+            hflip: true,
+        }
+    }
+
+    pub fn vhflipped() -> Self {
+        Self {
+            vflip: true,
+            hflip: true,
+        }
+    }
+}
+
+pub struct DeduplicatedData {
+    pub new_index: usize,
+    pub transformation: Transformation,
+}
+
+#[derive(Clone, PartialEq, Eq, Hash)]
+struct Tile {
+    data: [Colour; 64],
+}
+
+impl Tile {
+    fn split_image(input: &Image) -> Vec<Self> {
+        let mut ret = vec![];
+
+        for y in 0..(input.height / 8) {
+            for x in 0..(input.width / 8) {
+                let mut tile_data = Vec::with_capacity(64);
+
+                for j in 0..8 {
+                    for i in 0..8 {
+                        tile_data.push(input.colour(x * 8 + i, y * 8 + j));
+                    }
+                }
+
+                ret.push(Tile {
+                    data: tile_data.try_into().unwrap(),
+                });
+            }
+        }
+
+        ret
+    }
+
+    fn vflipped(&self) -> Self {
+        let mut new_data = self.data;
+        for y in 0..8 {
+            for x in 0..8 {
+                new_data.swap(y * 8 + x, (7 - y) * 8 + x);
+            }
+        }
+
+        Self { data: new_data }
+    }
+
+    fn hflipped(&self) -> Self {
+        let mut new_data = self.data;
+
+        for y in 0..8 {
+            for x in 0..8 {
+                new_data.swap(y * 8 + x, y * 8 + (7 - x));
+            }
+        }
+
+        Self { data: new_data }
+    }
+}
+
+pub(crate) fn deduplicate_image(input: &Image, can_flip: bool) -> (Image, Vec<DeduplicatedData>) {
+    let mut resulting_tiles = vec![];
+    let mut deduplication_data = vec![];
+
+    let all_tiles = Tile::split_image(input);
+    let mut existing_tiles = HashMap::new();
+
+    let hasher = std::collections::hash_map::RandomState::new();
+
+    for tile in all_tiles {
+        let (tile, transformation) = if can_flip {
+            let vflipped = tile.vflipped();
+            let hflipped = tile.hflipped();
+            let vhflipped = vflipped.hflipped();
+
+            // find the one with the smallest hash
+            let tile_hash = hasher.hash_one(&tile);
+            let vflipped_hash = hasher.hash_one(&vflipped);
+            let hflipped_hash = hasher.hash_one(&hflipped);
+            let vhflipped_hash = hasher.hash_one(&vhflipped);
+
+            let minimum = tile_hash
+                .min(vflipped_hash)
+                .min(hflipped_hash)
+                .min(vhflipped_hash);
+
+            if minimum == tile_hash {
+                (tile, Transformation::none())
+            } else if minimum == vflipped_hash {
+                (vflipped, Transformation::vflipped())
+            } else if minimum == hflipped_hash {
+                (hflipped, Transformation::hflipped())
+            } else {
+                (vhflipped, Transformation::vhflipped())
+            }
+        } else {
+            (tile, Transformation::none())
+        };
+
+        let index = *existing_tiles.entry(tile.clone()).or_insert_with(|| {
+            resulting_tiles.push(tile);
+            resulting_tiles.len() - 1
+        });
+
+        deduplication_data.push(DeduplicatedData {
+            new_index: index,
+            transformation,
+        });
+    }
+
+    let image_data = resulting_tiles
+        .iter()
+        .flat_map(|tile| tile.data)
+        .collect::<Vec<_>>();
+    (Image::from_colour_data(image_data), deduplication_data)
+}
diff --git a/examples/hyperspace-roll/src/background.rs b/examples/hyperspace-roll/src/background.rs
index 61035ce1..fa0ea199 100644
--- a/examples/hyperspace-roll/src/background.rs
+++ b/examples/hyperspace-roll/src/background.rs
@@ -6,11 +6,11 @@ use agb::{
 use crate::sfx::Sfx;
 
 include_background_gfx!(backgrounds, "121105",
-    stars => "gfx/stars.aseprite",
-    title => "gfx/title-screen.aseprite",
-    help => "gfx/help-text.aseprite",
-    descriptions1 => "gfx/descriptions1.png",
-    descriptions2 => "gfx/descriptions2.png",
+    stars => deduplicate "gfx/stars.aseprite",
+    title => deduplicate "gfx/title-screen.aseprite",
+    help => deduplicate "gfx/help-text.aseprite",
+    descriptions1 => deduplicate "gfx/descriptions1.png",
+    descriptions2 => deduplicate "gfx/descriptions2.png",
 );
 
 pub fn load_palettes(vram: &mut VRamManager) {
@@ -35,12 +35,7 @@ pub(crate) fn load_help_text(
             vram,
             (x + at_tile.0, at_tile.1).into(),
             &help_tileset,
-            TileSetting::new(
-                tile_id,
-                false,
-                false,
-                backgrounds::help.palette_assignments[tile_id as usize],
-            ),
+            backgrounds::help.tile_settings[tile_id as usize],
         )
     }
 }
@@ -50,13 +45,13 @@ pub(crate) fn load_description(
     descriptions_map: &mut RegularMap,
     vram: &mut VRamManager,
 ) {
-    let (tileset, palette_assignments) = if face_id < 10 {
+    let (tileset, tile_settings) = if face_id < 10 {
         (
             TileSet::new(
                 backgrounds::descriptions1.tiles,
                 agb::display::tiled::TileFormat::FourBpp,
             ),
-            backgrounds::descriptions1.palette_assignments,
+            backgrounds::descriptions1.tile_settings,
         )
     } else {
         (
@@ -64,7 +59,7 @@ pub(crate) fn load_description(
                 backgrounds::descriptions2.tiles,
                 agb::display::tiled::TileFormat::FourBpp,
             ),
-            backgrounds::descriptions2.palette_assignments,
+            backgrounds::descriptions2.tile_settings,
         )
     };
 
@@ -75,7 +70,7 @@ pub(crate) fn load_description(
                 vram,
                 (x, y).into(),
                 &tileset,
-                TileSetting::new(tile_id, false, false, palette_assignments[tile_id as usize]),
+                tile_settings[tile_id as usize],
             )
         }
     }
@@ -87,16 +82,12 @@ fn create_background_map(map: &mut RegularMap, vram: &mut VRamManager, stars_til
         for y in 0..32u16 {
             let blank = rng::gen().rem_euclid(32) < 30;
 
-            let (tile_id, palette_id) = if blank {
-                ((1 << 10) - 1, 0)
+            let tile_setting = if blank {
+                TileSetting::new((1 << 10) - 1, false, false, 0)
             } else {
                 let tile_id = rng::gen().rem_euclid(64) as u16;
-                (
-                    tile_id,
-                    backgrounds::stars.palette_assignments[tile_id as usize],
-                )
+                backgrounds::stars.tile_settings[tile_id as usize]
             };
-            let tile_setting = TileSetting::new(tile_id, false, false, palette_id);
 
             map.set_tile(vram, (x, y).into(), stars_tileset, tile_setting);
         }
@@ -121,12 +112,7 @@ pub fn show_title_screen(background: &mut RegularMap, vram: &mut VRamManager, sf
                 vram,
                 (x, y).into(),
                 &tile_set,
-                TileSetting::new(
-                    tile_id,
-                    false,
-                    false,
-                    backgrounds::title.palette_assignments[tile_id as usize],
-                ),
+                backgrounds::title.tile_settings[tile_id as usize],
             );
         }