From 7ac79095107fedb24b409dc474a9ec03e89b6fc0 Mon Sep 17 00:00:00 2001
From: Gary Talent <gary@drinkingtea.net>
Date: Thu, 17 Feb 2022 04:27:23 -0600
Subject: [PATCH] [nostalgia/core] Upgrade TileSheet format to support
 subsheets and add conversion system

---
 src/nostalgia/core/CMakeLists.txt             |   2 +
 src/nostalgia/core/core.hpp                   |   1 +
 src/nostalgia/core/gfx.hpp                    | 164 +++++++++++++++++-
 src/nostalgia/core/media.hpp                  |  17 +-
 .../core/studio/tilesheeteditor-imgui.hpp     |   2 +-
 src/nostalgia/core/studio/tilesheeteditor.cpp |   8 +-
 .../core/studio/tilesheeteditormodel.cpp      |  16 +-
 .../core/studio/tilesheetpixelgrid.cpp        |  22 +--
 src/nostalgia/core/studio/tilesheetpixels.cpp |   6 +-
 src/nostalgia/core/typeconv.cpp               |  71 ++++++++
 src/nostalgia/core/typeconv.hpp               |  17 ++
 11 files changed, 290 insertions(+), 36 deletions(-)
 create mode 100644 src/nostalgia/core/typeconv.cpp
 create mode 100644 src/nostalgia/core/typeconv.hpp

diff --git a/src/nostalgia/core/CMakeLists.txt b/src/nostalgia/core/CMakeLists.txt
index 0bc67d33..1131fc73 100644
--- a/src/nostalgia/core/CMakeLists.txt
+++ b/src/nostalgia/core/CMakeLists.txt
@@ -49,6 +49,7 @@ add_library(
 	NostalgiaCore
 		gfx.cpp
 		media.cpp
+		typeconv.cpp
 		${NOSTALGIA_CORE_IMPL_SRC}
 )
 
@@ -79,6 +80,7 @@ install(
 		gfx.hpp
 		input.hpp
 		media.hpp
+		typeconv.hpp
 	DESTINATION
 		include/nostalgia/core
 )
diff --git a/src/nostalgia/core/core.hpp b/src/nostalgia/core/core.hpp
index c7af77ea..5c2cca97 100644
--- a/src/nostalgia/core/core.hpp
+++ b/src/nostalgia/core/core.hpp
@@ -13,6 +13,7 @@
 #include "gfx.hpp"
 #include "input.hpp"
 #include "media.hpp"
+#include "typeconv.hpp"
 
 namespace nostalgia::core {
 
diff --git a/src/nostalgia/core/gfx.hpp b/src/nostalgia/core/gfx.hpp
index 6d20f86a..8db44fd0 100644
--- a/src/nostalgia/core/gfx.hpp
+++ b/src/nostalgia/core/gfx.hpp
@@ -35,7 +35,7 @@ struct Palette {
 	ox::Vector<Color16> colors;
 };
 
-struct TileSheet {
+struct TileSheetV1 {
 	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
 	static constexpr auto TypeVersion = 1;
 	int8_t bpp = 0;
@@ -48,7 +48,7 @@ struct TileSheet {
 
 	[[nodiscard]]
 	constexpr uint8_t getPixel4Bpp(std::size_t idx) const noexcept {
-		oxAssert(bpp == 4, "NostalgiaGraphic::getPixel4Bpp: wrong bpp");
+		oxAssert(bpp == 4, "TileSheetV1::getPixel4Bpp: wrong bpp");
 		if (idx & 1) {
 			return this->pixels[idx / 2] >> 4;
 		} else {
@@ -58,7 +58,7 @@ struct TileSheet {
 
 	[[nodiscard]]
 	constexpr uint8_t getPixel8Bpp(std::size_t idx) const noexcept {
-		oxAssert(bpp == 8, "NostalgiaGraphic::getPixel8Bpp: wrong bpp");
+		oxAssert(bpp == 8, "TileSheetV1::getPixel8Bpp: wrong bpp");
 		return this->pixels[idx];
 	}
 
@@ -73,14 +73,14 @@ struct TileSheet {
 
 	[[nodiscard]]
 	constexpr auto getPixel4Bpp(const geo::Point &pt) const noexcept {
-		oxAssert(bpp == 4, "NostalgiaGraphic::getPixel4Bpp: wrong bpp");
+		oxAssert(bpp == 4, "TileSheetV1::getPixel4Bpp: wrong bpp");
 		const auto idx = ptToIdx(pt, this->columns);
 		return getPixel4Bpp(idx);
 	}
 
 	[[nodiscard]]
 	constexpr auto getPixel8Bpp(const geo::Point &pt) const noexcept {
-		oxAssert(bpp == 8, "NostalgiaGraphic::getPixel8Bpp: wrong bpp");
+		oxAssert(bpp == 8, "TileSheetV1::getPixel8Bpp: wrong bpp");
 		const auto idx = ptToIdx(pt, this->columns);
 		return getPixel8Bpp(idx);
 	}
@@ -92,7 +92,7 @@ struct TileSheet {
 	}
 
 	constexpr void setPixel(uint64_t idx, uint8_t palIdx) noexcept {
-		 auto &pixel = this->pixels[idx / 2];
+		auto &pixel = this->pixels[idx / 2];
 		if (bpp == 4) {
 			if (idx & 1) {
 				pixel = (pixel & 0b0000'1111) | (palIdx << 4);
@@ -110,11 +110,144 @@ struct TileSheet {
 	}
 };
 
+struct TileSheet {
+	using SubSheetIdx = ox::Vector<std::size_t, 4>;
+
+	struct SubSheet {
+		static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
+		static constexpr auto TypeVersion = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
+		ox::BString<32> name;
+		std::size_t begin = 0;
+		std::size_t size = 0;
+		int rows = 1;
+		int columns = 1;
+		ox::Vector<SubSheet> subsheets;
+	};
+
+	static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
+	static constexpr auto TypeVersion = 2;
+	int8_t bpp = 0;
+	// rows and columns are really only used by TileSheetEditor
+	ox::FileAddress defaultPalette;
+	Palette pal;
+	ox::Vector<uint8_t> pixels;
+	SubSheet subsheet;
+
+	[[nodiscard]]
+	constexpr const auto &getSubSheet(const SubSheetIdx &idx, std::size_t idxIt, const SubSheet *pSubsheet) const noexcept {
+		if (idxIt == idx.size()) {
+			return *pSubsheet;
+		}
+		return getSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[idx[idxIt]]);
+	}
+
+	[[nodiscard]]
+	constexpr const auto &getSubSheet(const SubSheetIdx &idx) const noexcept {
+		return getSubSheet(idx, 0, &subsheet);
+	}
+
+	[[nodiscard]]
+	constexpr auto &getSubSheet(const SubSheetIdx &idx, std::size_t idxIt, const SubSheet *pSubsheet) noexcept {
+		if (idxIt == idx.size()) {
+			return *pSubsheet;
+		}
+		return getSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[idx[idxIt]]);
+	}
+
+	[[nodiscard]]
+	constexpr auto &getSubSheet(const SubSheetIdx &idx) noexcept {
+		return getSubSheet(idx, 0, &subsheet);
+	}
+
+	[[nodiscard]]
+	constexpr const auto &columns(const SubSheetIdx &idx = {}) const noexcept {
+		return getSubSheet(idx).columns;
+	}
+
+	[[nodiscard]]
+	constexpr const auto &rows(const SubSheetIdx &idx = {}) const noexcept {
+		return getSubSheet(idx).rows;
+	}
+
+	[[nodiscard]]
+	constexpr auto &columns(const SubSheetIdx &idx = {}) noexcept {
+		return getSubSheet(idx).columns;
+	}
+
+	[[nodiscard]]
+	constexpr auto &rows(const SubSheetIdx &idx = {}) noexcept {
+		return getSubSheet(idx).rows;
+	}
+
+	[[nodiscard]]
+	constexpr uint8_t getPixel4Bpp(std::size_t idx) const noexcept {
+		oxAssert(bpp == 4, "TileSheetV1::getPixel4Bpp: wrong bpp");
+		if (idx & 1) {
+			return this->pixels[idx / 2] >> 4;
+		} else {
+			return this->pixels[idx / 2] & 0b0000'1111;
+		}
+	}
+
+	[[nodiscard]]
+	constexpr uint8_t getPixel8Bpp(std::size_t idx) const noexcept {
+		oxAssert(bpp == 8, "TileSheetV1::getPixel8Bpp: wrong bpp");
+		return this->pixels[idx];
+	}
+
+	[[nodiscard]]
+	constexpr auto getPixel(std::size_t idx) const noexcept {
+		if (this->bpp == 4) {
+			return getPixel4Bpp(idx);
+		} else {
+			return getPixel8Bpp(idx);
+		}
+	}
+
+	[[nodiscard]]
+	constexpr auto getPixel4Bpp(const geo::Point &pt, const SubSheetIdx &subsheetIdx) const noexcept {
+		oxAssert(bpp == 4, "TileSheetV1::getPixel4Bpp: wrong bpp");
+		const auto idx = ptToIdx(pt, this->getSubSheet(subsheetIdx).columns);
+		return getPixel4Bpp(idx);
+	}
+
+	[[nodiscard]]
+	constexpr auto getPixel8Bpp(const geo::Point &pt, const SubSheetIdx &subsheetIdx) const noexcept {
+		oxAssert(bpp == 8, "TileSheetV1::getPixel8Bpp: wrong bpp");
+		const auto idx = ptToIdx(pt, this->getSubSheet(subsheetIdx).columns);
+		return getPixel8Bpp(idx);
+	}
+
+	[[nodiscard]]
+	constexpr auto getPixel(const geo::Point &pt, const SubSheetIdx &subsheetIdx) const noexcept {
+		const auto idx = ptToIdx(pt, this->getSubSheet(subsheetIdx).columns);
+		return getPixel(idx);
+	}
+
+	constexpr void setPixel(uint64_t idx, uint8_t palIdx) noexcept {
+		 auto &pixel = this->pixels[idx / 2];
+		if (bpp == 4) {
+			if (idx & 1) {
+				pixel = (pixel & 0b0000'1111) | (palIdx << 4);
+			} else {
+				pixel = (pixel & 0b1111'0000) | (palIdx);
+			}
+		} else {
+			pixel = palIdx;
+		}
+	}
+
+	constexpr void setPixel(const SubSheetIdx &subsheetIdx, const geo::Point &pt, uint8_t palIdx) noexcept {
+		const auto idx = ptToIdx(pt, this->getSubSheet(subsheetIdx).columns);
+		setPixel(idx, palIdx);
+	}
+};
+
 oxModelBegin(Palette)
 	oxModelField(colors)
 oxModelEnd()
 
-oxModelBegin(TileSheet)
+oxModelBegin(TileSheetV1)
 	oxModelField(bpp)
 	oxModelField(rows)
 	oxModelField(columns)
@@ -123,6 +256,23 @@ oxModelBegin(TileSheet)
 	oxModelField(pixels)
 oxModelEnd()
 
+oxModelBegin(TileSheet)
+	oxModelField(bpp)
+	oxModelField(defaultPalette)
+	oxModelField(pal)
+	oxModelField(pixels)
+	oxModelField(subsheet)
+oxModelEnd()
+
+oxModelBegin(TileSheet::SubSheet)
+	oxModelField(name);
+	oxModelField(begin);
+	oxModelField(size);
+	oxModelField(rows);
+	oxModelField(columns);
+	oxModelField(subsheets)
+oxModelEnd()
+
 struct Sprite {
 	unsigned idx = 0;
 	unsigned x = 0;
diff --git a/src/nostalgia/core/media.hpp b/src/nostalgia/core/media.hpp
index 6bd0f047..b1d43bab 100644
--- a/src/nostalgia/core/media.hpp
+++ b/src/nostalgia/core/media.hpp
@@ -4,27 +4,40 @@
 
 #pragma once
 
+#include <ox/std/defines.hpp>
+
 #include <ox/claw/claw.hpp>
 #include <ox/fs/fs.hpp>
 
 #include "context.hpp"
+#include "typeconv.hpp"
 
 namespace nostalgia::core {
 
 template<typename T>
 ox::Result<AssetRef<T>> readObj(Context *ctx, const ox::FileAddress &file, bool forceLoad = false) noexcept {
 #ifndef OX_BARE_METAL
+	constexpr auto readConvert = [](const ox::Buffer &buff) -> ox::Result<T> {
+		auto [obj, err] = ox::readClaw<T>(buff);
+		if (err) {
+			if (err != ox::Error_ClawTypeVersionMismatch && err != ox::Error_ClawTypeMismatch) {
+				return err;
+			}
+			oxReturnError(convert(buff, T::TypeName, T::TypeVersion, &obj));
+		}
+		return obj;
+	};
 	oxRequire(path, file.getPath());
 	if (forceLoad) {
 		oxRequire(buff, ctx->rom->read(file));
-		oxRequire(obj, ox::readClaw<T>(buff));
+		oxRequire(obj, readConvert(buff));
 		oxRequire(cached, ctx->assetManager.template setAsset(path, obj));
 		return cached;
 	} else {
 		auto [cached, err] = ctx->assetManager.template getAsset<T>(path);
 		if (err) {
 			oxRequire(buff, ctx->rom->read(file));
-			oxRequire(obj, ox::readClaw<T>(buff));
+			oxRequire(obj, readConvert(buff));
 			oxReturnError(ctx->assetManager.template setAsset(path, obj).moveTo(&cached));
 		}
 		return cached;
diff --git a/src/nostalgia/core/studio/tilesheeteditor-imgui.hpp b/src/nostalgia/core/studio/tilesheeteditor-imgui.hpp
index 196ee6f6..f63d6547 100644
--- a/src/nostalgia/core/studio/tilesheeteditor-imgui.hpp
+++ b/src/nostalgia/core/studio/tilesheeteditor-imgui.hpp
@@ -47,7 +47,7 @@ class TileSheetEditorImGui: public studio::Editor {
 
 		void draw(core::Context*) noexcept override;
 
-		virtual studio::UndoStack *undoStack() noexcept override;
+		studio::UndoStack *undoStack() noexcept final;
 
 	protected:
 		void saveItem() override;
diff --git a/src/nostalgia/core/studio/tilesheeteditor.cpp b/src/nostalgia/core/studio/tilesheeteditor.cpp
index 64d274a3..428b2757 100644
--- a/src/nostalgia/core/studio/tilesheeteditor.cpp
+++ b/src/nostalgia/core/studio/tilesheeteditor.cpp
@@ -40,8 +40,8 @@ void TileSheetEditor::draw() noexcept {
 
 void TileSheetEditor::scrollV(const geo::Vec2 &paneSz, float wheel, bool zoomMod) noexcept {
 	const auto pixelSize = m_pixelsDrawer.pixelSize(paneSz);
-	const ImVec2 sheetSize(pixelSize.x * static_cast<float>(img().columns) * TileWidth,
-	                       pixelSize.y * static_cast<float>(img().rows) * TileHeight);
+	const ImVec2 sheetSize(pixelSize.x * static_cast<float>(img().columns()) * TileWidth,
+	                       pixelSize.y * static_cast<float>(img().rows()) * TileHeight);
 	if (zoomMod) {
 		m_pixelSizeMod = ox::clamp(m_pixelSizeMod + wheel * 0.02f, 0.55f, 2.f);
 		m_pixelsDrawer.setPixelSizeMod(m_pixelSizeMod);
@@ -57,8 +57,8 @@ void TileSheetEditor::scrollV(const geo::Vec2 &paneSz, float wheel, bool zoomMod
 
 void TileSheetEditor::scrollH(const geo::Vec2 &paneSz, float wheelh) noexcept {
 	const auto pixelSize = m_pixelsDrawer.pixelSize(paneSz);
-	const ImVec2 sheetSize(pixelSize.x * static_cast<float>(img().columns) * TileWidth,
-						   pixelSize.y * static_cast<float>(img().rows) * TileHeight);
+	const ImVec2 sheetSize(pixelSize.x * static_cast<float>(img().columns()) * TileWidth,
+						   pixelSize.y * static_cast<float>(img().rows()) * TileHeight);
 	m_scrollOffset.x += wheelh * 0.1f;
 	m_scrollOffset.x = ox::clamp(m_scrollOffset.x, -(sheetSize.x / 2), 0.f);
 }
diff --git a/src/nostalgia/core/studio/tilesheeteditormodel.cpp b/src/nostalgia/core/studio/tilesheeteditormodel.cpp
index 973f170d..20fe3c6f 100644
--- a/src/nostalgia/core/studio/tilesheeteditormodel.cpp
+++ b/src/nostalgia/core/studio/tilesheeteditormodel.cpp
@@ -25,9 +25,9 @@ void TileSheetEditorModel::paste() {
 
 void TileSheetEditorModel::drawCommand(const geo::Point &pt, std::size_t palIdx) noexcept {
 	if (m_ongoingDrawCommand) {
-		m_updated = m_ongoingDrawCommand->append(ptToIdx(pt, m_img.columns));
+		m_updated = m_ongoingDrawCommand->append(ptToIdx(pt, m_img.columns()));
 	} else {
-		const auto idx = ptToIdx(pt, m_img.columns);
+		const auto idx = ptToIdx(pt, m_img.columns());
 		if (m_img.getPixel(idx) != palIdx) {
 			pushCommand(new DrawCommand(&m_updated, &m_img, idx, palIdx));
 		}
@@ -48,7 +48,7 @@ void TileSheetEditorModel::ackUpdate() noexcept {
 
 void TileSheetEditorModel::getFillPixels(bool *pixels, const geo::Point &pt, int oldColor) const noexcept {
 	const auto tileIdx = [this](const geo::Point &pt) noexcept {
-		return ptToIdx(pt, img().columns) / PixelsPerTile;
+		return ptToIdx(pt, img().columns()) / PixelsPerTile;
 	};
 	// get points
 	const auto leftPt = pt + geo::Point(-1, 0);
@@ -56,11 +56,11 @@ void TileSheetEditorModel::getFillPixels(bool *pixels, const geo::Point &pt, int
 	const auto topPt = pt + geo::Point(0, -1);
 	const auto bottomPt = pt + geo::Point(0, 1);
 	// calculate indices
-	const auto idx = ptToIdx(pt, m_img.columns);
-	const auto leftIdx = ptToIdx(leftPt, m_img.columns);
-	const auto rightIdx = ptToIdx(rightPt, m_img.columns);
-	const auto topIdx = ptToIdx(topPt, m_img.columns);
-	const auto bottomIdx = ptToIdx(bottomPt, m_img.columns);
+	const auto idx = ptToIdx(pt, m_img.columns());
+	const auto leftIdx = ptToIdx(leftPt, m_img.columns());
+	const auto rightIdx = ptToIdx(rightPt, m_img.columns());
+	const auto topIdx = ptToIdx(topPt, m_img.columns());
+	const auto bottomIdx = ptToIdx(bottomPt, m_img.columns());
 	const auto tile = tileIdx(pt);
 	// mark pixels to update
 	pixels[idx % PixelsPerTile] = true;
diff --git a/src/nostalgia/core/studio/tilesheetpixelgrid.cpp b/src/nostalgia/core/studio/tilesheetpixelgrid.cpp
index 534ec425..208ebd25 100644
--- a/src/nostalgia/core/studio/tilesheetpixelgrid.cpp
+++ b/src/nostalgia/core/studio/tilesheetpixelgrid.cpp
@@ -69,31 +69,31 @@ void TileSheetGrid::setBufferObjects(const geo::Vec2 &paneSize, const TileSheet
 		setBufferObject(pt1, pt2, c, vbo, pixSize);
 	};
 	// set buffer length
-	const auto width = img.columns * TileWidth;
-	const auto height = img.rows * TileHeight;
+	const auto width = img.columns() * TileWidth;
+	const auto height = img.rows() * TileHeight;
 	const auto pixelCnt = static_cast<unsigned>(width * height);
-	const auto tileCnt = static_cast<unsigned>(img.columns * img.rows);
+	const auto tileCnt = static_cast<unsigned>(img.columns() * img.rows());
 	m_bufferSet.vertices.resize((tileCnt + pixelCnt) * VertexVboLength);
 	// set buffer
 	auto i = 0ull;
 	// pixel outlines
 	constexpr auto pixOutlineColor = color32(0.4431f, 0.4901f, 0.4941f);
-	for (auto x = 0; x < img.columns * TileWidth + 1; ++x) {
-		set(i, {x, 0}, {x, img.rows * TileHeight}, pixOutlineColor);
+	for (auto x = 0; x < img.columns() * TileWidth + 1; ++x) {
+		set(i, {x, 0}, {x, img.rows() * TileHeight}, pixOutlineColor);
 		++i;
 	}
-	for (auto y = 0; y < img.rows * TileHeight + 1; ++y) {
-		set(i, {0, y}, {img.columns * TileWidth, y}, pixOutlineColor);
+	for (auto y = 0; y < img.rows() * TileHeight + 1; ++y) {
+		set(i, {0, y}, {img.columns() * TileWidth, y}, pixOutlineColor);
 		++i;
 	}
 	// tile outlines
 	constexpr auto tileOutlineColor = color32(0.f, 0.f, 0.f);
-	for (auto x = 0; x < img.columns * TileWidth + 1; x += TileWidth) {
-		set(i, {x, 0}, {x, img.rows * TileHeight}, tileOutlineColor);
+	for (auto x = 0; x < img.columns() * TileWidth + 1; x += TileWidth) {
+		set(i, {x, 0}, {x, img.rows() * TileHeight}, tileOutlineColor);
 		++i;
 	}
-	for (auto y = 0; y < img.rows * TileHeight + 1; y += TileHeight) {
-		set(i, {0, y}, {img.columns * TileWidth, y}, tileOutlineColor);
+	for (auto y = 0; y < img.rows() * TileHeight + 1; y += TileHeight) {
+		set(i, {0, y}, {img.columns() * TileWidth, y}, tileOutlineColor);
 		++i;
 	}
 }
diff --git a/src/nostalgia/core/studio/tilesheetpixels.cpp b/src/nostalgia/core/studio/tilesheetpixels.cpp
index a0b7060f..b080c8df 100644
--- a/src/nostalgia/core/studio/tilesheetpixels.cpp
+++ b/src/nostalgia/core/studio/tilesheetpixels.cpp
@@ -81,7 +81,7 @@ void TileSheetPixels::setPixelBufferObject(const geo::Vec2 &paneSize, unsigned v
 void TileSheetPixels::setBufferObjects(const geo::Vec2 &paneSize, const TileSheet &img, const Palette &pal, glutils::BufferSet *bg) noexcept {
 	const auto setPixel = [this, paneSize, bg, img, pal](std::size_t i, uint8_t p) {
 		const auto color = pal.colors[p];
-		const auto pt = idxToPt(static_cast<int>(i), img.columns);
+		const auto pt = idxToPt(static_cast<int>(i), img.columns());
 		const auto fx = static_cast<float>(pt.x);
 		const auto fy = static_cast<float>(pt.y);
 		const auto vbo = &bg->vertices[i * VertexVboLength];
@@ -89,8 +89,8 @@ void TileSheetPixels::setBufferObjects(const geo::Vec2 &paneSize, const TileShee
 		setPixelBufferObject(paneSize, i * VertexVboRows, fx, fy, color, vbo, ebo);
 	};
 	// set buffer lengths
-	const auto width = img.columns * TileWidth;
-	const auto height = img.rows * TileHeight;
+	const auto width = img.columns() * TileWidth;
+	const auto height = img.rows() * TileHeight;
 	const auto tiles = static_cast<unsigned>(width * height);
 	m_bufferSet.vertices.resize(tiles * VertexVboLength);
 	m_bufferSet.elements.resize(tiles * VertexEboLength);
diff --git a/src/nostalgia/core/typeconv.cpp b/src/nostalgia/core/typeconv.cpp
new file mode 100644
index 00000000..56c0571a
--- /dev/null
+++ b/src/nostalgia/core/typeconv.cpp
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
+ */
+
+#include <ox/std/defines.hpp>
+
+#ifndef OX_BARE_METAL
+#include <ox/claw/read.hpp>
+
+#include "typeconv.hpp"
+
+namespace nostalgia::core {
+
+struct Converter {
+	virtual bool matches(const ox::String &srcTypeName, int srcTypeVersion, const ox::String &dstTypeName, int dstTypeVersion) noexcept = 0;
+
+	virtual ~Converter() noexcept = default;
+
+	virtual ox::Error convert(const ox::Buffer &pV1Buff, void *pV2) noexcept = 0;
+};
+
+struct TileSheetV1ToV2Converter: public Converter {
+	constexpr TileSheetV1ToV2Converter() noexcept = default;
+
+	virtual ~TileSheetV1ToV2Converter() noexcept = default;
+
+	bool matches(const ox::String &srcTypeName, int srcTypeVersion, const ox::String &dstTypeName, int dstTypeVersion) noexcept final {
+		return srcTypeName == TileSheetV1::TypeName
+		    && srcTypeVersion == TileSheetV1::TypeVersion
+		    && dstTypeName == TileSheet::TypeName
+		    && dstTypeVersion == TileSheet::TypeVersion;
+	}
+
+	ox::Error convert(const ox::Buffer &pV1Buff, void *pV2) noexcept final {
+		oxRequire(v1, ox::readClaw<TileSheetV1>(pV1Buff));
+		auto v2 = static_cast<TileSheet*>(pV2);
+		v2->bpp              = v1.bpp;
+		v2->subsheet.name    = "Root";
+		v2->subsheet.rows    = v1.rows;
+		v2->subsheet.columns = v1.columns;
+		v2->subsheet.size    = v1.pixels.size();
+		v2->defaultPalette   = v1.defaultPalette;
+		v2->pal              = v1.pal;
+		v2->pixels           = v1.pixels;
+		return OxError(0);
+	}
+};
+
+static const auto converters = [] {
+	ox::Vector<ox::UniquePtr<Converter>, 1> converters;
+	converters.emplace_back(new TileSheetV1ToV2Converter());
+	return converters;
+}();
+
+static auto findConverter(const ox::String &srcTypeName, int srcTypeVersion, const ox::String &dstTypeName, int dstTypeVersion) noexcept -> ox::Result<Converter*> {
+	for (auto &c : converters) {
+		if (c->matches(srcTypeName, srcTypeVersion, dstTypeName, dstTypeVersion)) {
+			return c.get();
+		}
+	}
+	return OxError(1, "Could not find converter");
+};
+
+ox::Error convert(const ox::Buffer &srcBuffer, const ox::String &dstTypeName, int dstTypeVersion, void *dstObj) noexcept {
+	oxRequire(hdr, ox::readClawHeader(srcBuffer));
+	oxRequire(c, findConverter(hdr.typeName, hdr.typeVersion, dstTypeName, dstTypeVersion));
+	return c->convert(srcBuffer, dstObj);
+}
+
+}
+#endif
diff --git a/src/nostalgia/core/typeconv.hpp b/src/nostalgia/core/typeconv.hpp
new file mode 100644
index 00000000..c6b63012
--- /dev/null
+++ b/src/nostalgia/core/typeconv.hpp
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2016 - 2022 Gary Talent (gary@drinkingtea.net). All rights reserved.
+ */
+
+#pragma once
+
+#include <ox/std/def.hpp>
+#include <ox/std/error.hpp>
+#include <ox/std/string.hpp>
+
+#include "gfx.hpp"
+
+namespace nostalgia::core {
+
+ox::Error convert(const ox::Buffer &srcBuffer, const ox::String &dstTypeName, int dstTypeVersion, void *dstObj) noexcept;
+
+}