Squashed 'deps/nostalgia/' content from commit 9cb6bd4a

git-subtree-dir: deps/nostalgia
git-subtree-split: 9cb6bd4a32e9f39a858f72443ff5c6d40489fe22
This commit is contained in:
2023-12-23 14:17:05 -06:00
commit 587dd92414
1112 changed files with 283489 additions and 0 deletions

2
src/CMakeLists.txt Normal file
View File

@ -0,0 +1,2 @@
add_subdirectory(nostalgia)
add_subdirectory(olympic)

View File

@ -0,0 +1,38 @@
project(nostalgia CXX)
#project packages
set(NOSTALGIA_BUILD_PLAYER ON CACHE BOOL "Build Player")
set(NOSTALGIA_BUILD_STUDIO ON CACHE BOOL "Build Studio")
if(BUILDCORE_TARGET STREQUAL "gba")
set(NOSTALGIA_BUILD_STUDIO OFF)
endif()
if(APPLE)
set(CMAKE_INSTALL_NAME_DIR "@executable_path/../Library/nostalgia")
set(NOSTALGIA_DIST_BIN NostalgiaStudio.app/Contents/MacOS)
set(NOSTALGIA_DIST_LIB NostalgiaStudio.app/Contents/Library)
set(NOSTALGIA_DIST_MODULE NostalgiaStudio.app/Contents/Plugins)
set(NOSTALGIA_DIST_RESOURCES NostalgiaStudio.app/Contents/Resources)
set(NOSTALGIA_DIST_MAC_APP_CONTENTS NostalgiaStudio.app/Contents)
else()
set(NOSTALGIA_DIST_BIN bin)
set(NOSTALGIA_DIST_LIB lib)
set(NOSTALGIA_DIST_MODULE lib)
set(NOSTALGIA_DIST_RESOURCES share)
endif()
add_subdirectory(modules)
if(${NOSTALGIA_BUILD_PLAYER})
add_subdirectory(player)
endif()
if(NOT BUILDCORE_TARGET STREQUAL "gba")
add_subdirectory(tools)
if(${NOSTALGIA_BUILD_STUDIO})
add_subdirectory(studio)
endif()
endif()

View File

@ -0,0 +1,52 @@
# module dir list
add_subdirectory(core)
add_subdirectory(scene)
# module libraries
# Keel
add_library(
NostalgiaKeelModules STATIC
keelmodules.cpp
)
target_link_libraries(
NostalgiaKeelModules PUBLIC
Keel
NostalgiaCore-Keel
NostalgiaScene-Keel
)
install(
FILES
keelmodules.hpp
DESTINATION
include/nostalgia/modules
)
# Studio
if(NOSTALGIA_BUILD_STUDIO)
add_library(
NostalgiaStudioModules STATIC
studiomodules.cpp
)
target_link_libraries(
NostalgiaStudioModules PUBLIC
StudioAppLib
NostalgiaCore-Studio-ImGui
NostalgiaScene-Studio
)
install(
FILES
studiomodules.hpp
DESTINATION
include/nostalgia/modules
)
endif()
add_library(NostalgiaProfile INTERFACE)
target_compile_definitions(
NostalgiaProfile INTERFACE
OLYMPIC_PROJECT_NAME="Nostalgia"
OLYMPIC_PROJECT_NAMESPACE=nostalgia
OLYMPIC_PROJECT_DATADIR=".nostalgia"
)

View File

@ -0,0 +1,12 @@
add_subdirectory(src)
if(NOT BUILDCORE_TARGET STREQUAL "gba")
add_subdirectory(test)
endif()
install(
DIRECTORY
include/nostalgia
DESTINATION
include
)

View File

@ -0,0 +1,154 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
namespace nostalgia::core {
using Color16 = uint16_t;
/**
* Nostalgia Core logically uses 16 bit colors, but must translate that to 32
* bit colors in some implementations.
*/
using Color32 = uint32_t;
[[nodiscard]]
constexpr Color32 toColor32(Color16 nc) noexcept {
const auto r = static_cast<Color32>(((nc & 0b0000000000011111) >> 0) * 8);
const auto g = static_cast<Color32>(((nc & 0b0000001111100000) >> 5) * 8);
const auto b = static_cast<Color32>(((nc & 0b0111110000000000) >> 10) * 8);
const auto a = static_cast<Color32>(255);
return r | (g << 8) | (b << 16) | (a << 24);
}
[[nodiscard]]
constexpr uint8_t red16(Color16 c) noexcept {
return c & 0b0000000000011111;
}
[[nodiscard]]
constexpr uint8_t green16(Color16 c) noexcept {
return (c & 0b0000001111100000) >> 5;
}
[[nodiscard]]
constexpr uint8_t blue16(Color16 c) noexcept {
return (c & 0b0111110000000000) >> 10;
}
[[nodiscard]]
constexpr uint8_t alpha16(Color16 c) noexcept {
return static_cast<uint8_t>(c >> 15);
}
[[nodiscard]]
constexpr uint8_t red32(Color16 c) noexcept {
return red16(c) * 8;
}
[[nodiscard]]
constexpr uint8_t green32(Color16 c) noexcept {
return green16(c) * 8;
}
[[nodiscard]]
constexpr uint8_t blue32(Color16 c) noexcept {
return blue16(c) * 8;
}
[[nodiscard]]
constexpr uint8_t alpha32(Color16 c) noexcept {
return static_cast<uint8_t>((c >> 15) * 255);
}
[[nodiscard]]
constexpr uint8_t red32(Color32 c) noexcept {
return (c & 0x000000ff) >> 0;
}
[[nodiscard]]
constexpr uint8_t green32(Color32 c) noexcept {
return (c & 0x0000ff00) >> 8;
}
[[nodiscard]]
constexpr uint8_t blue32(Color32 c) noexcept {
return (c & 0x00ff0000) >> 16;
}
[[nodiscard]]
constexpr Color32 color32(uint8_t r, uint8_t g, uint8_t b) noexcept {
return static_cast<Color32>(r | (g << 8) | (b << 16));
}
[[nodiscard]]
constexpr Color32 color32(Color16 c) noexcept {
return color32(red32(c), green32(c), blue32(c));
}
[[nodiscard]]
constexpr Color32 color32(float r, float g, float b) noexcept {
return static_cast<Color32>(static_cast<uint8_t>(r * 255) | (static_cast<uint8_t>(g * 255) << 8) | (static_cast<uint8_t>(b * 255) << 16));
}
[[nodiscard]]
constexpr float redf(Color16 c) noexcept {
return static_cast<float>(red16(c)) / 31.f;
}
[[nodiscard]]
constexpr float greenf(Color16 c) noexcept {
return static_cast<float>(green16(c)) / 31.f;
}
[[nodiscard]]
constexpr float bluef(Color16 c) noexcept {
return static_cast<float>(blue16(c)) / 31.f;
}
[[nodiscard]]
constexpr float redf(Color32 c) noexcept {
return static_cast<float>(red32(c)) / 255.f;
}
[[nodiscard]]
constexpr float greenf(Color32 c) noexcept {
return static_cast<float>(green32(c)) / 255.f;
}
[[nodiscard]]
constexpr float bluef(Color32 c) noexcept {
return static_cast<float>(blue32(c)) / 255.f;
}
[[nodiscard]]
constexpr Color16 color16(int r, int g, int b, int a = 0) noexcept {
return static_cast<Color16>(ox::min<uint8_t>(static_cast<uint8_t>(r), 31))
| static_cast<Color16>(ox::min<uint8_t>(static_cast<uint8_t>(g), 31) << 5)
| static_cast<Color16>(ox::min<uint8_t>(static_cast<uint8_t>(b), 31) << 10)
| static_cast<Color16>(a << 15);
}
[[nodiscard]]
constexpr Color16 color16(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0) noexcept {
return static_cast<Color16>(ox::min<uint8_t>(r, 31))
| static_cast<Color16>(ox::min<uint8_t>(g, 31) << 5)
| static_cast<Color16>(ox::min<uint8_t>(b, 31) << 10)
| static_cast<Color16>(a << 15);
}
static_assert(color16(0, 31, 0) == 992);
static_assert(color16(16, 31, 0) == 1008);
static_assert(color16(16, 31, 8) == 9200);
static_assert(color16(16, 32, 8) == 9200);
}

View File

@ -0,0 +1,18 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/stringliteral.hpp>
namespace nostalgia::core {
constexpr auto TileWidth = 8;
constexpr auto TileHeight = 8;
constexpr auto PixelsPerTile = TileWidth * TileHeight;
constexpr ox::StringLiteral FileExt_ng("ng");
constexpr ox::StringLiteral FileExt_npal("npal");
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/model/desctypes.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/size.hpp>
#include <turbine/context.hpp>
#include "initparams.hpp"
namespace nostalgia::core {
class Context;
struct ContextDeleter {
void operator()(Context *p) noexcept;
};
using ContextUPtr = ox::UPtr<Context, ContextDeleter>;
ox::Result<ContextUPtr> init(turbine::Context &tctx, InitParams const&params = {}) noexcept;
}

View File

@ -0,0 +1,14 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "color.hpp"
#include "context.hpp"
#include "gfx.hpp"
#include "initparams.hpp"
#include "keelmodule.hpp"
#include "palette.hpp"
#include "ptidxconv.hpp"
#include "tilesheet.hpp"

View File

@ -0,0 +1,159 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/array.hpp>
#include <ox/std/size.hpp>
#include <ox/std/types.hpp>
#include <ox/model/def.hpp>
#include "context.hpp"
namespace nostalgia::core {
struct Sprite {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Sprite";
static constexpr auto TypeVersion = 1;
bool enabled = false;
int x = 0;
int y = 0;
unsigned tileIdx = 0;
unsigned spriteShape = 0;
unsigned spriteSize = 0;
unsigned flipX = 0;
unsigned bpp = 0;
/**
* Valid priorities: 0-3
*/
unsigned priority = 0;
};
oxModelBegin(Sprite)
oxModelField(idx)
oxModelField(x)
oxModelField(y)
oxModelField(enabled)
oxModelField(tileIdx)
oxModelField(spriteShape)
oxModelField(spriteSize)
oxModelField(flipX)
oxModelField(bpp)
oxModelField(priority)
oxModelEnd()
struct TileSheetSetEntrySection {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheetSetEntrySection";
static constexpr auto TypeVersion = 1;
int32_t begin = 0;
int32_t tiles = 0;
[[nodiscard]]
constexpr auto end() const noexcept {
return begin + tiles - 1;
}
};
oxModelBegin(TileSheetSetEntrySection)
oxModelField(begin)
oxModelField(size)
oxModelEnd()
struct TileSheetSetEntry {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheetSetEntry";
static constexpr auto TypeVersion = 1;
ox::FileAddress tilesheet;
ox::Vector<TileSheetSetEntrySection> sections;
};
oxModelBegin(TileSheetSetEntry)
oxModelField(tilesheet)
oxModelField(sections)
oxModelEnd()
struct TileSheetSet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheetSet";
static constexpr auto TypeVersion = 1;
static constexpr auto Preloadable = true;
int bpp = 0;
ox::Vector<TileSheetSetEntry> entries;
};
oxModelBegin(TileSheetSet)
oxModelField(bpp)
oxModelField(entries)
oxModelEnd()
ox::Error loadBgPalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept;
ox::Error loadSpritePalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
TileSheetSet const&set) noexcept;
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette = false) noexcept;
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette = false) noexcept;
ox::Error loadSpriteTileSheet(
Context &ctx,
TileSheetSet const&set) noexcept;
void setBgTile(Context &ctx, uint_t bgIdx, int column, int row, uint8_t tile) noexcept;
void clearBg(Context &ctx, uint_t bgIdx) noexcept;
[[nodiscard]]
uint8_t bgStatus(Context &ctx) noexcept;
void setBgStatus(Context &ctx, uint32_t status) noexcept;
[[nodiscard]]
bool bgStatus(Context &ctx, unsigned bg) noexcept;
void setBgStatus(Context &ctx, unsigned bg, bool status) noexcept;
void setBgCbb(Context &ctx, unsigned bgIdx, unsigned cbb) noexcept;
void setBgPriority(Context &ctx, uint_t bgIdx, uint_t priority) noexcept;
void hideSprite(Context &ctx, unsigned) noexcept;
void showSprite(Context &ctx, unsigned) noexcept;
void setSprite(Context &c, uint_t idx, Sprite const&s) noexcept;
[[nodiscard]]
uint_t spriteCount(Context &ctx) noexcept;
ox::Error initConsole(Context &ctx) noexcept;
void puts(Context &ctx, int column, int row, ox::CRStringView str) noexcept;
}
namespace nostalgia::core::gl {
constexpr ox::CStringView GlslVersion = "#version 330";
[[nodiscard]]
ox::Size drawSize(int scale = 5) noexcept;
void draw(core::Context &ctx, ox::Size const&renderSz) noexcept;
void draw(core::Context&, int scale = 5) noexcept;
}

View File

@ -0,0 +1,15 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace nostalgia::core {
struct InitParams {
bool glInstallDrawer = true;
uint_t glSpriteCount = 128;
uint_t glBlocksPerSprite = 64;
};
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <keel/module.hpp>
namespace nostalgia::core {
const keel::Module *keelModule() noexcept;
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/array.hpp>
#include <ox/std/point.hpp>
#include <ox/std/size.hpp>
#include <ox/std/types.hpp>
#include <ox/std/vector.hpp>
#include <ox/model/def.hpp>
#include "color.hpp"
namespace nostalgia::core {
struct NostalgiaPalette {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaPalette";
static constexpr auto TypeVersion = 1;
ox::Vector<Color16> colors = {};
};
struct Palette {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.Palette";
static constexpr auto TypeVersion = 1;
ox::Vector<Color16> colors = {};
[[nodiscard]]
constexpr Color16 color(auto idx) const noexcept {
if (idx < colors.size()) [[likely]] {
return colors[idx];
}
return 0;
}
};
oxModelBegin(NostalgiaPalette)
oxModelField(colors)
oxModelEnd()
oxModelBegin(Palette)
oxModelField(colors)
oxModelEnd()
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/point.hpp>
#include "consts.hpp"
namespace nostalgia::core {
[[nodiscard]]
constexpr std::size_t ptToIdx(int x, int y, int c, int scale = 1) noexcept {
const auto tileWidth = TileWidth * scale;
const auto tileHeight = TileHeight * scale;
const auto pixelsPerTile = tileWidth * tileHeight;
const auto colLength = static_cast<std::size_t>(pixelsPerTile);
const auto rowLength = static_cast<std::size_t>(static_cast<std::size_t>(c / tileWidth) * colLength);
const auto colStart = static_cast<std::size_t>(colLength * static_cast<std::size_t>(x / tileWidth));
const auto rowStart = static_cast<std::size_t>(rowLength * static_cast<std::size_t>(y / tileHeight));
const auto colOffset = static_cast<std::size_t>(x % tileWidth);
const auto rowOffset = static_cast<std::size_t>((y % tileHeight) * tileHeight);
return static_cast<std::size_t>(colStart + colOffset + rowStart + rowOffset);
}
[[nodiscard]]
constexpr std::size_t ptToIdx(const ox::Point &pt, int c, int scale = 1) noexcept {
return ptToIdx(pt.x, pt.y, c * TileWidth, scale);
}
[[nodiscard]]
constexpr ox::Point idxToPt(int i, int c, int scale = 1) noexcept {
const auto tileWidth = TileWidth * scale;
const auto tileHeight = TileHeight * scale;
const auto pixelsPerTile = tileWidth * tileHeight;
// prevent divide by zeros
if (!c) {
++c;
}
const auto t = i / pixelsPerTile; // tile number
const auto iti = i % pixelsPerTile; // in tile index
const auto tc = t % c; // tile column
const auto tr = t / c; // tile row
const auto itx = iti % tileWidth; // in tile x
const auto ity = iti / tileHeight; // in tile y
return {
itx + tc * tileWidth,
ity + tr * tileHeight,
};
}
static_assert(idxToPt(4, 1) == ox::Point{4, 0});
static_assert(idxToPt(8, 1) == ox::Point{0, 1});
static_assert(idxToPt(8, 2) == ox::Point{0, 1});
static_assert(idxToPt(64, 2) == ox::Point{8, 0});
static_assert(idxToPt(128, 2) == ox::Point{0, 8});
static_assert(idxToPt(129, 2) == ox::Point{1, 8});
static_assert(idxToPt(192, 2) == ox::Point{8, 8});
static_assert(idxToPt(384, 8) == ox::Point{48, 0});
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
namespace nostalgia::core {
const studio::Module *studioModule() noexcept;
}

View File

@ -0,0 +1,336 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/std/array.hpp>
#include <ox/std/point.hpp>
#include <ox/std/size.hpp>
#include <ox/std/span.hpp>
#include <ox/std/types.hpp>
#include <ox/model/def.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include "palette.hpp"
namespace nostalgia::core {
// Predecessor to TileSheet, kept for backward compatibility
struct TileSheetV1 {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.NostalgiaGraphic";
static constexpr auto TypeVersion = 1;
int8_t bpp = 0;
// rows and columns are really only used by TileSheetEditor
int rows = 1;
int columns = 1;
ox::FileAddress defaultPalette;
Palette pal;
ox::Vector<uint8_t> pixels = {};
};
struct TileSheetV2 {
using SubSheetIdx = ox::Vector<std::size_t, 4>;
struct SubSheet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet.SubSheet";
static constexpr auto TypeVersion = 1;
ox::String name;
int columns = 0;
int rows = 0;
ox::Vector<SubSheet> subsheets;
ox::Vector<uint8_t> pixels;
constexpr SubSheet() noexcept = default;
constexpr SubSheet(ox::CRStringView pName, int pColumns, int pRows, int bpp) noexcept:
name(pName),
columns(pColumns),
rows(pRows),
pixels(static_cast<size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
}
};
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
static constexpr auto TypeVersion = 2;
int8_t bpp = 4;
ox::FileAddress defaultPalette;
SubSheet subsheet{"Root", 1, 1, bpp};
};
using SubSheetId = int32_t;
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 = 3;
SubSheetId id = 0;
ox::String name;
int columns = 0;
int rows = 0;
ox::Vector<SubSheet> subsheets;
ox::Vector<uint8_t> pixels;
constexpr SubSheet() noexcept = default;
constexpr SubSheet(SubSheet const&other) noexcept = default;
SubSheet(SubSheet &&other) noexcept;
SubSheet(
SubSheetId pId,
ox::CRStringView pName,
int pColumns,
int pRows,
int bpp) noexcept;
SubSheet(
SubSheetId pId,
ox::CRStringView pName,
int pColumns,
int pRows,
ox::Vector<uint8_t> pPixels) noexcept;
constexpr SubSheet &operator=(const SubSheet &other) noexcept = default;
SubSheet &operator=(SubSheet &&other) noexcept;
[[nodiscard]]
std::size_t idx(ox::Point const&pt) const noexcept;
/**
* Reads all pixels of this sheet or its children into the given pixel list
* @param pixels
*/
void readPixelsTo(ox::Vector<uint8_t> *pPixels, int8_t pBpp) const noexcept;
/**
* Reads all pixels of this sheet or its children into the given pixel list
* @param pixels
*/
void readPixelsTo(ox::Vector<uint8_t> *pPixels) const noexcept;
[[nodiscard]]
constexpr std::size_t size() const noexcept {
return static_cast<std::size_t>(columns) * static_cast<std::size_t>(rows);
}
[[nodiscard]]
std::size_t unusedPixels() const noexcept;
[[nodiscard]]
uint8_t getPixel4Bpp(std::size_t idx) const noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(std::size_t idx) const noexcept;
[[nodiscard]]
uint8_t getPixel(int8_t pBpp, std::size_t idx) const noexcept;
[[nodiscard]]
uint8_t getPixel4Bpp(const ox::Point &pt) const noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(const ox::Point &pt) const noexcept;
[[nodiscard]]
uint8_t getPixel(int8_t pBpp, const ox::Point &pt) const noexcept;
constexpr auto walkPixels(int8_t pBpp, auto callback) const noexcept {
if (pBpp == 4) {
const auto pixelCnt = ox::min<std::size_t>(static_cast<std::size_t>(columns * rows * PixelsPerTile) / 2,
pixels.size());
//oxAssert(pixels.size() == pixelCnt, "Pixel count does not match rows and columns");
for (std::size_t i = 0; i < pixelCnt; ++i) {
const auto colorIdx1 = static_cast<uint8_t>(pixels[i] & 0xF);
const auto colorIdx2 = static_cast<uint8_t>(pixels[i] >> 4);
callback(i * 2 + 0, colorIdx1);
callback(i * 2 + 1, colorIdx2);
}
} else {
const auto pixelCnt = ox::min<std::size_t>(
static_cast<std::size_t>(columns * rows * PixelsPerTile),
pixels.size());
for (std::size_t i = 0; i < pixelCnt; ++i) {
const auto p = pixels[i];
callback(i, p);
}
}
}
void setPixel(int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept;
void setPixel(int8_t pBpp, ox::Point const&pt, uint8_t palIdx) noexcept;
ox::Error setPixelCount(int8_t pBpp, std::size_t cnt) noexcept;
/**
* Gets a count of the pixels in this sheet, and not that of its children.
* @param pBpp bits per pixel, need for knowing how to count the pixels
* @return a count of the pixels in this sheet
*/
[[nodiscard]]
unsigned pixelCnt(int8_t pBpp) const noexcept;
/**
* Gets the offset in tiles of the desired subsheet.
*/
ox::Result<unsigned> getTileOffset(
ox::SpanView<ox::StringView> const&pNamePath,
int8_t pBpp,
std::size_t pIt = 0,
unsigned pCurrentTotal = 0) const noexcept;
ox::Result<SubSheetId> getIdFor(
ox::SpanView<ox::StringView> const&pNamePath,
std::size_t pIt = 0) const noexcept;
ox::Result<ox::StringView> getNameFor(SubSheetId pId) const noexcept;
};
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.TileSheet";
static constexpr auto TypeVersion = 3;
int8_t bpp = 4;
SubSheetId idIt = 0;
ox::FileAddress defaultPalette;
SubSheet subsheet{0, "Root", 1, 1, bpp};
constexpr TileSheet() noexcept = default;
TileSheet(TileSheet const&other) noexcept = default;
inline TileSheet(TileSheet &&other) noexcept:
bpp(other.bpp),
idIt(other.idIt),
defaultPalette(std::move(other.defaultPalette)),
subsheet(std::move(other.subsheet)) {
}
TileSheet &operator=(TileSheet const&other) noexcept;
TileSheet &operator=(TileSheet &&other) noexcept;
[[nodiscard]]
SubSheetIdx validateSubSheetIdx(
SubSheetIdx const&pIdx,
std::size_t pIdxIt,
const SubSheet *pSubsheet) noexcept;
/**
* validateSubSheetIdx takes a SubSheetIdx and moves the index to the
* preceding or parent sheet if the current corresponding sheet does
* not exist.
* @param idx SubSheetIdx to validate and correct
* @return a valid version of idx
*/
[[nodiscard]]
SubSheetIdx validateSubSheetIdx(const SubSheetIdx &idx) noexcept;
[[nodiscard]]
static SubSheet const&getSubSheet(
SubSheetIdx const&idx,
std::size_t idxIt,
const SubSheet *pSubsheet) noexcept;
[[nodiscard]]
static SubSheet &getSubSheet(
SubSheetIdx const&idx,
std::size_t idxIt,
SubSheet *pSubsheet) noexcept;
[[nodiscard]]
const SubSheet &getSubSheet(SubSheetIdx const&idx) const noexcept;
[[nodiscard]]
SubSheet &getSubSheet(SubSheetIdx const&idx) noexcept;
ox::Error addSubSheet(SubSheetIdx const&idx) noexcept;
[[nodiscard]]
static ox::Error rmSubSheet(
SubSheetIdx const&idx,
std::size_t idxIt,
SubSheet *pSubsheet) noexcept;
[[nodiscard]]
ox::Error rmSubSheet(SubSheetIdx const&idx) noexcept;
[[nodiscard]]
uint8_t getPixel4Bpp(
ox::Point const&pt,
SubSheetIdx const&subsheetIdx) const noexcept;
[[nodiscard]]
uint8_t getPixel8Bpp(
ox::Point const&pt,
SubSheetIdx const&subsheetIdx) const noexcept;
ox::Result<SubSheetId> getIdFor(ox::CRStringView path) const noexcept;
ox::Result<unsigned> getTileOffset(ox::CRStringView pNamePath) const noexcept;
ox::Result<ox::StringView> getNameFor(SubSheetId pId) const noexcept;
[[nodiscard]]
ox::Vector<uint8_t> pixels() const noexcept;
};
using TileSheetV3 = TileSheet;
struct CompactTileSheet {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.CompactTileSheet";
static constexpr auto TypeVersion = 1;
int8_t bpp = 0;
ox::FileAddress defaultPalette;
ox::Vector<uint8_t> pixels = {};
};
oxModelBegin(TileSheetV1)
oxModelField(bpp)
oxModelField(rows)
oxModelField(columns)
oxModelField(defaultPalette)
oxModelField(pal)
oxModelField(pixels)
oxModelEnd()
oxModelBegin(TileSheetV2::SubSheet)
oxModelField(name);
oxModelField(rows);
oxModelField(columns);
oxModelField(subsheets)
oxModelField(pixels)
oxModelEnd()
oxModelBegin(TileSheetV2)
oxModelField(bpp)
oxModelField(defaultPalette)
oxModelField(subsheet)
oxModelEnd()
oxModelBegin(TileSheetV3::SubSheet)
oxModelField(name);
oxModelField(rows);
oxModelField(columns);
oxModelField(subsheets)
oxModelField(pixels)
oxModelEnd()
oxModelBegin(TileSheetV3)
oxModelField(bpp)
oxModelField(idIt)
oxModelField(defaultPalette)
oxModelField(subsheet)
oxModelEnd()
oxModelBegin(CompactTileSheet)
oxModelField(bpp)
oxModelField(defaultPalette)
oxModelField(pixels)
oxModelEnd()
ox::Vector<uint32_t> resizeTileSheetData(
ox::Vector<uint32_t> const&srcPixels,
ox::Size const&srcSize,
int scale = 2) noexcept;
}

View File

@ -0,0 +1,33 @@
add_library(
NostalgiaCore
gfx.cpp
tilesheet.cpp
)
add_subdirectory(gba)
if(NOT BUILDCORE_TARGET STREQUAL "gba")
add_subdirectory(opengl)
endif()
target_include_directories(
NostalgiaCore PUBLIC
../include
)
target_link_libraries(
NostalgiaCore PUBLIC
Turbine
)
add_subdirectory(keel)
if(NOSTALGIA_BUILD_STUDIO)
add_subdirectory(studio)
endif()
install(
TARGETS
NostalgiaCore
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@ -0,0 +1,21 @@
add_library(
NostalgiaCore-GBA OBJECT
context.cpp
gfx.cpp
panic.cpp
)
target_include_directories(
NostalgiaCore-GBA PUBLIC
../../include
)
target_link_libraries(
NostalgiaCore-GBA PUBLIC
TeaGBA
Keel
Turbine
)
if(BUILDCORE_TARGET STREQUAL "gba")
set_source_files_properties(gfx.cpp PROPERTIES COMPILE_FLAGS -marm)
target_link_libraries(NostalgiaCore PUBLIC NostalgiaCore-GBA)
endif()

View File

@ -0,0 +1,26 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/gfx.hpp>
#include "context.hpp"
namespace nostalgia::core {
void ContextDeleter::operator()(Context *p) noexcept {
ox::safeDelete(p);
}
Context::Context(turbine::Context &tctx) noexcept: turbineCtx(tctx) {
}
ox::Error initGfx(Context &ctx, InitParams const&) noexcept;
ox::Result<ContextUPtr> init(turbine::Context &tctx, InitParams const&params) noexcept {
auto ctx = ox::make_unique<Context>(tctx);
oxReturnError(initGfx(*ctx, params));
return ContextUPtr(std::move(ctx));
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <nostalgia/core/context.hpp>
namespace nostalgia::core {
struct BgCbbData {
unsigned bpp = 4;
};
class Context {
public:
turbine::Context &turbineCtx;
ox::Array<BgCbbData, 4> cbbData;
explicit Context(turbine::Context &tctx) noexcept;
Context(Context &other) noexcept = delete;
Context(Context const&other) noexcept = delete;
Context(Context const&&other) noexcept = delete;
virtual ~Context() noexcept = default;
[[nodiscard]]
ox::MemFS const&rom() const noexcept {
return static_cast<ox::MemFS const&>(*turbine::rom(turbineCtx));
}
};
}

View File

@ -0,0 +1,367 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/fs/fs.hpp>
#include <ox/mc/mc.hpp>
#include <ox/std/array.hpp>
#include <teagba/addresses.hpp>
#include <teagba/gfx.hpp>
#include <teagba/registers.hpp>
#include <turbine/turbine.hpp>
#include <nostalgia/core/color.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/tilesheet.hpp>
#include "context.hpp"
namespace nostalgia::core {
constexpr auto GbaTileColumns = 32;
constexpr auto GbaTileRows = 32;
constexpr auto SpriteCount = 128;
struct GbaPaletteTarget {
static constexpr auto TypeName = Palette::TypeName;
static constexpr auto TypeVersion = Palette::TypeVersion;
volatile uint16_t *palette = nullptr;
};
struct GbaTileMapTarget {
static constexpr auto TypeName = CompactTileSheet::TypeName;
static constexpr auto TypeVersion = CompactTileSheet::TypeVersion;
unsigned &bpp;
ox::FileAddress defaultPalette;
volatile uint16_t *tileMap = nullptr;
// the following values are not actually in CompactTileSheet,
// and only exist to communicate with the loading process
size_t tileWriteIdx = 0;
unsigned targetBpp = 0;
TileSheetSetEntry const*setEntry = nullptr;
};
constexpr ox::Error model(auto *io, ox::CommonPtrWith<GbaPaletteTarget> auto *t) noexcept {
oxReturnError(io->template setTypeInfo<Palette>());
if (t->palette) {
const auto colorHandler = [t](std::size_t i, const Color16 *c) {
t->palette[i] = *c;
return ox::Error{};
};
return io->template field<Color16, decltype(colorHandler)>("colors", colorHandler);
} else {
constexpr auto colorHandler = [](std::size_t, const Color16*) {
return ox::Error{};
};
return io->template field<Color16, decltype(colorHandler)>("colors", colorHandler);
}
}
[[nodiscard]]
static bool loadPixel(TileSheetSetEntry const&setEntry, size_t &sectionIdx, int tileIdx) noexcept {
if (setEntry.sections.size() <= sectionIdx) {
return false;
}
auto &section = setEntry.sections[sectionIdx];
if (tileIdx < section.begin) {
return false;
}
if (tileIdx > section.end()) {
if (sectionIdx >= setEntry.sections.size()) {
return false;
}
++sectionIdx;
return tileIdx > section.begin && tileIdx <= section.end();
}
return true;
}
constexpr ox::Error model(auto *io, ox::CommonPtrWith<GbaTileMapTarget> auto *t) noexcept {
oxReturnError(io->template setTypeInfo<CompactTileSheet>());
oxReturnError(io->field("bpp", &t->bpp));
oxReturnError(io->field("defaultPalette", &t->defaultPalette));
if (t->targetBpp == 0) {
t->targetBpp = t->bpp;
}
if (t->targetBpp != t->bpp && t->bpp == 8) {
return OxError(1, "Cannot load an 8 BPP tilesheet into a 4 BPP CBB");
}
ox::Error out{};
if (t->setEntry) {
// The following code is atrocious, but it works.
// It might be possible to clean it up a little, but it probably
// cannot be seriously optimized without preloading TileSheets.
size_t sectionIdx = 0;
if (t->targetBpp == t->bpp) {
uint16_t intermediate = 0;
size_t const fourBpp = t->bpp == 4;
const auto handleTileMap = [t, &intermediate, &sectionIdx, fourBpp]
(std::size_t i, uint8_t const*tile) {
auto const tileIdx = static_cast<int>((i * (2 * fourBpp)) / PixelsPerTile);
if (!loadPixel(*t->setEntry, sectionIdx, tileIdx)) {
return ox::Error{};
}
if (i & 1) { // i is odd
intermediate |= static_cast<uint16_t>(*tile) << 8;
t->tileMap[t->tileWriteIdx] = intermediate;
++t->tileWriteIdx;
} else { // i is even
intermediate = *tile & 0x00ff;
}
return ox::Error{};
};
out = io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
} else if (t->targetBpp > t->bpp) { // 4 -> 8 bits
const auto handleTileMap = [t, &sectionIdx](std::size_t i, uint8_t const*tile) {
auto constexpr BytesPerTile4Bpp = 32;
auto const tileIdx = static_cast<int>(i / BytesPerTile4Bpp);
if (!loadPixel(*t->setEntry, sectionIdx, tileIdx)) {
return ox::Error{};
}
uint16_t const px1 = *tile & 0xf;
uint16_t const px2 = *tile >> 4;
t->tileMap[t->tileWriteIdx] = static_cast<uint16_t>(px1 | (px2 << 8));
++t->tileWriteIdx;
return ox::Error{};
};
out = io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
}
} else {
uint16_t intermediate = 0;
const auto handleTileMap = [t, &intermediate](std::size_t i, const uint8_t*tile) {
if (i & 1) { // i is odd
intermediate |= static_cast<uint16_t>(*tile) << 8;
t->tileMap[i / 2] = intermediate;
} else { // i is even
intermediate = *tile & 0x00ff;
}
return ox::Error{};
};
out = io->template field<uint8_t, decltype(handleTileMap)>("tileMap", handleTileMap);
}
return out;
}
ox::Error initGfx(Context&, InitParams const&) noexcept {
for (auto bgCtl = &REG_BG0CTL; bgCtl <= &REG_BG3CTL; bgCtl += 2) {
teagba::bgSetSbb(*bgCtl, 28);
}
for (uint16_t i = 0; i < SpriteCount; ++i) {
auto &sa = teagba::spriteAttr(i);
sa.idx = i;
}
return {};
}
ox::Error loadBgPalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept {
auto &rom = ctx.rom();
GbaPaletteTarget const palTarget{.palette = MEM_BG_PALETTE};
oxRequire(palStat, rom.stat(paletteAddr));
oxRequire(pal, rom.directAccess(paletteAddr));
oxReturnError(ox::readMC(pal, static_cast<std::size_t>(palStat.size), &palTarget));
return {};
}
ox::Error loadSpritePalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept {
auto &rom = ctx.rom();
GbaPaletteTarget const palTarget{.palette = MEM_SPRITE_PALETTE};
oxRequire(palStat, rom.stat(paletteAddr));
oxRequire(pal, rom.directAccess(paletteAddr));
oxReturnError(ox::readMC(pal, static_cast<std::size_t>(palStat.size), &palTarget));
return {};
}
static ox::Error loadTileSheetSet(
Context &ctx,
uint16_t *tileMapTargetMem,
TileSheetSet const&set) noexcept {
auto &rom = ctx.rom();
size_t tileWriteIdx = 0;
for (auto const&entry : set.entries) {
oxRequire(tsStat, rom.stat(entry.tilesheet));
oxRequire(ts, rom.directAccess(entry.tilesheet));
unsigned tilesheetBpp{};
GbaTileMapTarget target{
.bpp = tilesheetBpp,
.defaultPalette = {},
.tileMap = tileMapTargetMem + tileWriteIdx,
.targetBpp = static_cast<unsigned>(set.bpp),
.setEntry = &entry,
};
oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
tileWriteIdx += target.tileWriteIdx;
}
return {};
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette) noexcept {
auto &rom = ctx.rom();
oxRequire(tsStat, rom.stat(tilesheetAddr));
oxRequire(ts, rom.directAccess(tilesheetAddr));
GbaTileMapTarget target{
.bpp = ctx.cbbData[cbb].bpp,
.defaultPalette = {},
.tileMap = MEM_BG_TILES[cbb].data(),
};
oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
// update bpp of all bgs with the updated cbb
const auto bpp = ctx.cbbData[cbb].bpp;
teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) {
if (teagba::bgCbb(bgCtl) == cbb) {
teagba::bgSetBpp(bgCtl, bpp);
}
});
if (loadDefaultPalette && target.defaultPalette) {
oxReturnError(loadBgPalette(ctx, target.defaultPalette));
}
return {};
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
TileSheetSet const&set) noexcept {
auto const bpp = static_cast<unsigned>(set.bpp);
oxReturnError(loadTileSheetSet(ctx, MEM_BG_TILES[cbb].data(), set));
// update bpp of all bgs with the updated cbb
ctx.cbbData[cbb].bpp = bpp;
teagba::iterateBgCtl([bpp, cbb](volatile BgCtl &bgCtl) {
if (teagba::bgCbb(bgCtl) == cbb) {
teagba::bgSetBpp(bgCtl, bpp);
}
});
return {};
}
static void setSpritesBpp(unsigned const bpp) noexcept {
auto const eightBpp = static_cast<uint16_t >(bpp == 8);
for (auto i = 0u; i < SpriteCount; ++i) {
auto &sa = teagba::spriteAttr(i);
auto &a = sa.attr0;
a |= static_cast<uint16_t>((a & ~static_cast<uint16_t>(1u << 13)) | (eightBpp << 13));
}
}
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette) noexcept {
auto &rom = ctx.rom();
oxRequire(tsStat, rom.stat(tilesheetAddr));
oxRequire(ts, rom.directAccess(tilesheetAddr));
unsigned bpp{};
GbaTileMapTarget target{
.bpp = bpp,
.defaultPalette = {},
.tileMap = MEM_SPRITE_TILES,
};
oxReturnError(ox::readMC(ts, static_cast<std::size_t>(tsStat.size), &target));
if (loadDefaultPalette && target.defaultPalette) {
oxReturnError(loadSpritePalette(ctx, target.defaultPalette));
}
setSpritesBpp(bpp);
return {};
}
ox::Error loadSpriteTileSheet(
Context &ctx,
TileSheetSet const&set) noexcept {
auto const bpp = static_cast<unsigned>(set.bpp);
oxReturnError(loadTileSheetSet(ctx, MEM_SPRITE_TILES, set));
setSpritesBpp(bpp);
return {};
}
void setBgTile(Context&, uint_t bgIdx, int column, int row, uint8_t tile) noexcept {
const auto tileIdx = static_cast<std::size_t>(row * GbaTileColumns + column);
MEM_BG_MAP[bgIdx][tileIdx] = tile;
}
void clearBg(Context&, uint_t bgIdx) noexcept {
memset(MEM_BG_MAP[bgIdx].data(), 0, GbaTileRows * GbaTileColumns);
}
uint8_t bgStatus(Context&) noexcept {
return (REG_DISPCTL >> 8u) & 0b1111u;
}
void setBgStatus(Context&, uint32_t status) noexcept {
constexpr auto BgStatus = 8;
REG_DISPCTL = (REG_DISPCTL & ~0b111100000000u) | status << BgStatus;
}
bool bgStatus(Context&, unsigned bg) noexcept {
return (REG_DISPCTL >> (8 + bg)) & 1;
}
void setBgStatus(Context&, unsigned bg, bool status) noexcept {
constexpr auto Bg0Status = 8;
const auto mask = static_cast<uint32_t>(status) << (Bg0Status + bg);
REG_DISPCTL = REG_DISPCTL | ((REG_DISPCTL & ~mask) | mask);
}
void setBgCbb(Context &ctx, unsigned bgIdx, unsigned cbb) noexcept {
auto &bgCtl = regBgCtl(bgIdx);
const auto &cbbData = ctx.cbbData[cbb];
teagba::bgSetBpp(bgCtl, cbbData.bpp);
teagba::bgSetCbb(bgCtl, cbb);
}
void setBgPriority(Context&, uint_t bgIdx, uint_t priority) noexcept {
auto &bgCtl = regBgCtl(bgIdx);
bgCtl = (bgCtl & 0b1111'1111'1111'1100u) | (priority & 0b11);
}
void hideSprite(Context&, unsigned idx) noexcept {
//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
teagba::addSpriteUpdate({
.attr0 = uint16_t{0b11 << 8},
.idx = static_cast<uint16_t>(idx),
});
}
void showSprite(Context&, unsigned idx) noexcept {
//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
teagba::addSpriteUpdate({
.attr0 = 0,
.idx = static_cast<uint16_t>(idx),
});
}
void setSprite(Context&, uint_t idx, Sprite const&s) noexcept {
//oxAssert(g_spriteUpdates < config::GbaSpriteBufferLen, "Sprite update buffer overflow");
uint16_t const eightBpp = s.bpp == 8;
teagba::addSpriteUpdate({
.attr0 = static_cast<uint16_t>(
(static_cast<uint16_t>(s.y & ox::onMask<uint8_t>(0b111'1111)))
| (static_cast<uint16_t>(1) << 10) // enable alpha
| (static_cast<uint16_t>(eightBpp) << 13)
| (static_cast<uint16_t>(s.spriteShape) << 14)),
.attr1 = static_cast<uint16_t>(
(static_cast<uint16_t>(s.x) & ox::onMask<uint8_t>(8))
| (static_cast<uint16_t>(s.flipX) << 12)
| (static_cast<uint16_t>(s.spriteSize) << 14)),
.attr2 = static_cast<uint16_t>(
// double tileIdx if 8 bpp
(static_cast<uint16_t>((s.tileIdx * (1 + eightBpp)) & ox::onMask<uint16_t>(8)))
| (static_cast<uint16_t>(s.priority & 0b11) << 10)),
.idx = static_cast<uint16_t>(idx),
});
}
uint_t spriteCount(Context&) noexcept {
return SpriteCount;
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <nostalgia/core/context.hpp>
namespace nostalgia::core {
ox::Error initGfx(Context &ctx, InitParams const&) noexcept;
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/def.hpp>
#include <keel/media.hpp>
#include <turbine/turbine.hpp>
#include <teagba/addresses.hpp>
#include <teagba/bios.hpp>
#include <nostalgia/core/core.hpp>
#include "gfx.hpp"
#define HEAP_BEGIN (reinterpret_cast<char*>(MEM_EWRAM_BEGIN))
#define HEAP_SIZE ((MEM_EWRAM_END - MEM_EWRAM_BEGIN) / 2)
#define HEAP_END (reinterpret_cast<char*>(MEM_EWRAM_BEGIN + HEAP_SIZE))
namespace ox {
using namespace nostalgia::core;
void panic(const char *file, int line, const char *panicMsg, ox::Error const&err) noexcept {
// reset heap to make sure we have enough memory to allocate context data
ox::heapmgr::initHeap(HEAP_BEGIN, HEAP_END);
auto tctx = turbine::init(keel::loadRomFs("").unwrap(), "Nostalgia").unwrap();
auto ctx = init(*tctx).unwrap();
oxIgnoreError(initGfx(*ctx, {}));
oxIgnoreError(initConsole(*ctx));
setBgStatus(*ctx, 0, true);
clearBg(*ctx, 0);
ox::BString<23> serr = "Error code: ";
serr += static_cast<int64_t>(err);
puts(*ctx, 32 + 1, 1, "SADNESS...");
puts(*ctx, 32 + 1, 4, "UNEXPECTED STATE:");
puts(*ctx, 32 + 2, 6, panicMsg);
if (err) {
puts(*ctx, 32 + 2, 8, serr);
}
puts(*ctx, 32 + 1, 15, "PLEASE RESTART THE SYSTEM");
// print to terminal if in mGBA
oxErrf("\033[31;1;1mPANIC:\033[0m [{}:{}]: {}\n", file, line, panicMsg);
if (err.msg) {
oxErrf("\tError Message:\t{}\n", err.msg);
}
oxErrf("\tError Code:\t{}\n", static_cast<ErrorCode>(err));
if (err.file != nullptr) {
oxErrf("\tError Location:\t{}:{}\n", err.file, err.line);
}
// disable all interrupt handling and IntrWait on no interrupts
REG_IE = 0;
teagba::intrwait(0, 0);
}
}

View File

@ -0,0 +1,165 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
// map ASCII values to the nostalgia charset
constexpr ox::Array<char, 128> charMap = {
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0, // space
38, // !
0, // "
49, // #
0, // $
0, // %
0, // &
0, // '
42, // (
43, // )
0, // *
0, // +
37, // ,
0, // -
39, // .
0, // /
27, // 0
28, // 1
29, // 2
30, // 3
31, // 4
32, // 5
33, // 6
34, // 7
35, // 8
36, // 9
40, // :
0, // ;
0, // <
41, // =
0, // >
0, // ?
0, // @
1, // A
2, // B
3, // C
4, // D
5, // E
6, // F
7, // G
8, // H
9, // I
10, // J
11, // K
12, // L
13, // M
14, // N
15, // O
16, // P
17, // Q
18, // R
19, // S
20, // T
21, // U
22, // V
23, // W
24, // X
25, // Y
26, // Z
44, // [
0, // backslash
45, // ]
0, // ^
0, // _
0, // `
1, // a
2, // b
3, // c
4, // d
5, // e
6, // f
7, // g
8, // h
9, // i
10, // j
11, // k
12, // l
13, // m
14, // n
15, // o
16, // p
17, // q
18, // r
19, // s
20, // t
21, // u
22, // v
23, // w
24, // x
25, // y
26, // z
46, // {
51, // |
48, // }
50, // ~
};
ox::Error initConsole(Context &ctx) noexcept {
constexpr ox::FileAddress TilesheetAddr = ox::StringLiteral("/TileSheets/Charset.ng");
constexpr ox::FileAddress PaletteAddr = ox::StringLiteral("/Palettes/Charset.npal");
setBgStatus(ctx, 0b0001);
setBgCbb(ctx, 0, 0);
oxReturnError(loadBgTileSheet(ctx, 0, TilesheetAddr));
return loadBgPalette(ctx, PaletteAddr);
}
void puts(
Context &ctx,
int const column,
int const row,
ox::CRStringView str) noexcept {
auto const col = static_cast<uint_t>(column);
for (auto i = 0u; i < str.bytes(); ++i) {
setBgTile(
ctx,
0,
static_cast<int>(col + i),
row,
static_cast<uint8_t>(charMap[static_cast<uint8_t>(str[i])]));
}
}
}

View File

@ -0,0 +1,19 @@
add_library(
NostalgiaCore-Keel
keelmodule.cpp
typeconv.cpp
)
target_link_libraries(
NostalgiaCore-Keel PUBLIC
Keel
NostalgiaCore
)
install(
TARGETS
NostalgiaCore-Keel
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@ -0,0 +1,76 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/model/model.hpp>
#include <keel/asset.hpp>
#include <keel/module.hpp>
#include <nostalgia/core/palette.hpp>
#include <nostalgia/core/tilesheet.hpp>
#include "typeconv.hpp"
namespace nostalgia::core {
class KeelModule: public keel::Module {
private:
NostalgiaPaletteToPaletteConverter m_nostalgiaPaletteToPaletteConverter;
TileSheetV1ToTileSheetV2Converter m_tileSheetV1ToTileSheetV2Converter;
TileSheetV2ToTileSheetV3Converter m_tileSheetV2ToTileSheetConverter;
TileSheetToCompactTileSheetConverter m_tileSheetToCompactTileSheetConverter;
public:
[[nodiscard]]
ox::String id() const noexcept override {
return ox::String("net.drinkingtea.nostalgia.core");
}
[[nodiscard]]
ox::Vector<keel::TypeDescGenerator> types() const noexcept final {
return {
keel::generateTypeDesc<TileSheetV1>,
keel::generateTypeDesc<TileSheetV2>,
keel::generateTypeDesc<TileSheet>,
keel::generateTypeDesc<CompactTileSheet>,
keel::generateTypeDesc<Palette>,
};
}
[[nodiscard]]
ox::Vector<const keel::BaseConverter*> converters() const noexcept final {
return {
&m_nostalgiaPaletteToPaletteConverter,
&m_tileSheetV1ToTileSheetV2Converter,
&m_tileSheetV2ToTileSheetConverter,
&m_tileSheetToCompactTileSheetConverter,
};
}
[[nodiscard]]
ox::Vector<keel::PackTransform> packTransforms() const noexcept final {
return {
// convert tilesheets to CompactTileSheets
[](keel::Context &ctx, ox::Buffer &buff) -> ox::Error {
oxRequire(hdr, keel::readAssetHeader(buff));
const auto typeId = ox::buildTypeId(
hdr.clawHdr.typeName, hdr.clawHdr.typeVersion, hdr.clawHdr.typeParams);
if (typeId == ox::buildTypeId<TileSheetV1>() ||
typeId == ox::buildTypeId<TileSheetV2>() ||
typeId == ox::buildTypeId<TileSheet>()) {
oxReturnError(keel::convertBuffToBuff<core::CompactTileSheet>(
ctx, buff, ox::ClawFormat::Metal).moveTo(buff));
}
return {};
},
};
}
};
static const KeelModule mod;
const keel::Module *keelModule() noexcept {
return &mod;
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "typeconv.hpp"
namespace nostalgia::core {
ox::Error NostalgiaPaletteToPaletteConverter::convert(
keel::Context&,
NostalgiaPalette &src,
Palette &dst) const noexcept {
dst.colors = std::move(src.colors);
return {};
}
ox::Error TileSheetV1ToTileSheetV2Converter::convert(
keel::Context&,
TileSheetV1 &src,
TileSheetV2 &dst) const noexcept {
dst.bpp = src.bpp;
dst.defaultPalette = std::move(src.defaultPalette);
dst.subsheet.name = "Root";
dst.subsheet.rows = src.rows;
dst.subsheet.columns = src.columns;
dst.subsheet.pixels = std::move(src.pixels);
return {};
}
void TileSheetV2ToTileSheetV3Converter::convertSubsheet(
TileSheetV2::SubSheet &src,
TileSheetV3::SubSheet &dst,
SubSheetId &idIt) noexcept {
dst.id = idIt;
dst.name = std::move(src.name);
dst.columns = src.columns;
dst.rows = src.rows;
dst.pixels = std::move(src.pixels);
++idIt;
dst.subsheets.resize(src.subsheets.size());
for (auto i = 0u; i < src.subsheets.size(); ++i) {
convertSubsheet(src.subsheets[i], dst.subsheets[i], idIt);
}
}
ox::Error TileSheetV2ToTileSheetV3Converter::convert(
keel::Context&,
TileSheetV2 &src,
TileSheetV3 &dst) const noexcept {
dst.bpp = src.bpp;
dst.defaultPalette = std::move(src.defaultPalette);
convertSubsheet(src.subsheet, dst.subsheet, dst.idIt);
return {};
}
ox::Error TileSheetToCompactTileSheetConverter::convert(
keel::Context&,
TileSheet &src,
CompactTileSheet &dst) const noexcept {
dst.bpp = src.bpp;
dst.defaultPalette = std::move(src.defaultPalette);
dst.pixels = src.pixels();
return {};
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/def.hpp>
#include <keel/typeconv.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/palette.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::core {
// Type converters
class NostalgiaPaletteToPaletteConverter: public keel::Converter<NostalgiaPalette, Palette> {
ox::Error convert(keel::Context&, NostalgiaPalette &src, Palette &dst) const noexcept final;
};
class TileSheetV1ToTileSheetV2Converter: public keel::Converter<TileSheetV1, TileSheetV2> {
ox::Error convert(keel::Context&, TileSheetV1 &src, TileSheetV2 &dst) const noexcept final;
};
class TileSheetV2ToTileSheetV3Converter: public keel::Converter<TileSheetV2, TileSheetV3> {
static void convertSubsheet(
TileSheetV2::SubSheet &src,
TileSheetV3::SubSheet &dst,
SubSheetId &idIt) noexcept;
ox::Error convert(keel::Context&, TileSheetV2 &src, TileSheetV3 &dst) const noexcept final;
};
class TileSheetToCompactTileSheetConverter: public keel::Converter<TileSheet, CompactTileSheet> {
ox::Error convert(keel::Context&, TileSheet &src, CompactTileSheet &dst) const noexcept final;
};
}

View File

@ -0,0 +1,9 @@
target_sources(
NostalgiaCore PRIVATE
context.cpp
gfx.cpp
)
target_link_libraries(
NostalgiaCore PUBLIC
GlUtils
)

View File

@ -0,0 +1,32 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "context.hpp"
#include "gfx.hpp"
namespace nostalgia::core {
void ContextDeleter::operator()(Context *p) noexcept {
ox::safeDelete(p);
}
Context::Context(turbine::Context &tctx, InitParams const&params) noexcept:
turbineCtx(tctx),
spriteBlocks(params.glSpriteCount, params.glBlocksPerSprite),
drawer(*this),
spriteCount(params.glSpriteCount),
blocksPerSprite(params.glBlocksPerSprite) {
}
Context::~Context() noexcept {
shutdownGfx(*this);
}
ox::Result<ContextUPtr> init(turbine::Context &tctx, InitParams const&params) noexcept {
auto ctx = ox::make_unique<Context>(tctx, params);
oxReturnError(initGfx(*ctx, params));
return ContextUPtr(ctx.release());
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
#include <glutils/glutils.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/context.hpp>
#include "gfx.hpp"
namespace nostalgia::core {
class Context {
public:
turbine::Context &turbineCtx;
glutils::GLProgram bgShader;
glutils::GLProgram spriteShader;
ox::Array<renderer::CBB, 4> cbbs;
renderer::SpriteBlockset spriteBlocks;
ox::Array<Sprite, 128> spriteStates;
ox::Array<renderer::Background, 4> backgrounds;
renderer::Drawer drawer;
uint_t spriteCount = 0;
uint_t blocksPerSprite = 0;
explicit Context(turbine::Context &tctx, InitParams const&params) noexcept;
Context(Context const&) = delete;
Context(Context&&) = delete;
Context &operator=(Context const&) = delete;
Context &operator=(Context&&) = delete;
~Context() noexcept;
};
}

View File

@ -0,0 +1,694 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/array.hpp>
#include <ox/std/fmt.hpp>
#include <ox/std/vec.hpp>
#include <keel/media.hpp>
#include <glutils/glutils.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/tilesheet.hpp>
#include "context.hpp"
#include "gfx.hpp"
namespace nostalgia::core {
namespace renderer {
constexpr auto Scale = 1;
constexpr auto PriorityScale = 0.01f;
Drawer::Drawer(Context &ctx) noexcept: m_ctx(ctx) {}
void Drawer::draw(turbine::Context &tctx) noexcept {
core::gl::draw(m_ctx, turbine::getScreenSize(tctx));
}
constexpr ox::CStringView bgvshadTmpl = R"glsl(
{}
in vec2 vTexCoord;
in vec3 vPosition;
in float vTileIdx;
out vec2 fTexCoord;
uniform float vXScale;
uniform float vTileHeight;
uniform float vBgIdx;
void main() {
float xScaleInvert = 1.0 - vXScale;
gl_Position = vec4(
vPosition.x * vXScale - xScaleInvert,
vPosition.y,
vPosition.z - 0.001 * vBgIdx,
1.0);
fTexCoord = vec2(
vTexCoord.x,
vTexCoord.y * vTileHeight + vTileIdx * vTileHeight);
})glsl";
constexpr ox::CStringView bgfshadTmpl = R"glsl(
{}
out vec4 outColor;
in vec2 fTexCoord;
uniform sampler2D image;
uniform vec2 fSrcImgSz;
uniform vec4 fPalette[256];
void main() {
outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256)];
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
if (outColor.a == 0) {
discard;
}
})glsl";
constexpr ox::CStringView spritevshadTmpl = R"glsl(
{}
in float vEnabled;
in vec3 vPosition;
in vec2 vTexCoord;
out vec2 fTexCoord;
uniform float vXScale;
uniform float vTileHeight;
void main() {
float xScaleInvert = 1.0 - vXScale;
gl_Position = vec4(
vPosition.x * vXScale - xScaleInvert,
vPosition.y,
// offset to ensure sprites draw on top of BGs by default
vPosition.z - 0.004,
1.0) * vEnabled;
fTexCoord = vTexCoord * vec2(1, vTileHeight);
})glsl";
constexpr ox::CStringView spritefshadTmpl = R"glsl(
{}
out vec4 outColor;
in vec2 fTexCoord;
uniform sampler2D image;
uniform vec2 fSrcImgSz;
uniform vec4 fPalette[256];
void main() {
outColor = fPalette[int(texture(image, fTexCoord).rgb.r * 256)];
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
if (outColor.a == 0) {
discard;
}
})glsl";
[[nodiscard]]
static constexpr auto bgVertexRow(uint_t x, uint_t y) noexcept {
return y * TileRows + x;
}
static void setSpriteBufferObject(
uint_t vi,
float enabled,
float x,
float y,
uint_t textureRow,
uint_t flipX,
uint_t priority,
float *vbo,
GLuint *ebo) noexcept {
// don't worry, this memcpy gets optimized to something much more ideal
constexpr float xmod = 0.1f;
constexpr float ymod = 0.1f;
x *= xmod;
y *= -ymod;
x -= 1.f;
y += 1.f - ymod;
auto const prif = static_cast<float>(priority) * PriorityScale;
auto const textureRowf = static_cast<float>(textureRow);
float const L = flipX ? 1 : 0;
float const R = flipX ? 0 : 1;
ox::Array<float, SpriteVertexVboLength> const vertices {
// vEnabled| vPosition | vTexCoord
enabled, x, y, prif, L, textureRowf + 1, // bottom left
enabled, x + xmod, y, prif, R, textureRowf + 1, // bottom right
enabled, x + xmod, y + ymod, prif, R, textureRowf + 0, // top right
enabled, x, y + ymod, prif, L, textureRowf + 0, // top left
};
memcpy(vbo, vertices.data(), sizeof(vertices));
ox::Array<GLuint, SpriteVertexEboLength> const elms {
vi + 0, vi + 1, vi + 2,
vi + 2, vi + 3, vi + 0,
};
memcpy(ebo, elms.data(), sizeof(elms));
}
static void setTileBufferObject(
uint_t vi,
float x,
float y,
float textureTileIdx,
float priority,
float *vbo,
GLuint *ebo) noexcept {
// don't worry, this memcpy gets optimized to something much more ideal
constexpr float ymod = 0.1f;
constexpr float xmod = 0.1f;
x *= xmod;
y *= -ymod;
x -= 1.0f;
y += 1.0f - ymod;
auto const prif = priority * PriorityScale;
ox::Array<float, BgVertexVboLength> const vertices {
x, y, prif, 0, 1, textureTileIdx, // bottom left
x + xmod, y, prif, 1, 1, textureTileIdx, // bottom right
x + xmod, y + ymod, prif, 1, 0, textureTileIdx, // top right
x, y + ymod, prif, 0, 0, textureTileIdx, // top left
};
memcpy(vbo, vertices.data(), sizeof(vertices));
ox::Array<GLuint, BgVertexEboLength> const elms {
vi + 0, vi + 1, vi + 2,
vi + 2, vi + 3, vi + 0,
};
memcpy(ebo, elms.data(), sizeof(elms));
}
static void initSpriteBufferObjects(Context &ctx, glutils::BufferSet &bs) noexcept {
for (auto i = 0u; i < ctx.spriteCount; ++i) {
auto vbo = &bs.vertices[i * static_cast<std::size_t>(SpriteVertexVboLength)];
auto ebo = &bs.elements[i * static_cast<std::size_t>(SpriteVertexEboLength)];
setSpriteBufferObject(i * static_cast<uint_t>(SpriteVertexVboRows) * ctx.blocksPerSprite, 0, 0, 0, 0, false, 0, vbo, ebo);
}
}
static void initBackgroundBufferObjects(glutils::BufferSet &bs) noexcept {
for (auto x = 0u; x < TileColumns; ++x) {
for (auto y = 0u; y < TileRows; ++y) {
const auto i = bgVertexRow(x, y);
auto vbo = &bs.vertices[i * static_cast<std::size_t>(BgVertexVboLength)];
auto ebo = &bs.elements[i * static_cast<std::size_t>(BgVertexEboLength)];
setTileBufferObject(
static_cast<uint_t>(i * BgVertexVboRows),
static_cast<float>(x),
static_cast<float>(y),
0,
0,
vbo,
ebo);
}
}
}
static void initSpritesBufferset(Context &ctx) noexcept {
auto const shader = ctx.spriteShader.id;
auto &bs = ctx.spriteBlocks;
// vao
bs.vao = glutils::generateVertexArrayObject();
glBindVertexArray(bs.vao);
// vbo & ebo
bs.vbo = glutils::generateBuffer();
bs.ebo = glutils::generateBuffer();
initSpriteBufferObjects(ctx, bs);
glutils::sendVbo(bs);
glutils::sendEbo(bs);
// vbo layout
// in float vEnabled;
auto const enabledAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vEnabled"));
glEnableVertexAttribArray(enabledAttr);
glVertexAttribPointer(enabledAttr, 1, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float), nullptr);
// in vec3 vPosition;
auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
glEnableVertexAttribArray(posAttr);
glVertexAttribPointer(posAttr, 3, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{1 * sizeof(float)}));
// in vec2 vTexCoord;
auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
glEnableVertexAttribArray(texCoordAttr);
glVertexAttribPointer(texCoordAttr, 2, GL_FLOAT, GL_FALSE, SpriteVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{4 * sizeof(float)}));
}
static void initBackgroundBufferset(
GLuint shader,
glutils::BufferSet &bs) noexcept {
// vao
bs.vao = glutils::generateVertexArrayObject();
glBindVertexArray(bs.vao);
// vbo & ebo
bs.vbo = glutils::generateBuffer();
bs.ebo = glutils::generateBuffer();
initBackgroundBufferObjects(bs);
glutils::sendVbo(bs);
glutils::sendEbo(bs);
// vbo layout
auto const posAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vPosition"));
glEnableVertexAttribArray(posAttr);
glVertexAttribPointer(posAttr, 3, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float), nullptr);
auto const texCoordAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTexCoord"));
glEnableVertexAttribArray(texCoordAttr);
glVertexAttribPointer(
texCoordAttr, 2, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{3 * sizeof(float)}));
auto const heightMultAttr = static_cast<GLuint>(glGetAttribLocation(shader, "vTileIdx"));
glEnableVertexAttribArray(heightMultAttr);
glVertexAttribPointer(
heightMultAttr, 1, GL_FLOAT, GL_FALSE, BgVertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{5 * sizeof(float)}));
}
static glutils::GLTexture createTexture(
GLsizei w,
GLsizei h,
const void *pixels) noexcept {
GLuint texId = 0;
glGenTextures(1, &texId);
glutils::GLTexture tex(texId);
tex.width = w;
tex.height = h;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex.id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tex.width, tex.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
return tex;
}
static void drawBackground(CBB &cbb) noexcept {
glBindVertexArray(cbb.vao);
if (cbb.updated) {
cbb.updated = false;
glutils::sendVbo(cbb);
}
glBindTexture(GL_TEXTURE_2D, cbb.tex);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(cbb.elements.size()), GL_UNSIGNED_INT, nullptr);
}
static void drawBackgrounds(
Context &ctx,
ox::Size const&renderSz) noexcept {
// load background shader and its uniforms
glUseProgram(ctx.bgShader);
const auto uniformSrcImgSz = glGetUniformLocation(ctx.bgShader, "fSrcImgSz");
const auto uniformXScale = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vXScale"));
const auto uniformTileHeight = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vTileHeight"));
const auto uniformBgIdx = static_cast<GLint>(glGetUniformLocation(ctx.bgShader, "vBgIdx"));
const auto [wi, hi] = renderSz;
const auto wf = static_cast<float>(wi);
const auto hf = static_cast<float>(hi);
glUniform1f(uniformXScale, hf / wf);
auto bgIdx = 0.f;
for (const auto &bg : ctx.backgrounds) {
if (bg.enabled) {
auto &cbb = ctx.cbbs[bg.cbbIdx];
const auto tileRows = cbb.tex.height / (TileHeight * Scale);
glUniform1f(uniformTileHeight, 1.0f / static_cast<float>(tileRows));
glUniform2f(
uniformSrcImgSz,
static_cast<float>(cbb.tex.width),
static_cast<float>(cbb.tex.height));
glUniform1f(uniformBgIdx, bgIdx);
drawBackground(cbb);
++bgIdx;
}
}
}
static void drawSprites(Context &ctx, ox::Size const&renderSz) noexcept {
glUseProgram(ctx.spriteShader);
auto &sb = ctx.spriteBlocks;
const auto uniformXScale = glGetUniformLocation(ctx.bgShader, "vXScale");
const auto uniformTileHeight = glGetUniformLocation(ctx.spriteShader, "vTileHeight");
const auto [wi, hi] = renderSz;
const auto wf = static_cast<float>(wi);
const auto hf = static_cast<float>(hi);
glUniform1f(uniformXScale, hf / wf);
// update vbo
glBindVertexArray(sb.vao);
if (sb.updated) {
sb.updated = false;
glutils::sendVbo(sb);
}
// set vTileHeight uniform
const auto tileRows = sb.tex.height / (TileHeight * Scale);
glUniform1f(uniformTileHeight, 1.0f / static_cast<float>(tileRows));
// draw
glBindTexture(GL_TEXTURE_2D, sb.tex);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(sb.elements.size()), GL_UNSIGNED_INT, nullptr);
}
static void loadPalette(
GLuint shaderPgrm,
Palette const&pal) noexcept {
static constexpr std::size_t ColorCnt = 256;
ox::Array<GLfloat, ColorCnt * 4> palette{};
for (auto i = 0u; const auto c : pal.colors) {
palette[i++] = redf(c);
palette[i++] = greenf(c);
palette[i++] = bluef(c);
palette[i++] = 255;
}
// make first color transparent
palette[3] = 0;
glUseProgram(shaderPgrm);
const auto uniformPalette = static_cast<GLint>(glGetUniformLocation(shaderPgrm, "fPalette"));
glUniform4fv(uniformPalette, ColorCnt, palette.data());
}
static void setSprite(
Context &ctx,
uint_t const idx,
Sprite const&s) noexcept {
// Tonc Table 8.4
static constexpr ox::Array<ox::Vec<uint_t>, 12> dimensions{
// col 0
{1, 1}, // 0, 0
{2, 2}, // 0, 1
{4, 4}, // 0, 2
{8, 8}, // 0, 3
// col 1
{2, 1}, // 1, 0
{4, 1}, // 1, 1
{4, 2}, // 1, 2
{8, 4}, // 1, 3
// col 2
{1, 1}, // 2, 0
{1, 4}, // 2, 1
{2, 4}, // 2, 2
{4, 8}, // 2, 3
};
oxAssert(idx < ctx.spriteStates.size(), "overflow");
auto const dim = dimensions[(s.spriteShape << 2) | s.spriteSize];
auto const uX = static_cast<int>(s.x) % 255;
auto const uY = static_cast<int>(s.y + 8) % 255 - 8;
oxAssert(1 < ctx.spriteBlocks.vertices.size(), "vbo overflow");
oxAssert(1 < ctx.spriteBlocks.elements.size(), "ebo overflow");
const auto spriteVboSz = ctx.blocksPerSprite * renderer::SpriteVertexVboLength;
const auto spriteEboSz = ctx.blocksPerSprite * renderer::SpriteVertexEboLength;
auto const vboBase = spriteVboSz * idx;
auto const eboBase = spriteEboSz * idx;
auto i = 0u;
const auto set = [&](int xIt, int yIt, bool enabled) {
auto const fX = static_cast<float>(uX + xIt * 8) / 8;
auto const fY = static_cast<float>(uY + yIt * 8) / 8;
auto const vboIdx = vboBase + renderer::SpriteVertexVboLength * i;
auto const eboIdx = eboBase + renderer::SpriteVertexEboLength * i;
oxAssert(vboIdx < ctx.spriteBlocks.vertices.size(), "vbo overflow");
oxAssert(eboIdx < ctx.spriteBlocks.elements.size(), "ebo overflow");
auto const vbo = &ctx.spriteBlocks.vertices[vboIdx];
auto const ebo = &ctx.spriteBlocks.elements[eboIdx];
renderer::setSpriteBufferObject(
static_cast<uint_t>(vboIdx),
enabled,
fX,
fY,
s.tileIdx + i,
s.flipX,
s.priority & 0b11,
vbo,
ebo);
++i;
};
if (!s.flipX) {
for (auto yIt = 0; yIt < static_cast<int>(dim.y); ++yIt) {
for (auto xIt = 0u; xIt < dim.x; ++xIt) {
set(static_cast<int>(xIt), static_cast<int>(yIt), s.enabled);
}
}
} else {
for (auto yIt = 0u; yIt < dim.y; ++yIt) {
for (auto xIt = dim.x - 1; xIt < ~0u; --xIt) {
set(static_cast<int>(xIt), static_cast<int>(yIt), s.enabled);
}
}
}
// clear remaining blocks in the sprite
for (; i < ctx.blocksPerSprite; ++i) {
set(0, 0, false);
}
ctx.spriteBlocks.updated = true;
}
}
ox::Error initGfx(
Context &ctx,
InitParams const&initParams) noexcept {
const auto bgVshad = ox::sfmt(renderer::bgvshadTmpl, gl::GlslVersion);
const auto bgFshad = ox::sfmt(renderer::bgfshadTmpl, gl::GlslVersion);
const auto spriteVshad = ox::sfmt(renderer::spritevshadTmpl, gl::GlslVersion);
const auto spriteFshad = ox::sfmt(renderer::spritefshadTmpl, gl::GlslVersion);
oxReturnError(glutils::buildShaderProgram(bgVshad, bgFshad).moveTo(ctx.bgShader));
oxReturnError(
glutils::buildShaderProgram(spriteVshad, spriteFshad).moveTo(ctx.spriteShader));
for (auto &cbb : ctx.cbbs) {
initBackgroundBufferset(ctx.bgShader, cbb);
}
renderer::initSpritesBufferset(ctx);
if (initParams.glInstallDrawer) {
turbine::gl::addDrawer(ctx.turbineCtx, &ctx.drawer);
}
return {};
}
void shutdownGfx(Context &ctx) noexcept {
turbine::gl::removeDrawer(ctx.turbineCtx, &ctx.drawer);
}
struct TileSheetData {
ox::Vector<uint32_t> pixels;
int width = 0;
int height = 0;
[[nodiscard]]
constexpr ox::Size size() const noexcept {
return {width, height};
}
};
static ox::Result<TileSheetData> normalizeTileSheet(
CompactTileSheet const&tilesheet) noexcept {
const uint_t bytesPerTile = tilesheet.bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
const auto tiles = tilesheet.pixels.size() / bytesPerTile;
constexpr int width = 8;
const int height = 8 * static_cast<int>(tiles);
ox::Vector<uint32_t> pixels;
if (bytesPerTile == 64) { // 8 BPP
pixels.resize(tilesheet.pixels.size());
for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) {
pixels[i] = tilesheet.pixels[i];
}
} else { // 4 BPP
pixels.resize(tilesheet.pixels.size() * 2);
for (std::size_t i = 0; i < tilesheet.pixels.size(); ++i) {
pixels[i * 2 + 0] = tilesheet.pixels[i] & 0xF;
pixels[i * 2 + 1] = tilesheet.pixels[i] >> 4;
}
}
return TileSheetData{std::move(pixels), width, height};
}
ox::Error loadBgPalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept {
auto &kctx = keelCtx(ctx.turbineCtx);
oxRequire(palette, readObj<Palette>(kctx, paletteAddr));
renderer::loadPalette(ctx.bgShader, *palette);
return {};
}
ox::Error loadSpritePalette(
Context &ctx,
ox::FileAddress const&paletteAddr) noexcept {
auto &kctx = keelCtx(ctx.turbineCtx);
oxRequire(palette, readObj<Palette>(kctx, paletteAddr));
renderer::loadPalette(ctx.spriteShader, *palette);
return {};
}
static ox::Result<TileSheetData> buildSetTsd(
Context &ctx,
TileSheetSet const&set) noexcept {
auto &kctx = keelCtx(ctx.turbineCtx);
TileSheetData setTsd;
setTsd.width = TileWidth;
for (auto const&entry : set.entries) {
oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, entry.tilesheet));
oxRequire(tsd, normalizeTileSheet(*tilesheet));
for (auto const&s : entry.sections) {
auto const size = s.tiles * PixelsPerTile;
for (auto i = 0; i < size; ++i) {
auto const srcIdx = static_cast<size_t>(i) + static_cast<size_t>(s.begin * PixelsPerTile);
setTsd.pixels.push_back(tsd.pixels[srcIdx]);
}
setTsd.height += TileHeight * s.tiles;
}
}
return setTsd;
}
ox::Error loadBgTileSheet(
Context &ctx,
uint_t cbb,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette) noexcept {
auto &kctx = keelCtx(ctx.turbineCtx);
oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, tilesheetAddr));
oxRequire(tsd, normalizeTileSheet(*tilesheet));
oxTracef("nostalgia.core.gfx.gl", "loadBgTexture: { cbbIdx: {}, w: {}, h: {} }", cbb, tsd.width, tsd.height);
ctx.cbbs[cbb].tex = renderer::createTexture(tsd.width, tsd.height, tsd.pixels.data());
if (loadDefaultPalette) {
oxReturnError(loadBgPalette(ctx, tilesheet->defaultPalette));
}
return {};
}
ox::Error loadBgTileSheet(
Context &ctx,
unsigned cbb,
TileSheetSet const&set) noexcept {
oxRequire(setTsd, buildSetTsd(ctx, set));
ctx.cbbs[cbb].tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
return {};
}
ox::Error loadSpriteTileSheet(
Context &ctx,
ox::FileAddress const&tilesheetAddr,
bool loadDefaultPalette) noexcept {
auto &kctx = keelCtx(ctx.turbineCtx);
oxRequire(tilesheet, readObj<CompactTileSheet>(kctx, tilesheetAddr));
oxRequire(tsd, normalizeTileSheet(*tilesheet));
oxTracef("nostalgia.core.gfx.gl", "loadSpriteTexture: { w: {}, h: {} }", tsd.width, tsd.height);
ctx.spriteBlocks.tex = renderer::createTexture(tsd.width, tsd.height, tsd.pixels.data());
if (loadDefaultPalette) {
oxReturnError(loadSpritePalette(ctx, tilesheet->defaultPalette));
}
return {};
}
ox::Error loadSpriteTileSheet(
Context &ctx,
TileSheetSet const&set) noexcept {
oxRequire(setTsd, buildSetTsd(ctx, set));
ctx.spriteBlocks.tex = renderer::createTexture(setTsd.width, setTsd.height, setTsd.pixels.data());
return {};
}
void setBgTile(
Context &ctx,
uint_t bgIdx,
int column,
int row,
uint8_t tile) noexcept {
oxTracef(
"nostalgia.core.gfx.setBgTile",
"bgIdx: {}, column: {}, row: {}, tile: {}",
bgIdx, column, row, tile);
const auto z = static_cast<uint_t>(bgIdx);
const auto y = static_cast<uint_t>(row);
const auto x = static_cast<uint_t>(column);
const auto i = renderer::bgVertexRow(x, y);
auto &cbb = ctx.cbbs[z];
const auto vbo = &cbb.vertices[i * renderer::BgVertexVboLength];
const auto ebo = &cbb.elements[i * renderer::BgVertexEboLength];
auto &bg = ctx.backgrounds[bgIdx];
renderer::setTileBufferObject(
static_cast<uint_t>(i * renderer::BgVertexVboRows),
static_cast<float>(x),
static_cast<float>(y),
static_cast<float>(tile),
bg.priority,
vbo,
ebo);
cbb.updated = true;
}
void clearBg(Context &ctx, uint_t bgIdx) noexcept {
auto &cbb = ctx.cbbs[static_cast<std::size_t>(bgIdx)];
initBackgroundBufferObjects(cbb);
cbb.updated = true;
auto &bg = ctx.backgrounds[static_cast<std::size_t>(bgIdx)];
bg.priority = 0;
}
uint8_t bgStatus(Context &ctx) noexcept {
uint8_t out = 0;
for (uint_t i = 0; i < ctx.cbbs.size(); ++i) {
out |= static_cast<uint8_t>(static_cast<uint_t>(ctx.backgrounds[i].enabled) << i);
}
return out;
}
void setBgStatus(Context &ctx, uint32_t status) noexcept {
for (uint_t i = 0; i < ctx.cbbs.size(); ++i) {
ctx.backgrounds[i].enabled = (status >> i) & 1;
}
}
bool bgStatus(Context &ctx, uint_t bg) noexcept {
return ctx.backgrounds[bg].enabled;
}
void setBgStatus(Context &ctx, uint_t bg, bool status) noexcept {
ctx.backgrounds[bg].enabled = status;
}
void setBgCbb(Context &ctx, uint_t bgIdx, uint_t cbbIdx) noexcept {
auto &bg = ctx.backgrounds[bgIdx];
bg.cbbIdx = cbbIdx;
}
void setBgPriority(Context &ctx, uint_t bgIdx, uint_t priority) noexcept {
auto &bg = ctx.backgrounds[bgIdx];
bg.priority = static_cast<float>(priority & 0b11);
}
void hideSprite(Context &ctx, uint_t idx) noexcept {
auto &s = ctx.spriteStates[idx];
s.enabled = false;
renderer::setSprite(ctx, idx, s);
}
void showSprite(Context &ctx, uint_t idx) noexcept {
auto &s = ctx.spriteStates[idx];
s.enabled = true;
renderer::setSprite(ctx, idx, s);
}
void setSprite(Context &ctx, uint_t idx, Sprite const&sprite) noexcept {
auto &s = ctx.spriteStates[idx];
s = sprite;
renderer::setSprite(ctx, idx, s);
}
uint_t spriteCount(Context &ctx) noexcept {
return ctx.spriteCount;
}
namespace gl {
ox::Size drawSize(int scale) noexcept {
return {240 * scale, 160 * scale};
}
void draw(core::Context &ctx, ox::Size const&renderSz) noexcept {
glViewport(0, 0, renderSz.width, renderSz.height);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
renderer::drawBackgrounds(ctx, renderSz);
if (ctx.spriteBlocks.tex) {
renderer::drawSprites(ctx, renderSz);
}
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
}
void draw(core::Context &ctx, int scale) noexcept {
draw(ctx, drawSize(scale));
}
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
#include <turbine/gfx.hpp>
#include <glutils/glutils.hpp>
#include <nostalgia/core/context.hpp>
namespace nostalgia::core::renderer {
constexpr uint64_t TileRows = 128;
constexpr uint64_t TileColumns = 128;
constexpr uint64_t TileCount = TileRows * TileColumns;
constexpr uint64_t BgVertexVboRows = 4;
constexpr uint64_t BgVertexVboRowLength = 6;
constexpr uint64_t BgVertexVboLength = BgVertexVboRows * BgVertexVboRowLength;
constexpr uint64_t BgVertexEboLength = 6;
constexpr uint64_t SpriteVertexVboRows = 4;
constexpr uint64_t SpriteVertexVboRowLength = 6;
constexpr uint64_t SpriteVertexVboLength = SpriteVertexVboRows * SpriteVertexVboRowLength;
constexpr uint64_t SpriteVertexEboLength = 6;
struct CBB: public glutils::BufferSet {
bool updated = false;
constexpr CBB() noexcept {
vertices.resize(TileCount * BgVertexVboLength);
elements.resize(TileCount * BgVertexEboLength);
}
};
struct SpriteBlockset: public glutils::BufferSet {
bool updated = false;
constexpr SpriteBlockset(uint64_t spriteCount, uint64_t blocksPerSprite) noexcept {
vertices.resize(spriteCount * SpriteVertexVboLength * blocksPerSprite);
elements.resize(spriteCount * SpriteVertexEboLength * blocksPerSprite);
}
};
struct Background {
float priority = 0;
bool enabled = false;
unsigned cbbIdx = 0;
};
class Drawer: public turbine::gl::Drawer {
private:
Context &m_ctx;
public:
explicit Drawer(Context &ctx) noexcept;
void draw(turbine::Context&) noexcept final;
};
}
namespace nostalgia::core {
ox::Error initGfx(Context &ctx, InitParams const&) noexcept;
void shutdownGfx(Context &ctx) noexcept;
}

View File

@ -0,0 +1,30 @@
add_library(NostalgiaCore-Studio)
add_library(
NostalgiaCore-Studio-ImGui
studiomodule.cpp
tilesheeteditor/tilesheeteditor-imgui.cpp
)
target_link_libraries(
NostalgiaCore-Studio PUBLIC
NostalgiaCore
Studio
)
target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
NostalgiaCore-Studio
Studio
)
install(
TARGETS
NostalgiaCore-Studio-ImGui
NostalgiaCore-Studio
LIBRARY DESTINATION
${NOSTALGIA_DIST_MODULE}
)
add_subdirectory(paletteeditor)
add_subdirectory(tilesheeteditor)

View File

@ -0,0 +1,5 @@
target_sources(
NostalgiaCore-Studio PRIVATE
paletteeditor.cpp
paletteeditor-imgui.cpp
)

View File

@ -0,0 +1,137 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <ox/std/memory.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/gfx.hpp>
#include "paletteeditor.hpp"
#include "paletteeditor-imgui.hpp"
namespace nostalgia::core {
PaletteEditorImGui::PaletteEditorImGui(turbine::Context &ctx, ox::CRStringView path):
Editor(path),
m_ctx(ctx),
m_pal(*keel::readObj<Palette>(keelCtx(m_ctx), ox::FileAddress(itemPath())).unwrapThrow()) {
}
void PaletteEditorImGui::draw(turbine::Context&) noexcept {
static constexpr auto flags = ImGuiTableFlags_RowBg;
const auto paneSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("Colors", ImVec2(paneSize.x - 208, paneSize.y), true);
{
const auto colorsSz = ImGui::GetContentRegionAvail();
static constexpr auto toolbarHeight = 40;
{
const auto sz = ImVec2(70, 24);
if (ImGui::Button("Add", sz)) {
const auto colorSz = static_cast<int>(m_pal.colors.size());
constexpr Color16 c = 0;
undoStack()->push(ox::make_unique<AddColorCommand>(&m_pal, c, colorSz));
}
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedRow >= m_pal.colors.size());
{
if (ImGui::Button("Remove", sz)) {
undoStack()->push(
ox::make_unique<RemoveColorCommand>(
&m_pal,
m_pal.colors[static_cast<std::size_t>(m_selectedRow)],
static_cast<int>(m_selectedRow)));
m_selectedRow = ox::min(m_pal.colors.size() - 1, m_selectedRow);
}
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedRow <= 0);
{
if (ImGui::Button("Move Up", sz)) {
undoStack()->push(ox::make_unique<MoveColorCommand>(&m_pal, m_selectedRow, -1));
--m_selectedRow;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(m_selectedRow >= m_pal.colors.size() - 1);
{
if (ImGui::Button("Move Down", sz)) {
undoStack()->push(ox::make_unique<MoveColorCommand>(&m_pal, m_selectedRow, 1));
++m_selectedRow;
}
}
ImGui::EndDisabled();
}
ImGui::EndDisabled();
}
ImGui::BeginTable("Colors", 5, flags, ImVec2(colorsSz.x, colorsSz.y - (toolbarHeight + 5)));
{
ImGui::TableSetupColumn("Idx", ImGuiTableColumnFlags_WidthFixed, 25);
ImGui::TableSetupColumn("Red", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Green", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Blue", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Color Preview", ImGuiTableColumnFlags_NoHide);
ImGui::TableHeadersRow();
constexpr auto colVal = [] (unsigned num) {
ox::Array<char, 4> numStr;
ImGui::TableNextColumn();
ox_itoa(num, numStr.data());
ImGui::SetCursorPosX(
ImGui::GetCursorPosX() + ImGui::GetColumnWidth() - ImGui::CalcTextSize(numStr.data()).x);
ImGui::Text("%s", numStr.data());
};
for (auto i = 0u; const auto c : m_pal.colors) {
ImGui::PushID(static_cast<int>(i));
ImGui::TableNextRow();
// Color No.
colVal(i);
// Red
colVal(red16(c));
// Green
colVal(green16(c));
// Blue
colVal(blue16(c));
// ColorPreview
ImGui::TableNextColumn();
const auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
if (ImGui::Selectable("##ColorRow", i == m_selectedRow, ImGuiSelectableFlags_SpanAllColumns)) {
m_selectedRow = i;
}
ImGui::PopID();
++i;
}
}
ImGui::EndTable();
}
ImGui::EndChild();
if (m_selectedRow < m_pal.colors.size()) {
ImGui::SameLine();
ImGui::BeginChild("ColorEditor", ImVec2(200, paneSize.y), true);
{
const auto c = m_pal.colors[m_selectedRow];
int r = red16(c);
int g = green16(c);
int b = blue16(c);
int const a = alpha16(c);
ImGui::InputInt("Red", &r, 1, 5);
ImGui::InputInt("Green", &g, 1, 5);
ImGui::InputInt("Blue", &b, 1, 5);
const auto newColor = color16(r, g, b, a);
if (c != newColor) {
undoStack()->push(ox::make_unique<UpdateColorCommand>(&m_pal, static_cast<int>(m_selectedRow), c, newColor));
}
}
ImGui::EndChild();
}
}
ox::Error PaletteEditorImGui::saveItem() noexcept {
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
return sctx->project->writeObj(itemPath(), m_pal);
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/palette.hpp>
namespace nostalgia::core {
class PaletteEditorImGui: public studio::Editor {
private:
turbine::Context &m_ctx;
Palette m_pal;
std::size_t m_selectedRow = 0;
public:
PaletteEditorImGui(turbine::Context &ctx, ox::CRStringView path);
void draw(turbine::Context&) noexcept final;
protected:
ox::Error saveItem() noexcept final;
};
}

View File

@ -0,0 +1,105 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "paletteeditor.hpp"
namespace nostalgia::core {
AddColorCommand::AddColorCommand(Palette *pal, Color16 color, int idx) noexcept {
m_pal = pal;
m_color = color;
m_idx = idx;
}
int AddColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::AddColor);
}
void AddColorCommand::redo() noexcept {
m_pal->colors.insert(static_cast<std::size_t>(m_idx), m_color);
}
void AddColorCommand::undo() noexcept {
oxIgnoreError(m_pal->colors.erase(static_cast<std::size_t>(m_idx)));
}
RemoveColorCommand::RemoveColorCommand(Palette *pal, Color16 color, int idx) noexcept {
m_pal = pal;
m_color = color;
m_idx = idx;
}
int RemoveColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::RemoveColor);
}
void RemoveColorCommand::redo() noexcept {
oxIgnoreError(m_pal->colors.erase(static_cast<std::size_t>(m_idx)));
}
void RemoveColorCommand::undo() noexcept {
m_pal->colors.insert(static_cast<std::size_t>(m_idx), m_color);
}
UpdateColorCommand::UpdateColorCommand(Palette *pal, int idx, Color16 oldColor, Color16 newColor) noexcept {
m_pal = pal;
m_idx = idx;
m_oldColor = oldColor;
m_newColor = newColor;
//setObsolete(m_oldColor == m_newColor);
}
bool UpdateColorCommand::mergeWith(const UndoCommand *cmd) noexcept {
if (cmd->commandId() != static_cast<int>(PaletteEditorCommandId::UpdateColor)) {
return false;
}
auto ucCmd = static_cast<const UpdateColorCommand*>(cmd);
if (m_idx != ucCmd->m_idx) {
return false;
}
m_newColor = ucCmd->m_newColor;
return true;
}
[[nodiscard]]
int UpdateColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::UpdateColor);
}
void UpdateColorCommand::redo() noexcept {
m_pal->colors[static_cast<std::size_t>(m_idx)] = m_newColor;
}
void UpdateColorCommand::undo() noexcept {
m_pal->colors[static_cast<std::size_t>(m_idx)] = m_oldColor;
}
MoveColorCommand::MoveColorCommand(Palette *pal, std::size_t idx, int offset) noexcept {
m_pal = pal;
m_idx = idx;
m_offset = offset;
}
int MoveColorCommand::commandId() const noexcept {
return static_cast<int>(PaletteEditorCommandId::MoveColor);
}
void MoveColorCommand::redo() noexcept {
moveColor(static_cast<int>(m_idx), m_offset);
}
void MoveColorCommand::undo() noexcept {
moveColor(static_cast<int>(m_idx) + m_offset, -m_offset);
}
void MoveColorCommand::moveColor(int idx, int offset) noexcept {
const auto c = m_pal->colors[static_cast<std::size_t>(idx)];
oxIgnoreError(m_pal->colors.erase(static_cast<std::size_t>(idx)));
m_pal->colors.insert(static_cast<std::size_t>(idx + offset), c);
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <nostalgia/core/color.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/palette.hpp>
namespace nostalgia::core {
enum class PaletteEditorCommandId {
AddColor,
RemoveColor,
UpdateColor,
MoveColor,
};
class AddColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
Color16 m_color = 0;
int m_idx = -1;
public:
AddColorCommand(Palette *pal, Color16 color, int idx) noexcept;
~AddColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
void redo() noexcept override;
void undo() noexcept override;
};
class RemoveColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
Color16 m_color = 0;
int m_idx = -1;
public:
RemoveColorCommand(Palette *pal, Color16 color, int idx) noexcept;
~RemoveColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
void redo() noexcept override;
void undo() noexcept override;
};
class UpdateColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
Color16 m_oldColor = 0;
Color16 m_newColor = 0;
int m_idx = -1;
public:
UpdateColorCommand(Palette *pal, int idx, Color16 oldColor, Color16 newColor) noexcept;
~UpdateColorCommand() noexcept override = default;
[[nodiscard]]
bool mergeWith(const UndoCommand *cmd) noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
void redo() noexcept final;
void undo() noexcept final;
};
class MoveColorCommand: public studio::UndoCommand {
private:
Palette *m_pal = nullptr;
std::size_t m_idx = 0;
int m_offset = 0;
public:
MoveColorCommand(Palette *pal, std::size_t idx, int offset) noexcept;
~MoveColorCommand() noexcept override = default;
[[nodiscard]]
int commandId() const noexcept override;
public:
void redo() noexcept override;
void undo() noexcept override;
private:
void moveColor(int idx, int offset) noexcept;
};
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/memory.hpp>
#include <studio/studio.hpp>
#include "paletteeditor/paletteeditor-imgui.hpp"
#include "tilesheeteditor/tilesheeteditor-imgui.hpp"
namespace nostalgia::core {
class StudioModule: public studio::Module {
ox::Vector<studio::EditorMaker> editors(turbine::Context &ctx) const noexcept final {
return {
studio::editorMaker<TileSheetEditorImGui>(ctx, FileExt_ng),
studio::editorMaker<PaletteEditorImGui>(ctx, FileExt_npal),
};
}
ox::Vector<ox::UPtr<studio::ItemMaker>> itemMakers(turbine::Context&) const noexcept final {
ox::Vector<ox::UniquePtr<studio::ItemMaker>> out;
out.emplace_back(ox::make<studio::ItemMakerT<core::TileSheet>>("Tile Sheet", "TileSheets", FileExt_ng));
out.emplace_back(ox::make<studio::ItemMakerT<core::Palette>>("Palette", "Palettes", FileExt_npal));
return out;
}
};
static StudioModule mod;
const studio::Module *studioModule() noexcept {
return &mod;
}
}

View File

@ -0,0 +1,14 @@
target_sources(
NostalgiaCore-Studio PRIVATE
tilesheeteditorview.cpp
tilesheeteditormodel.cpp
tilesheetpixelgrid.cpp
tilesheetpixels.cpp
)
target_link_libraries(
NostalgiaCore-Studio-ImGui PUBLIC
lodepng
)
add_subdirectory(commands)

View File

@ -0,0 +1,11 @@
target_sources(
NostalgiaCore-Studio PRIVATE
addsubsheetcommand.cpp
cutpastecommand.cpp
deletetilescommand.cpp
drawcommand.cpp
inserttilescommand.cpp
palettechangecommand.cpp
rmsubsheetcommand.cpp
updatesubsheetcommand.cpp
)

View File

@ -0,0 +1,63 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "addsubsheetcommand.hpp"
namespace nostalgia::core {
AddSubSheetCommand::AddSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx parentIdx) noexcept:
m_img(img), m_parentIdx(std::move(parentIdx)) {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (!parent.subsheets.empty()) {
auto idx = m_parentIdx;
idx.emplace_back(parent.subsheets.size());
m_addedSheets.push_back(idx);
} else {
auto idx = m_parentIdx;
idx.emplace_back(0u);
m_addedSheets.push_back(idx);
*idx.back().value = 1;
m_addedSheets.push_back(idx);
}
}
void AddSubSheetCommand::redo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (m_addedSheets.size() < 2) {
auto i = parent.subsheets.size();
parent.subsheets.emplace_back(m_img.idIt++, ox::sfmt("Subsheet {}", i), 1, 1, m_img.bpp);
} else {
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 0", parent.columns, parent.rows, std::move(parent.pixels));
parent.rows = 0;
parent.columns = 0;
parent.subsheets.emplace_back(m_img.idIt++, "Subsheet 1", 1, 1, m_img.bpp);
}
}
void AddSubSheetCommand::undo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
if (parent.subsheets.size() == 2) {
auto s = parent.subsheets[0];
parent.rows = s.rows;
parent.columns = s.columns;
parent.pixels = std::move(s.pixels);
parent.subsheets.clear();
} else {
for (auto idx = m_addedSheets.rbegin(); idx != m_addedSheets.rend(); ++idx) {
oxLogError(m_img.rmSubSheet(*idx));
}
}
}
int AddSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::AddSubSheet);
}
TileSheet::SubSheetIdx const&AddSubSheetCommand::subsheetIdx() const noexcept {
return m_parentIdx;
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class AddSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_parentIdx;
ox::Vector<TileSheet::SubSheetIdx, 2> m_addedSheets;
public:
AddSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx parentIdx) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/undostack.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::core {
// Command IDs to use with QUndoCommand::id()
enum class CommandId {
Draw = 1,
AddSubSheet = 2,
RmSubSheet = 3,
DeleteTile = 4,
InsertTile = 4,
UpdateSubSheet = 5,
Cut = 6,
Paste = 7,
PaletteChange = 8,
};
constexpr bool operator==(CommandId c, int i) noexcept {
return static_cast<int>(c) == i;
}
constexpr bool operator==(int i, CommandId c) noexcept {
return static_cast<int>(c) == i;
}
class TileSheetCommand: public studio::UndoCommand {
public:
[[nodiscard]]
virtual TileSheet::SubSheetIdx const&subsheetIdx() const noexcept = 0;
};
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "cutpastecommand.hpp"
namespace nostalgia::core {
TileSheetClipboard::Pixel::Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept {
colorIdx = pColorIdx;
pt = pPt;
}
void TileSheetClipboard::addPixel(const ox::Point &pt, uint16_t colorIdx) noexcept {
m_pixels.emplace_back(colorIdx, pt);
}
const ox::Vector<TileSheetClipboard::Pixel> &TileSheetClipboard::pixels() const noexcept {
return m_pixels;
}
CutPasteCommand::CutPasteCommand(
CommandId commandId,
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Point &dstStart,
const ox::Point &dstEnd,
const TileSheetClipboard &cb) noexcept:
m_commandId(commandId),
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)) {
const auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &p : cb.pixels()) {
const auto dstPt = p.pt + dstStart;
if (dstPt.x <= dstEnd.x && dstPt.y <= dstEnd.y) {
const auto idx = subsheet.idx(dstPt);
m_changes.emplace_back(static_cast<uint32_t>(idx), p.colorIdx, subsheet.getPixel(m_img.bpp, idx));
}
}
}
void CutPasteCommand::redo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.newPalIdx));
}
}
void CutPasteCommand::undo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
}
int CutPasteCommand::commandId() const noexcept {
return static_cast<int>(m_commandId);
}
TileSheet::SubSheetIdx const&CutPasteCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
}

View File

@ -0,0 +1,84 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <turbine/clipboard.hpp>
#include "commands.hpp"
namespace nostalgia::core {
class TileSheetClipboard: public turbine::ClipboardObject<TileSheetClipboard> {
public:
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard";
static constexpr auto TypeVersion = 1;
oxModelFriend(TileSheetClipboard);
struct Pixel {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.core.studio.TileSheetClipboard.Pixel";
static constexpr auto TypeVersion = 1;
uint16_t colorIdx = 0;
ox::Point pt;
Pixel(uint16_t pColorIdx, ox::Point pPt) noexcept;
};
protected:
ox::Vector<Pixel> m_pixels;
public:
void addPixel(ox::Point const&pt, uint16_t colorIdx) noexcept;
[[nodiscard]]
ox::Vector<Pixel> const&pixels() const noexcept;
};
oxModelBegin(TileSheetClipboard::Pixel)
oxModelField(colorIdx)
oxModelField(pt)
oxModelEnd()
oxModelBegin(TileSheetClipboard)
oxModelFieldRename(pixels, m_pixels)
oxModelEnd()
class CutPasteCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t newPalIdx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pNewPalIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
newPalIdx = pNewPalIdx;
oldPalIdx = pOldPalIdx;
}
};
CommandId m_commandId;
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change> m_changes;
public:
CutPasteCommand(
CommandId commandId,
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
ox::Point const&dstStart,
ox::Point const&dstEnd,
TileSheetClipboard const&cb) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "deletetilescommand.hpp"
namespace nostalgia::core {
core::DeleteTilesCommand::DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_deletePos = tileIdx * bytesPerTile;
m_deleteSz = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + m_deletePos;
const auto sz = m_deleteSz * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void core::DeleteTilesCommand::redo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto srcPos = m_deletePos + m_deleteSz;
const auto src = p.data() + srcPos;
const auto dst1 = p.data() + m_deletePos;
const auto dst2 = p.data() + (p.size() - m_deleteSz);
ox_memmove(dst1, src, p.size() - srcPos);
ox_memset(dst2, 0, m_deleteSz * sizeof(decltype(p[0])));
}
void DeleteTilesCommand::undo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto src = p.data() + m_deletePos;
const auto dst1 = p.data() + m_deletePos + m_deleteSz;
const auto dst2 = src;
const auto sz = p.size() - m_deletePos - m_deleteSz;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
int DeleteTilesCommand::commandId() const noexcept {
return static_cast<int>(CommandId::DeleteTile);
}
TileSheet::SubSheetIdx const&DeleteTilesCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class DeleteTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_deletePos = 0;
std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
DeleteTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "drawcommand.hpp"
namespace nostalgia::core {
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx(palIdx) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
}
DrawCommand::DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Vector<std::size_t> &idxList,
int palIdx) noexcept:
m_img(img),
m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx(palIdx) {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto idx : idxList) {
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
}
}
bool DrawCommand::append(std::size_t idx) noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
if (m_changes.back().value->idx != idx && subsheet.getPixel(m_img.bpp, idx) != m_palIdx) {
// duplicate entries are bad
auto existing = ox::find_if(m_changes.cbegin(), m_changes.cend(), [idx](const auto &c) {
return c.idx == idx;
});
if (existing == m_changes.cend()) {
m_changes.emplace_back(static_cast<uint32_t>(idx), subsheet.getPixel(m_img.bpp, idx));
subsheet.setPixel(m_img.bpp, idx, static_cast<uint8_t>(m_palIdx));
return true;
}
}
return false;
}
bool DrawCommand::append(const ox::Vector<std::size_t> &idxList) noexcept {
auto out = false;
for (auto idx : idxList) {
out = append(idx) || out;
}
return out;
}
void DrawCommand::redo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(m_palIdx));
}
}
void DrawCommand::undo() noexcept {
auto &subsheet = m_img.getSubSheet(m_subSheetIdx);
for (const auto &c : m_changes) {
subsheet.setPixel(m_img.bpp, c.idx, static_cast<uint8_t>(c.oldPalIdx));
}
}
int DrawCommand::commandId() const noexcept {
return static_cast<int>(CommandId::Draw);
}
TileSheet::SubSheetIdx const&DrawCommand::subsheetIdx() const noexcept {
return m_subSheetIdx;
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class DrawCommand: public TileSheetCommand {
private:
struct Change {
uint32_t idx = 0;
uint16_t oldPalIdx = 0;
constexpr Change(uint32_t pIdx, uint16_t pOldPalIdx) noexcept {
idx = pIdx;
oldPalIdx = pOldPalIdx;
}
};
TileSheet &m_img;
TileSheet::SubSheetIdx m_subSheetIdx;
ox::Vector<Change, 8> m_changes;
int m_palIdx = 0;
public:
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
std::size_t idx,
int palIdx) noexcept;
DrawCommand(
TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx,
const ox::Vector<std::size_t> &idxList,
int palIdx) noexcept;
bool append(std::size_t idx) noexcept;
bool append(const ox::Vector<std::size_t> &idxList) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "inserttilescommand.hpp"
namespace nostalgia::core {
core::InsertTilesCommand::InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept:
m_img(img),
m_idx(std::move(idx)) {
const unsigned bytesPerTile = m_img.bpp == 4 ? PixelsPerTile / 2 : PixelsPerTile;
m_insertPos = tileIdx * bytesPerTile;
m_insertCnt = tileCnt * bytesPerTile;
m_deletedPixels.resize(m_insertCnt);
// copy pixels to be erased
{
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dst = m_deletedPixels.data();
auto src = p.data() + p.size() - m_insertCnt;
const auto sz = m_insertCnt * sizeof(decltype(p[0]));
ox_memcpy(dst, src, sz);
}
}
void InsertTilesCommand::redo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
auto dstPos = m_insertPos + m_insertCnt;
const auto dst = p.data() + dstPos;
const auto src = p.data() + m_insertPos;
ox_memmove(dst, src, p.size() - dstPos);
ox_memset(src, 0, m_insertCnt * sizeof(decltype(p[0])));
}
void InsertTilesCommand::undo() noexcept {
auto &s = m_img.getSubSheet(m_idx);
auto &p = s.pixels;
const auto srcIdx = m_insertPos + m_insertCnt;
const auto src = p.data() + srcIdx;
const auto dst1 = p.data() + m_insertPos;
const auto dst2 = p.data() + p.size() - m_insertCnt;
const auto sz = p.size() - srcIdx;
ox_memmove(dst1, src, sz);
ox_memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
}
int InsertTilesCommand::commandId() const noexcept {
return static_cast<int>(CommandId::InsertTile);
}
TileSheet::SubSheetIdx const&InsertTilesCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class InsertTilesCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
std::size_t m_insertPos = 0;
std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> m_deletedPixels = {};
public:
InsertTilesCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
std::size_t tileIdx,
std::size_t tileCnt) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "palettechangecommand.hpp"
namespace nostalgia::core {
core::PaletteChangeCommand::PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::CRStringView newPalette) noexcept:
m_img(img),
m_idx(std::move(idx)),
m_oldPalette(m_img.defaultPalette),
m_newPalette(ox::FileAddress(ox::sfmt<ox::BString<43>>("uuid://{}", newPalette))) {
}
void PaletteChangeCommand::redo() noexcept {
m_img.defaultPalette = m_newPalette;
}
void PaletteChangeCommand::undo() noexcept {
m_img.defaultPalette = m_oldPalette;
}
int PaletteChangeCommand::commandId() const noexcept {
return static_cast<int>(CommandId::PaletteChange);
}
TileSheet::SubSheetIdx const&PaletteChangeCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class PaletteChangeCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
ox::FileAddress m_oldPalette;
ox::FileAddress m_newPalette;
public:
PaletteChangeCommand(
TileSheet::SubSheetIdx idx,
TileSheet &img,
ox::CRStringView newPalette) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "rmsubsheetcommand.hpp"
namespace nostalgia::core {
core::RmSubSheetCommand::RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept:
m_img(img),
m_idx(std::move(idx)),
m_parentIdx(m_idx) {
m_parentIdx.pop_back();
auto &parent = m_img.getSubSheet(m_parentIdx);
m_sheet = parent.subsheets[*m_idx.back().value];
}
void RmSubSheetCommand::redo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
oxLogError(parent.subsheets.erase(*m_idx.back().value).error);
}
void RmSubSheetCommand::undo() noexcept {
auto &parent = m_img.getSubSheet(m_parentIdx);
auto i = *m_idx.back().value;
parent.subsheets.insert(i, m_sheet);
}
int RmSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::RmSubSheet);
}
TileSheet::SubSheetIdx const&RmSubSheetCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class RmSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheetIdx m_parentIdx;
TileSheet::SubSheet m_sheet;
public:
RmSubSheetCommand(TileSheet &img, TileSheet::SubSheetIdx idx) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "updatesubsheetcommand.hpp"
namespace nostalgia::core {
core::UpdateSubSheetCommand::UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::String name,
int cols,
int rows) noexcept:
m_img(img),
m_idx(std::move(idx)),
m_sheet(m_img.getSubSheet(m_idx)),
m_newName(std::move(name)),
m_newCols(cols),
m_newRows(rows) {
}
void UpdateSubSheetCommand::redo() noexcept {
auto &sheet = m_img.getSubSheet(m_idx);
sheet.name = m_newName;
sheet.columns = m_newCols;
sheet.rows = m_newRows;
oxLogError(sheet.setPixelCount(m_img.bpp, static_cast<std::size_t>(PixelsPerTile * m_newCols * m_newRows)));
}
void UpdateSubSheetCommand::undo() noexcept {
auto &sheet = m_img.getSubSheet(m_idx);
sheet = m_sheet;
}
int UpdateSubSheetCommand::commandId() const noexcept {
return static_cast<int>(CommandId::UpdateSubSheet);
}
TileSheet::SubSheetIdx const&UpdateSubSheetCommand::subsheetIdx() const noexcept {
return m_idx;
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include "commands.hpp"
namespace nostalgia::core {
class UpdateSubSheetCommand: public TileSheetCommand {
private:
TileSheet &m_img;
TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheet m_sheet;
ox::String m_newName;
int m_newCols = 0;
int m_newRows = 0;
public:
UpdateSubSheetCommand(
TileSheet &img,
TileSheet::SubSheetIdx idx,
ox::String name,
int cols,
int rows) noexcept;
void redo() noexcept final;
void undo() noexcept final;
[[nodiscard]]
int commandId() const noexcept final;
[[nodiscard]]
TileSheet::SubSheetIdx const&subsheetIdx() const noexcept override;
};
}

View File

@ -0,0 +1,512 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <lodepng.h>
#include <ox/std/point.hpp>
#include <keel/media.hpp>
#include "tilesheeteditor-imgui.hpp"
namespace nostalgia::core {
static ox::Vector<uint32_t> normalizePixelSizes(
ox::Vector<uint8_t> const&inPixels,
int const bpp) noexcept {
uint_t const bytesPerTile = bpp == 8 ? PixelsPerTile : PixelsPerTile / 2;
ox::Vector<uint32_t> outPixels;
if (bytesPerTile == 64) { // 8 BPP
outPixels.resize(inPixels.size());
for (std::size_t i = 0; i < inPixels.size(); ++i) {
outPixels[i] = inPixels[i];
}
} else { // 4 BPP
outPixels.resize(inPixels.size() * 2);
for (std::size_t i = 0; i < inPixels.size(); ++i) {
outPixels[i * 2 + 0] = inPixels[i] & 0xF;
outPixels[i * 2 + 1] = inPixels[i] >> 4;
}
}
return outPixels;
}
static ox::Vector<uint32_t> normalizePixelArrangement(
ox::Vector<uint32_t> const&inPixels,
int cols,
int scale) {
auto const scalePt = ox::Point{scale, scale};
auto const width = cols * TileWidth;
auto const height = static_cast<int>(inPixels.size()) / width;
auto const dstWidth = width * scale;
ox::Vector<uint32_t> outPixels(static_cast<size_t>((width * scale) * (height * scale)));
for (std::size_t dstIdx = 0; dstIdx < outPixels.size(); ++dstIdx) {
auto const dstPt = ox::Point{
static_cast<int>(dstIdx) % dstWidth,
static_cast<int>(dstIdx) / dstWidth};
auto const srcPt = dstPt / scalePt;
auto const srcIdx = ptToIdx(srcPt, cols);
outPixels[dstIdx] = inPixels[srcIdx];
}
return outPixels;
}
static ox::Error toPngFile(
ox::CStringView const&path,
ox::Vector<uint32_t> &&pixels,
Palette const&pal,
unsigned width,
unsigned height) noexcept {
for (auto &c : pixels) {
c = color32(pal.color(c)) | static_cast<Color32>(0XFF << 24);
}
constexpr auto fmt = LCT_RGBA;
return OxError(static_cast<ox::ErrorCode>(
lodepng_encode_file(
path.c_str(),
reinterpret_cast<uint8_t const*>(pixels.data()),
width,
height,
fmt,
8)));
}
TileSheetEditorImGui::TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringView path):
Editor(path),
m_ctx(ctx),
m_view(m_ctx, path, *undoStack()),
m_model(m_view.model()) {
oxIgnoreError(setPaletteSelection());
// connect signal/slots
undoStack()->changeTriggered.connect(this, &TileSheetEditorImGui::markUnsavedChanges);
m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubhseetToPng);
m_model.paletteChanged.connect(this, &TileSheetEditorImGui::setPaletteSelection);
}
void TileSheetEditorImGui::exportFile() {
m_exportMenu.show();
}
void TileSheetEditorImGui::cut() {
m_model.cut();
}
void TileSheetEditorImGui::copy() {
m_model.copy();
}
void TileSheetEditorImGui::paste() {
m_model.paste();
}
void TileSheetEditorImGui::keyStateChanged(turbine::Key key, bool down) {
if (!down) {
return;
}
if (key == turbine::Key::Escape) {
m_subsheetEditor.close();
m_exportMenu.close();
}
auto pal = m_model.pal();
if (pal) {
const auto colorCnt = pal->colors.size();
if (key == turbine::Key::Alpha_D) {
m_tool = Tool::Draw;
setCopyEnabled(false);
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key == turbine::Key::Alpha_S) {
m_tool = Tool::Select;
setCopyEnabled(true);
setCutEnabled(true);
setPasteEnabled(true);
} else if (key == turbine::Key::Alpha_F) {
m_tool = Tool::Fill;
setCopyEnabled(false);
setCutEnabled(false);
setPasteEnabled(false);
m_model.clearSelection();
} else if (key >= turbine::Key::Num_1 && key <= turbine::Key::Num_9 && key <= turbine::Key::Num_0 + colorCnt) {
auto idx = ox::min<std::size_t>(static_cast<uint32_t>(key - turbine::Key::Num_1), colorCnt - 1);
m_view.setPalIdx(idx);
} else if (key == turbine::Key::Num_0 && colorCnt >= 10) {
auto idx = ox::min<std::size_t>(static_cast<uint32_t>(key - turbine::Key::Num_1 + 9), colorCnt - 1);
m_view.setPalIdx(idx);
}
}
}
void TileSheetEditorImGui::draw(turbine::Context&) noexcept {
const auto paneSize = ImGui::GetContentRegionAvail();
const auto tileSheetParentSize = ImVec2(paneSize.x - m_palViewWidth, paneSize.y);
const auto fbSize = ox::Vec2(tileSheetParentSize.x - 16, tileSheetParentSize.y - 16);
ImGui::BeginChild("TileSheetView", tileSheetParentSize, true);
{
drawTileSheet(fbSize);
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("Controls", ImVec2(m_palViewWidth - 8, paneSize.y), true);
{
const auto controlsSize = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ToolBox", ImVec2(m_palViewWidth - 24, 30), true);
{
const auto btnSz = ImVec2(45, 14);
if (ImGui::Selectable("Select", m_tool == Tool::Select, 0, btnSz)) {
m_tool = Tool::Select;
}
ImGui::SameLine();
if (ImGui::Selectable("Draw", m_tool == Tool::Draw, 0, btnSz)) {
m_tool = Tool::Draw;
m_model.clearSelection();
}
ImGui::SameLine();
if (ImGui::Selectable("Fill", m_tool == Tool::Fill, 0, btnSz)) {
m_tool = Tool::Fill;
m_model.clearSelection();
}
}
ImGui::EndChild();
const auto ySize = controlsSize.y - 38;
// draw palette/color picker
ImGui::BeginChild("Palette", ImVec2(m_palViewWidth - 24, ySize / 2.f), true);
{
drawPaletteSelector();
}
ImGui::EndChild();
ImGui::BeginChild("SubSheets", ImVec2(m_palViewWidth - 24, ySize / 2.f), true);
{
static constexpr auto btnHeight = 18;
const auto btnSize = ImVec2(18, btnHeight);
if (ImGui::Button("+", btnSize)) {
auto insertOnIdx = m_model.activeSubSheetIdx();
const auto &parent = *m_model.activeSubSheet();
m_model.addSubsheet(insertOnIdx);
insertOnIdx.emplace_back(parent.subsheets.size() - 1);
m_model.setActiveSubsheet(insertOnIdx);
}
ImGui::SameLine();
if (ImGui::Button("-", btnSize)) {
const auto &activeSubsheetIdx = m_model.activeSubSheetIdx();
if (activeSubsheetIdx.size() > 0) {
m_model.rmSubsheet(activeSubsheetIdx);
}
}
ImGui::SameLine();
if (ImGui::Button("Edit", ImVec2(51, btnHeight))) {
showSubsheetEditor();
}
ImGui::SameLine();
if (ImGui::Button("Export", ImVec2(51, btnHeight))) {
m_exportMenu.show();
}
TileSheet::SubSheetIdx path;
static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
if (ImGui::BeginTable("Subsheets", 3, flags)) {
ImGui::TableSetupColumn("Subsheet", ImGuiTableColumnFlags_NoHide);
ImGui::TableSetupColumn("Columns", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Rows", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableHeadersRow();
drawSubsheetSelector(&m_view.img().subsheet, &path);
ImGui::EndTable();
}
}
ImGui::EndChild();
}
ImGui::EndChild();
m_subsheetEditor.draw();
m_exportMenu.draw();
}
void TileSheetEditorImGui::drawSubsheetSelector(TileSheet::SubSheet *subsheet, TileSheet::SubSheetIdx *path) {
ImGui::TableNextRow(0, 5);
using Str = ox::BasicString<100>;
auto pathStr = ox::join<Str>("##", *path).value;
auto lbl = ox::sfmt<Str>("{}##{}", subsheet->name, pathStr);
const auto rowSelected = *path == m_model.activeSubSheetIdx();
const auto flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_DefaultOpen
| (subsheet->subsheets.empty() ? ImGuiTreeNodeFlags_Leaf : 0)
| (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
ImGui::TableNextColumn();
const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
if (ImGui::IsItemClicked()) {
m_model.setActiveSubsheet(*path);
}
if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
showSubsheetEditor();
}
if (subsheet->subsheets.empty()) {
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet->columns);
ImGui::TableNextColumn();
ImGui::Text("%d", subsheet->rows);
} else {
ImGui::TableNextColumn();
ImGui::Text("--");
ImGui::TableNextColumn();
ImGui::Text("--");
}
if (open) {
for (auto i = 0ul; auto &child : subsheet->subsheets) {
path->push_back(i);
ImGui::PushID(static_cast<int>(i));
drawSubsheetSelector(&child, path);
ImGui::PopID();
path->pop_back();
++i;
}
ImGui::TreePop();
}
}
[[nodiscard]]
ox::Vec2 TileSheetEditorImGui::clickPos(ImVec2 const&winPos, ox::Vec2 clickPos) noexcept {
clickPos.x -= winPos.x + 10;
clickPos.y -= winPos.y + 10;
return clickPos;
}
ox::Error TileSheetEditorImGui::saveItem() noexcept {
return m_model.saveFile();
}
void TileSheetEditorImGui::showSubsheetEditor() noexcept {
const auto sheet = m_model.activeSubSheet();
if (!sheet->subsheets.empty()) {
m_subsheetEditor.show(sheet->name, -1, -1);
} else {
m_subsheetEditor.show(sheet->name, sheet->columns, sheet->rows);
}
}
ox::Error TileSheetEditorImGui::exportSubhseetToPng(int scale) noexcept {
oxRequire(path, studio::saveFile({{"PNG", "png"}}));
// subsheet to png
auto const&img = m_model.img();
auto const&s = *m_model.activeSubSheet();
auto const&pal = m_model.pal();
auto const width = s.columns * TileWidth;
auto const height = s.rows * TileHeight;
auto pixels = normalizePixelSizes(s.pixels, img.bpp);
pixels = normalizePixelArrangement(pixels, s.columns, scale);
auto const err = toPngFile(
path,
std::move(pixels),
*pal,
static_cast<unsigned>(width * scale),
static_cast<unsigned>(height * scale));
if (err) {
oxErrorf("Tilesheet export failed: {}", toStr(err));
}
return err;
}
void TileSheetEditorImGui::drawTileSheet(ox::Vec2 const&fbSize) noexcept {
const auto winPos = ImGui::GetWindowPos();
const auto fbSizei = ox::Size(static_cast<int>(fbSize.x), static_cast<int>(fbSize.y));
if (m_framebuffer.width != fbSizei.width || m_framebuffer.height != fbSizei.height) {
glutils::resizeInitFrameBuffer(m_framebuffer, fbSizei.width, fbSizei.height);
m_view.resizeView(fbSize);
} else if (m_view.updated()) {
m_view.ackUpdate();
}
glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
// clear screen and draw
glViewport(0, 0, fbSizei.width, fbSizei.height);
m_view.draw();
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ImGui::Image(
std::bit_cast<ImTextureID>(uintptr_t{m_framebuffer.color.id}),
static_cast<ImVec2>(fbSize),
ImVec2(0, 1),
ImVec2(1, 0));
// handle input, this must come after drawing
const auto &io = ImGui::GetIO();
const auto mousePos = ox::Vec2(io.MousePos);
if (ImGui::IsItemHovered()) {
const auto wheel = io.MouseWheel;
const auto wheelh = io.MouseWheelH;
if (wheel != 0) {
const auto zoomMod = ox::defines::OS == ox::OS::Darwin ?
io.KeySuper : turbine::buttonDown(m_ctx, turbine::Key::Mod_Ctrl);
m_view.scrollV(fbSize, wheel, zoomMod);
}
if (wheelh != 0) {
m_view.scrollH(fbSize, wheelh);
}
if (io.MouseDown[0] && m_prevMouseDownPos != mousePos) {
m_prevMouseDownPos = mousePos;
switch (m_tool) {
case Tool::Draw:
m_view.clickDraw(fbSize, clickPos(winPos, mousePos));
break;
case Tool::Fill:
m_view.clickFill(fbSize, clickPos(winPos, mousePos));
break;
case Tool::Select:
m_view.clickSelect(fbSize, clickPos(winPos, mousePos));
break;
case Tool::None:
break;
}
}
}
if (ImGui::BeginPopupContextItem("TileMenu", ImGuiPopupFlags_MouseButtonRight)) {
const auto popupPos = ox::Vec2(ImGui::GetWindowPos());
if (ImGui::MenuItem("Insert Tile")) {
m_view.insertTile(fbSize, clickPos(winPos, popupPos));
}
if (ImGui::MenuItem("Delete Tile")) {
m_view.deleteTile(fbSize, clickPos(winPos, popupPos));
}
ImGui::EndPopup();
}
if (io.MouseReleased[0]) {
m_prevMouseDownPos = {-1, -1};
m_view.releaseMouseButton();
}
}
void TileSheetEditorImGui::drawPaletteSelector() noexcept {
auto &sctx = *applicationData<studio::StudioContext>(m_ctx);
const auto &files = sctx.project->fileList(core::FileExt_npal);
const auto first = m_selectedPaletteIdx < files.size() ?
files[m_selectedPaletteIdx].c_str() : "";
if (ImGui::BeginCombo("Palette", first, 0)) {
for (auto n = 0u; n < files.size(); n++) {
const auto selected = (m_selectedPaletteIdx == n);
if (ImGui::Selectable(files[n].c_str(), selected) && m_selectedPaletteIdx != n) {
m_selectedPaletteIdx = n;
oxLogError(m_model.setPalette(files[n]));
}
if (selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
// header
if (ImGui::BeginTable("PaletteTable", 3, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) {
ImGui::TableSetupColumn("No.", 0, 0.45f);
ImGui::TableSetupColumn("", 0, 0.22f);
ImGui::TableSetupColumn("Color16", 0, 3);
ImGui::TableHeadersRow();
if (auto pal = m_view.pal()) {
for (auto i = 0u; auto c: pal->colors) {
ImGui::PushID(static_cast<int>(i));
// Column: color idx
ImGui::TableNextColumn();
const auto label = ox::BString<8>() + (i + 1);
const auto rowSelected = i == m_view.palIdx();
if (ImGui::Selectable(label.c_str(), rowSelected, ImGuiSelectableFlags_SpanAllColumns)) {
m_view.setPalIdx(i);
}
// Column: color RGB
ImGui::TableNextColumn();
auto ic = ImGui::GetColorU32(ImVec4(redf(c), greenf(c), bluef(c), 1));
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ic);
ImGui::TableNextColumn();
ImGui::Text("(%02d, %02d, %02d)", red16(c), green16(c), blue16(c));
ImGui::TableNextRow();
ImGui::PopID();
++i;
}
}
ImGui::EndTable();
}
}
ox::Error TileSheetEditorImGui::updateActiveSubsheet(ox::StringView const&name, int cols, int rows) noexcept {
return m_model.updateSubsheet(m_model.activeSubSheetIdx(), name, cols, rows);
}
ox::Error TileSheetEditorImGui::setPaletteSelection() noexcept {
const auto &palPath = m_model.palPath();
auto &sctx = *applicationData<studio::StudioContext>(m_ctx);
const auto &palList = sctx.project->fileList(core::FileExt_npal);
for (std::size_t i = 0; const auto &pal : palList) {
if (palPath == pal) {
m_selectedPaletteIdx = i;
break;
}
++i;
}
return {};
}
ox::Error TileSheetEditorImGui::markUnsavedChanges(const studio::UndoCommand*) noexcept {
setUnsavedChanges(true);
return {};
}
void TileSheetEditorImGui::SubSheetEditor::draw() noexcept {
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
constexpr auto popupName = "Edit Subsheet";
if (!m_show) {
return;
}
ImGui::OpenPopup(popupName);
const auto modSize = m_cols > 0;
const auto popupHeight = modSize ? 125.f : 80.f;
ImGui::SetNextWindowSize(ImVec2(235, popupHeight));
if (ImGui::BeginPopupModal(popupName, &m_show, modalFlags)) {
ImGui::InputText("Name", m_name.data(), m_name.cap());
if (modSize) {
ImGui::InputInt("Columns", &m_cols);
ImGui::InputInt("Rows", &m_rows);
}
if (ImGui::Button("OK", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup();
m_show = false;
inputSubmitted.emit(m_name, m_cols, m_rows);
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup();
m_show = false;
}
ImGui::EndPopup();
}
}
void TileSheetEditorImGui::SubSheetEditor::close() noexcept {
m_show = false;
}
void TileSheetEditorImGui::ExportMenu::draw() noexcept {
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
constexpr auto popupName = "Export Tile Sheet";
if (!m_show) {
return;
}
ImGui::OpenPopup(popupName);
constexpr auto popupHeight = 80.f;
ImGui::SetNextWindowSize(ImVec2(235, popupHeight));
if (ImGui::BeginPopupModal(popupName, &m_show, modalFlags)) {
ImGui::InputInt("Scale", &m_scale);
m_scale = ox::clamp(m_scale, 1, 20);
if (ImGui::Button("OK", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup();
m_show = false;
inputSubmitted.emit(m_scale);
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2{50, 20})) {
ImGui::CloseCurrentPopup();
m_show = false;
}
ImGui::EndPopup();
}
}
void TileSheetEditorImGui::ExportMenu::close() noexcept {
m_show = false;
}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/model/def.hpp>
#include <ox/std/vec.hpp>
#include <glutils/glutils.hpp>
#include <studio/editor.hpp>
#include "tilesheetpixelgrid.hpp"
#include "tilesheetpixels.hpp"
#include "tilesheeteditorview.hpp"
namespace nostalgia::core {
enum class Tool {
None,
Draw,
Fill,
Select,
};
class TileSheetEditorImGui: public studio::Editor {
private:
class SubSheetEditor {
ox::BString<100> m_name;
int m_cols = 0;
int m_rows = 0;
bool m_show = false;
public:
ox::Signal<ox::Error(ox::StringView const&name, int cols, int rows)> inputSubmitted;
constexpr void show(ox::StringView const&name, int cols, int rows) noexcept {
m_show = true;
m_name = name;
m_cols = cols;
m_rows = rows;
}
void draw() noexcept;
void close() noexcept;
};
class ExportMenu {
int m_scale = 0;
bool m_show = false;
public:
ox::Signal<ox::Error(int scale)> inputSubmitted;
constexpr void show() noexcept {
m_show = true;
m_scale = 5;
}
void draw() noexcept;
void close() noexcept;
};
std::size_t m_selectedPaletteIdx = 0;
turbine::Context &m_ctx;
ox::Vector<ox::String> m_paletteList;
SubSheetEditor m_subsheetEditor;
ExportMenu m_exportMenu;
glutils::FrameBuffer m_framebuffer;
TileSheetEditorView m_view;
TileSheetEditorModel &m_model;
float m_palViewWidth = 300;
ox::Vec2 m_prevMouseDownPos;
Tool m_tool = Tool::Draw;
public:
TileSheetEditorImGui(turbine::Context &ctx, ox::CRStringView path);
~TileSheetEditorImGui() override = default;
void exportFile() override;
void cut() override;
void copy() override;
void paste() override;
void keyStateChanged(turbine::Key key, bool down) override;
void draw(turbine::Context&) noexcept override;
void drawSubsheetSelector(TileSheet::SubSheet*, TileSheet::SubSheetIdx *path);
[[nodiscard]]
static ox::Vec2 clickPos(ImVec2 const&winPos, ox::Vec2 clickPos) noexcept;
protected:
ox::Error saveItem() noexcept override;
private:
void showSubsheetEditor() noexcept;
ox::Error exportSubhseetToPng(int scale) noexcept;
void drawTileSheet(ox::Vec2 const&fbSize) noexcept;
void drawPaletteSelector() noexcept;
ox::Error updateActiveSubsheet(ox::StringView const&name, int cols, int rows) noexcept;
ox::Error setPaletteSelection() noexcept;
// slots
private:
ox::Error markUnsavedChanges(const studio::UndoCommand*) noexcept;
};
}

View File

@ -0,0 +1,290 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/read.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/memory.hpp>
#include <turbine/clipboard.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include "commands/commands.hpp"
#include "commands/addsubsheetcommand.hpp"
#include "commands/cutpastecommand.hpp"
#include "commands/deletetilescommand.hpp"
#include "commands/drawcommand.hpp"
#include "commands/inserttilescommand.hpp"
#include "commands/palettechangecommand.hpp"
#include "commands/rmsubsheetcommand.hpp"
#include "commands/updatesubsheetcommand.hpp"
#include "tilesheeteditormodel.hpp"
namespace nostalgia::core {
const Palette TileSheetEditorModel::s_defaultPalette = {
.colors = ox::Vector<Color16>(128),
};
TileSheetEditorModel::TileSheetEditorModel(turbine::Context &ctx, ox::StringView path, studio::UndoStack &undoStack):
m_ctx(ctx),
m_path(path),
m_img(*readObj<TileSheet>(keelCtx(m_ctx), m_path).unwrapThrow()),
// ignore failure to load palette
m_pal(readObj<Palette>(keelCtx(m_ctx), m_img.defaultPalette).value),
m_undoStack(undoStack) {
m_pal.updated.connect(this, &TileSheetEditorModel::markUpdated);
m_undoStack.changeTriggered.connect(this, &TileSheetEditorModel::markUpdatedCmdId);
}
void TileSheetEditorModel::cut() {
TileSheetClipboard blankCb;
auto cb = ox::make_unique<TileSheetClipboard>();
const auto s = activeSubSheet();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::Point(x, y);
const auto idx = s->idx(pt);
const auto c = s->getPixel(m_img.bpp, idx);
pt.x -= m_selectionBounds.x;
pt.y -= m_selectionBounds.y;
cb->addPixel(pt, c);
blankCb.addPixel(pt, 0);
}
}
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
turbine::setClipboardObject(m_ctx, std::move(cb));
pushCommand(ox::make<CutPasteCommand>(CommandId::Cut, m_img, m_activeSubsSheetIdx, pt1, pt2, blankCb));
}
void TileSheetEditorModel::copy() {
auto cb = ox::make_unique<TileSheetClipboard>();
for (int y = m_selectionBounds.y; y <= m_selectionBounds.y2(); ++y) {
for (int x = m_selectionBounds.x; x <= m_selectionBounds.x2(); ++x) {
auto pt = ox::Point(x, y);
const auto s = activeSubSheet();
const auto idx = s->idx(pt);
const auto c = s->getPixel(m_img.bpp, idx);
pt.x -= m_selectionBounds.x;
pt.y -= m_selectionBounds.y;
cb->addPixel(pt, c);
}
}
turbine::setClipboardObject(m_ctx, std::move(cb));
}
void TileSheetEditorModel::paste() {
auto [cb, err] = turbine::getClipboardObject<TileSheetClipboard>(m_ctx);
if (err) {
oxLogError(err);
oxErrf("Could not read clipboard: {}", toStr(err));
return;
}
const auto s = activeSubSheet();
const auto pt1 = m_selectionOrigin == ox::Point(-1, -1) ? ox::Point(0, 0) : m_selectionOrigin;
const auto pt2 = ox::Point(s->columns * TileWidth, s->rows * TileHeight);
pushCommand(ox::make<CutPasteCommand>(CommandId::Paste, m_img, m_activeSubsSheetIdx, pt1, pt2, *cb));
}
ox::StringView TileSheetEditorModel::palPath() const noexcept {
auto [path, err] = m_img.defaultPalette.getPath();
if (err) {
return {};
}
constexpr ox::StringView uuidPrefix = "uuid://";
if (ox::beginsWith(path, uuidPrefix)) {
auto uuid = ox::StringView(path.data() + uuidPrefix.bytes(), path.bytes() - uuidPrefix.bytes());
auto out = keelCtx(m_ctx).uuidToPath.at(uuid);
if (out.error) {
return {};
}
return *out.value;
} else {
return path;
}
}
ox::Error TileSheetEditorModel::setPalette(ox::StringView path) noexcept {
oxRequire(uuid, keelCtx(m_ctx).pathToUuid.at(path));
pushCommand(ox::make<PaletteChangeCommand>(activeSubSheetIdx(), m_img, uuid->toString()));
return {};
}
void TileSheetEditorModel::drawCommand(ox::Point const&pt, std::size_t palIdx) noexcept {
const auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
if (pt.x >= activeSubSheet.columns * TileWidth || pt.y >= activeSubSheet.rows * TileHeight) {
return;
}
const auto idx = activeSubSheet.idx(pt);
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idx);
} else if (activeSubSheet.getPixel(m_img.bpp, idx) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idx, static_cast<int>(palIdx)));
}
}
void TileSheetEditorModel::endDrawCommand() noexcept {
m_ongoingDrawCommand = nullptr;
}
void TileSheetEditorModel::addSubsheet(TileSheet::SubSheetIdx const&parentIdx) noexcept {
pushCommand(ox::make<AddSubSheetCommand>(m_img, parentIdx));
}
void TileSheetEditorModel::rmSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
pushCommand(ox::make<RmSubSheetCommand>(m_img, idx));
}
void TileSheetEditorModel::insertTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<InsertTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
void TileSheetEditorModel::deleteTiles(TileSheet::SubSheetIdx const&idx, std::size_t tileIdx, std::size_t tileCnt) noexcept {
pushCommand(ox::make<DeleteTilesCommand>(m_img, idx, tileIdx, tileCnt));
}
ox::Error TileSheetEditorModel::updateSubsheet(TileSheet::SubSheetIdx const&idx, const ox::StringView &name, int cols, int rows) noexcept {
pushCommand(ox::make<UpdateSubSheetCommand>(m_img, idx, ox::String(name), cols, rows));
return {};
}
void TileSheetEditorModel::setActiveSubsheet(TileSheet::SubSheetIdx const&idx) noexcept {
m_activeSubsSheetIdx = idx;
this->activeSubsheetChanged.emit(m_activeSubsSheetIdx);
}
void TileSheetEditorModel::fill(ox::Point const&pt, int palIdx) noexcept {
const auto &s = m_img.getSubSheet(m_activeSubsSheetIdx);
// build idx list
ox::Array<bool, PixelsPerTile> updateMap = {};
const auto oldColor = s.getPixel(m_img.bpp, pt);
if (pt.x >= s.columns * TileWidth || pt.y >= s.rows * TileHeight) {
return;
}
getFillPixels(updateMap.data(), pt, oldColor);
ox::Vector<std::size_t> idxList;
auto i = s.idx(pt) / PixelsPerTile * PixelsPerTile;
for (auto u : updateMap) {
if (u) {
idxList.emplace_back(i);
}
++i;
}
// do updates to sheet
if (m_ongoingDrawCommand) {
m_updated = m_updated || m_ongoingDrawCommand->append(idxList);
} else if (s.getPixel(m_img.bpp, pt) != palIdx) {
pushCommand(ox::make<DrawCommand>(m_img, m_activeSubsSheetIdx, idxList, palIdx));
}
}
void TileSheetEditorModel::select(ox::Point const&pt) noexcept {
if (!m_selectionOngoing) {
m_selectionOrigin = pt;
m_selectionOngoing = true;
m_selectionBounds = {pt, pt};
m_updated = true;
} else if (m_selectionBounds.pt2() != pt) {
m_selectionBounds = {m_selectionOrigin, pt};
m_updated = true;
}
}
void TileSheetEditorModel::completeSelection() noexcept {
m_selectionOngoing = false;
auto s = activeSubSheet();
auto pt = m_selectionBounds.pt2();
pt.x = ox::min(s->columns * TileWidth, pt.x);
pt.y = ox::min(s->rows * TileHeight, pt.y);
m_selectionBounds.setPt2(pt);
}
void TileSheetEditorModel::clearSelection() noexcept {
m_updated = true;
m_selectionOrigin = {-1, -1};
m_selectionBounds = {{-1, -1}, {-1, -1}};
}
bool TileSheetEditorModel::updated() const noexcept {
return m_updated;
}
ox::Error TileSheetEditorModel::markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept {
m_updated = true;
const auto cmdId = cmd->commandId();
if (static_cast<CommandId>(cmdId) == CommandId::PaletteChange) {
oxReturnError(readObj<Palette>(keelCtx(m_ctx), m_img.defaultPalette).moveTo(m_pal));
paletteChanged.emit();
}
auto tsCmd = dynamic_cast<const TileSheetCommand*>(cmd);
auto idx = m_img.validateSubSheetIdx(tsCmd->subsheetIdx());
if (idx != m_activeSubsSheetIdx) {
setActiveSubsheet(idx);
}
return {};
}
ox::Error TileSheetEditorModel::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorModel::ackUpdate() noexcept {
m_updated = false;
}
ox::Error TileSheetEditorModel::saveFile() noexcept {
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
return sctx->project->writeObj(m_path, m_img);
}
bool TileSheetEditorModel::pixelSelected(std::size_t idx) const noexcept {
const auto s = activeSubSheet();
const auto pt = idxToPt(static_cast<int>(idx), s->columns);
return m_selectionBounds.contains(pt);
}
void TileSheetEditorModel::getFillPixels(bool *pixels, ox::Point const&pt, int oldColor) const noexcept {
const auto &activeSubSheet = *this->activeSubSheet();
const auto tileIdx = [activeSubSheet](const ox::Point &pt) noexcept {
return ptToIdx(pt, activeSubSheet.columns) / PixelsPerTile;
};
// get points
const auto leftPt = pt + ox::Point(-1, 0);
const auto rightPt = pt + ox::Point(1, 0);
const auto topPt = pt + ox::Point(0, -1);
const auto bottomPt = pt + ox::Point(0, 1);
// calculate indices
const auto idx = ptToIdx(pt, activeSubSheet.columns);
const auto leftIdx = ptToIdx(leftPt, activeSubSheet.columns);
const auto rightIdx = ptToIdx(rightPt, activeSubSheet.columns);
const auto topIdx = ptToIdx(topPt, activeSubSheet.columns);
const auto bottomIdx = ptToIdx(bottomPt, activeSubSheet.columns);
const auto tile = tileIdx(pt);
// mark pixels to update
pixels[idx % PixelsPerTile] = true;
if (!pixels[leftIdx % PixelsPerTile] && tile == tileIdx(leftPt) && activeSubSheet.getPixel(m_img.bpp, leftIdx) == oldColor) {
getFillPixels(pixels, leftPt, oldColor);
}
if (!pixels[rightIdx % PixelsPerTile] && tile == tileIdx(rightPt) && activeSubSheet.getPixel(m_img.bpp, rightIdx) == oldColor) {
getFillPixels(pixels, rightPt, oldColor);
}
if (!pixels[topIdx % PixelsPerTile] && tile == tileIdx(topPt) && activeSubSheet.getPixel(m_img.bpp, topIdx) == oldColor) {
getFillPixels(pixels, topPt, oldColor);
}
if (!pixels[bottomIdx % PixelsPerTile] && tile == tileIdx(bottomPt) && activeSubSheet.getPixel(m_img.bpp, bottomIdx) == oldColor) {
getFillPixels(pixels, bottomPt, oldColor);
}
}
void TileSheetEditorModel::pushCommand(studio::UndoCommand *cmd) noexcept {
m_undoStack.push(ox::UPtr<studio::UndoCommand>(cmd));
m_ongoingDrawCommand = dynamic_cast<DrawCommand*>(cmd);
m_updated = true;
}
}

View File

@ -0,0 +1,148 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/bounds.hpp>
#include <ox/std/point.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/string.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::core {
class TileSheetEditorModel: public ox::SignalHandler {
public:
ox::Signal<ox::Error(const TileSheet::SubSheetIdx&)> activeSubsheetChanged;
ox::Signal<ox::Error()> paletteChanged;
private:
static const Palette s_defaultPalette;
turbine::Context &m_ctx;
ox::String m_path;
TileSheet m_img;
TileSheet::SubSheetIdx m_activeSubsSheetIdx;
keel::AssetRef<Palette> m_pal;
studio::UndoStack &m_undoStack;
class DrawCommand *m_ongoingDrawCommand = nullptr;
bool m_updated = false;
bool m_selectionOngoing = false;
ox::Point m_selectionOrigin = {-1, -1};
ox::Bounds m_selectionBounds = {{-1, -1}, {-1, -1}};
public:
TileSheetEditorModel(turbine::Context &ctx, ox::StringView path, studio::UndoStack &undoStack);
~TileSheetEditorModel() override = default;
void cut();
void copy();
void paste();
[[nodiscard]]
constexpr const TileSheet &img() const noexcept;
[[nodiscard]]
constexpr TileSheet &img() noexcept;
[[nodiscard]]
constexpr const Palette *pal() const noexcept;
[[nodiscard]]
ox::StringView palPath() const noexcept;
ox::Error setPalette(ox::StringView path) noexcept;
void drawCommand(const ox::Point &pt, std::size_t palIdx) noexcept;
void endDrawCommand() noexcept;
void addSubsheet(const TileSheet::SubSheetIdx &parentIdx) noexcept;
void rmSubsheet(const TileSheet::SubSheetIdx &idx) noexcept;
void insertTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept;
void deleteTiles(const TileSheet::SubSheetIdx &idx, std::size_t tileIdx, std::size_t tileCnt) noexcept;
ox::Error updateSubsheet(const TileSheet::SubSheetIdx &idx, const ox::StringView &name, int cols, int rows) noexcept;
void setActiveSubsheet(const TileSheet::SubSheetIdx&) noexcept;
[[nodiscard]]
const TileSheet::SubSheet *activeSubSheet() const noexcept {
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
return &activeSubSheet;
}
[[nodiscard]]
TileSheet::SubSheet *activeSubSheet() noexcept {
auto &activeSubSheet = m_img.getSubSheet(m_activeSubsSheetIdx);
return &activeSubSheet;
}
[[nodiscard]]
constexpr TileSheet::SubSheetIdx const&activeSubSheetIdx() const noexcept {
return m_activeSubsSheetIdx;
}
void fill(ox::Point const&pt, int palIdx) noexcept;
void select(ox::Point const&pt) noexcept;
void completeSelection() noexcept;
void clearSelection() noexcept;
[[nodiscard]]
bool updated() const noexcept;
ox::Error markUpdatedCmdId(const studio::UndoCommand *cmd) noexcept;
ox::Error markUpdated() noexcept;
void ackUpdate() noexcept;
ox::Error saveFile() noexcept;
[[nodiscard]]
constexpr studio::UndoStack *undoStack() noexcept;
bool pixelSelected(std::size_t idx) const noexcept;
protected:
void getFillPixels(bool *pixels, ox::Point const&pt, int oldColor) const noexcept;
private:
void pushCommand(studio::UndoCommand *cmd) noexcept;
};
constexpr const TileSheet &TileSheetEditorModel::img() const noexcept {
return m_img;
}
constexpr TileSheet &TileSheetEditorModel::img() noexcept {
return m_img;
}
constexpr const Palette *TileSheetEditorModel::pal() const noexcept {
if (m_pal) {
return m_pal.get();
}
return &s_defaultPalette;
}
constexpr studio::UndoStack *TileSheetEditorModel::undoStack() noexcept {
return &m_undoStack;
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/point.hpp>
#include <keel/media.hpp>
#include <nostalgia/core/consts.hpp>
#include "tilesheeteditorview.hpp"
namespace nostalgia::core {
TileSheetEditorView::TileSheetEditorView(turbine::Context &ctx, ox::StringView path, studio::UndoStack &undoStack):
m_model(ctx, path, undoStack),
m_pixelsDrawer(&m_model) {
// build shaders
oxThrowError(m_pixelsDrawer.buildShader());
oxThrowError(m_pixelGridDrawer.buildShader());
m_model.activeSubsheetChanged.connect(this, &TileSheetEditorView::setActiveSubsheet);
}
void TileSheetEditorView::draw() noexcept {
constexpr Color32 bgColor = 0x717d7e;
glClearColor(redf(bgColor), greenf(bgColor), bluef(bgColor), 1.f);
glClear(GL_COLOR_BUFFER_BIT);
m_pixelsDrawer.draw(updated(), m_scrollOffset);
m_pixelGridDrawer.draw(updated(), m_scrollOffset);
}
void TileSheetEditorView::scrollV(ox::Vec2 const&paneSz, float wheel, bool zoomMod) noexcept {
const auto pixelSize = m_pixelsDrawer.pixelSize(paneSz);
const ImVec2 sheetSize(pixelSize.x * static_cast<float>(m_model.activeSubSheet()->columns) * TileWidth,
pixelSize.y * static_cast<float>(m_model.activeSubSheet()->rows) * TileHeight);
if (zoomMod) {
m_pixelSizeMod = ox::clamp(m_pixelSizeMod + wheel * 0.02f, 0.55f, 2.f);
m_pixelsDrawer.setPixelSizeMod(m_pixelSizeMod);
m_pixelGridDrawer.setPixelSizeMod(m_pixelSizeMod);
m_updated = true;
} else {
m_scrollOffset.y -= wheel * 0.1f;
}
// adjust scroll offset in both cases because the image can be zoomed
// or scrolled off screen
m_scrollOffset.y = ox::clamp(m_scrollOffset.y, 0.f, sheetSize.y / 2);
}
void TileSheetEditorView::scrollH(ox::Vec2 const&paneSz, float wheelh) noexcept {
const auto pixelSize = m_pixelsDrawer.pixelSize(paneSz);
const ImVec2 sheetSize(pixelSize.x * static_cast<float>(m_model.activeSubSheet()->columns) * TileWidth,
pixelSize.y * static_cast<float>(m_model.activeSubSheet()->rows) * TileHeight);
m_scrollOffset.x += wheelh * 0.1f;
m_scrollOffset.x = ox::clamp(m_scrollOffset.x, -(sheetSize.x / 2), 0.f);
}
void TileSheetEditorView::insertTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
const auto s = m_model.activeSubSheet();
const auto tileIdx = ptToIdx(pt, s->columns) / PixelsPerTile;
m_model.insertTiles(m_model.activeSubSheetIdx(), tileIdx, 1);
}
void TileSheetEditorView::deleteTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
const auto s = m_model.activeSubSheet();
const auto tileIdx = ptToIdx(pt, s->columns) / PixelsPerTile;
m_model.deleteTiles(m_model.activeSubSheetIdx(), tileIdx, 1);
}
void TileSheetEditorView::clickDraw(ox::Vec2 const &paneSize, ox::Vec2 const&clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
m_model.drawCommand(pt, m_palIdx);
}
void TileSheetEditorView::clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
m_model.select(pt);
}
void TileSheetEditorView::clickFill(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept {
const auto pt = clickPoint(paneSize, clickPos);
m_model.fill(pt, static_cast<int>(m_palIdx));
}
void TileSheetEditorView::releaseMouseButton() noexcept {
m_model.endDrawCommand();
m_model.completeSelection();
}
void TileSheetEditorView::resizeView(ox::Vec2 const&sz) noexcept {
m_viewSize = sz;
initView();
}
bool TileSheetEditorView::updated() const noexcept {
return m_updated || m_model.updated();
}
ox::Error TileSheetEditorView::markUpdated() noexcept {
m_updated = true;
return {};
}
void TileSheetEditorView::ackUpdate() noexcept {
m_updated = false;
m_pixelsDrawer.update(m_viewSize);
m_pixelGridDrawer.update(m_viewSize, *m_model.activeSubSheet());
m_model.ackUpdate();
}
void TileSheetEditorView::initView() noexcept {
m_pixelsDrawer.initBufferSet(m_viewSize);
m_pixelGridDrawer.initBufferSet(m_viewSize, *m_model.activeSubSheet());
}
ox::Point TileSheetEditorView::clickPoint(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) const noexcept {
auto [x, y] = clickPos;
const auto pixDrawSz = m_pixelsDrawer.pixelSize(paneSize);
x /= paneSize.x;
y /= paneSize.y;
x += -m_scrollOffset.x / 2;
y += m_scrollOffset.y / 2;
x /= pixDrawSz.x;
y /= pixDrawSz.y;
return {static_cast<int>(x * 2), static_cast<int>(y * 2)};
}
ox::Error TileSheetEditorView::setActiveSubsheet(TileSheet::SubSheetIdx const&) noexcept {
initView();
return {};
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vec.hpp>
#include <ox/model/def.hpp>
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
#include "tilesheeteditormodel.hpp"
#include "tilesheetpixelgrid.hpp"
#include "tilesheetpixels.hpp"
namespace nostalgia::core {
enum class TileSheetTool: int {
Select,
Draw,
Fill,
};
[[nodiscard]]
constexpr auto toString(TileSheetTool t) noexcept {
switch (t) {
case TileSheetTool::Select:
return "Select";
case TileSheetTool::Draw:
return "Draw";
case TileSheetTool::Fill:
return "Fill";
}
return "";
}
class TileSheetEditorView: public ox::SignalHandler {
private:
TileSheetEditorModel m_model;
TileSheetGrid m_pixelGridDrawer;
TileSheetPixels m_pixelsDrawer;
ox::Vec2 m_viewSize;
float m_pixelSizeMod = 1;
bool m_updated = false;
ox::Vec2 m_scrollOffset;
std::size_t m_palIdx = 0;
public:
TileSheetEditorView(turbine::Context &ctx, ox::StringView path, studio::UndoStack &undoStack);
~TileSheetEditorView() override = default;
void draw() noexcept;
void insertTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void deleteTile(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickDraw(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickSelect(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void clickFill(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) noexcept;
void releaseMouseButton() noexcept;
void scrollV(ox::Vec2 const&paneSz, float wheel, bool zoomMod) noexcept;
void scrollH(ox::Vec2 const&paneSz, float wheel) noexcept;
void resizeView(ox::Vec2 const&sz) noexcept;
[[nodiscard]]
constexpr TileSheet const&img() const noexcept;
[[nodiscard]]
constexpr TileSheet &img() noexcept;
[[nodiscard]]
constexpr const Palette *pal() const noexcept;
[[nodiscard]]
constexpr auto &model() noexcept {
return m_model;
}
[[nodiscard]]
constexpr auto &model() const noexcept {
return m_model;
}
constexpr auto setPalIdx(auto palIdx) noexcept {
m_palIdx = palIdx;
}
[[nodiscard]]
constexpr auto palIdx() const noexcept {
return m_palIdx;
}
[[nodiscard]]
bool updated() const noexcept;
ox::Error markUpdated() noexcept;
void ackUpdate() noexcept;
private:
void initView() noexcept;
ox::Point clickPoint(ox::Vec2 const&paneSize, ox::Vec2 const&clickPos) const noexcept;
ox::Error setActiveSubsheet(TileSheet::SubSheetIdx const&idx) noexcept;
};
constexpr TileSheet const&TileSheetEditorView::img() const noexcept {
return m_model.img();
}
constexpr TileSheet &TileSheetEditorView::img() noexcept {
return m_model.img();
}
constexpr const Palette *TileSheetEditorView::pal() const noexcept {
return m_model.pal();
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/write.hpp>
#include <nostalgia/core/consts.hpp>
#include "tilesheetpixelgrid.hpp"
namespace nostalgia::core {
void TileSheetGrid::setPixelSizeMod(float sm) noexcept {
m_pixelSizeMod = sm;
}
ox::Error TileSheetGrid::buildShader() noexcept {
const auto pixelLineVshad = ox::sfmt(VShad, gl::GlslVersion);
const auto pixelLineFshad = ox::sfmt(FShad, gl::GlslVersion);
const auto pixelLineGshad = ox::sfmt(GShad, gl::GlslVersion);
return glutils::buildShaderProgram(pixelLineVshad, pixelLineFshad, pixelLineGshad).moveTo(m_shader);
}
void TileSheetGrid::draw(bool update, ox::Vec2 const&scroll) noexcept {
glLineWidth(3 * m_pixelSizeMod * 0.5f);
glUseProgram(m_shader);
glBindVertexArray(m_bufferSet.vao);
if (update) {
glutils::sendVbo(m_bufferSet);
}
const auto uniformScroll = glGetUniformLocation(m_shader, "gScroll");
glUniform2f(uniformScroll, scroll.x, scroll.y);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(m_bufferSet.vertices.size() / VertexVboRowLength));
glBindVertexArray(0);
glUseProgram(0);
}
void TileSheetGrid::initBufferSet(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
// vao
m_bufferSet.vao = glutils::generateVertexArrayObject();
glBindVertexArray(m_bufferSet.vao);
// vbo
m_bufferSet.vbo = glutils::generateBuffer();
setBufferObjects(paneSize, subsheet);
glutils::sendVbo(m_bufferSet);
// vbo layout
const auto pt1Attr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPt1"));
glEnableVertexAttribArray(pt1Attr);
glVertexAttribPointer(pt1Attr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float), nullptr);
const auto pt2Attr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPt2"));
glEnableVertexAttribArray(pt2Attr);
glVertexAttribPointer(pt2Attr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{2 * sizeof(float)}));
const auto colorAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vColor"));
glEnableVertexAttribArray(colorAttr);
glVertexAttribPointer(colorAttr, 3, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{4 * sizeof(float)}));
}
void TileSheetGrid::update(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
glBindVertexArray(m_bufferSet.vao);
setBufferObjects(paneSize, subsheet);
glutils::sendVbo(m_bufferSet);
glutils::sendEbo(m_bufferSet);
}
void TileSheetGrid::setBufferObject(ox::Point pt1, ox::Point pt2, Color32 c, float *vbo, ox::Vec2 const&pixSize) noexcept {
const auto x1 = static_cast<float>(pt1.x) * pixSize.x - 1.f;
const auto y1 = 1.f - static_cast<float>(pt1.y) * pixSize.y;
const auto x2 = static_cast<float>(pt2.x) * pixSize.x - 1.f;
const auto y2 = 1.f - static_cast<float>(pt2.y) * pixSize.y;
// don't worry, this memcpy gets optimized to something much more ideal
const ox::Array<float, VertexVboLength> vertices = {x1, y1, x2, y2, redf(c), greenf(c), bluef(c)};
memcpy(vbo, vertices.data(), sizeof(vertices));
}
void TileSheetGrid::setBufferObjects(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept {
const auto pixSize = pixelSize(paneSize);
const auto set = [&](std::size_t i, ox::Point pt1, ox::Point pt2, Color32 c) {
const auto vbo = &m_bufferSet.vertices[i * VertexVboLength];
setBufferObject(pt1, pt2, c, vbo, pixSize);
};
// set buffer length
const auto width = subsheet.columns * TileWidth;
const auto height = subsheet.rows * TileHeight;
const auto tileCnt = static_cast<unsigned>(subsheet.columns + subsheet.rows);
const auto pixelCnt = static_cast<unsigned>(width + height);
m_bufferSet.vertices.resize(static_cast<std::size_t>(tileCnt + pixelCnt + 4) * VertexVboLength);
// set buffer
std::size_t i = 0;
// pixel outlines
constexpr auto pixOutlineColor = color32(0.4431f, 0.4901f, 0.4941f);
for (auto x = 0; x < subsheet.columns * TileWidth + 1; ++x) {
set(i, {x, 0}, {x, subsheet.rows * TileHeight}, pixOutlineColor);
++i;
}
for (auto y = 0; y < subsheet.rows * TileHeight + 1; ++y) {
set(i, {0, y}, {subsheet.columns * TileWidth, y}, pixOutlineColor);
++i;
}
// tile outlines
constexpr auto tileOutlineColor = color32(0.f, 0.f, 0.f);
for (auto x = 0; x < subsheet.columns * TileWidth + 1; x += TileWidth) {
set(i, {x, 0}, {x, subsheet.rows * TileHeight}, tileOutlineColor);
++i;
}
for (auto y = 0; y < subsheet.rows * TileHeight + 1; y += TileHeight) {
set(i, {0, y}, {subsheet.columns * TileWidth, y}, tileOutlineColor);
++i;
}
}
ox::Vec2 TileSheetGrid::pixelSize(ox::Vec2 const&paneSize) const noexcept {
const auto [sw, sh] = paneSize;
constexpr float ymod = 0.35f / 10.0f;
const auto xmod = ymod * sh / sw;
return {xmod * m_pixelSizeMod, ymod * m_pixelSizeMod};
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::core {
class TileSheetGrid {
private:
static constexpr auto VertexVboRows = 1;
static constexpr auto VertexVboRowLength = 7;
static constexpr auto VertexVboLength = VertexVboRows * VertexVboRowLength;
static constexpr auto VShad = R"glsl(
{}
in vec2 vPt1;
in vec2 vPt2;
in vec3 vColor;
out vec2 gPt2;
out vec3 gColor;
void main() {
gColor = vColor;
gl_Position = vec4(vPt1, 0.0, 1.0);
gPt2 = vPt2;
})glsl";
static constexpr auto FShad = R"glsl(
{}
in vec3 fColor;
out vec4 outColor;
void main() {
outColor = vec4(fColor, 1);
//outColor = vec4(0.4431, 0.4901, 0.4941, 1.0);
})glsl";
static constexpr auto GShad = R"glsl(
{}
layout(points) in;
layout(line_strip, max_vertices = 2) out;
in vec3 gColor[];
in vec2 gPt2[];
out vec3 fColor;
uniform vec2 gScroll;
void main() {
fColor = gColor[0];
gl_Position = gl_in[0].gl_Position + vec4(gScroll, 0, 0);
EmitVertex();
gl_Position = vec4(gPt2[0] + gScroll, 0, 1);
EmitVertex();
EndPrimitive();
})glsl";
glutils::GLProgram m_shader;
glutils::BufferSet m_bufferSet;
float m_pixelSizeMod = 1;
public:
void setPixelSizeMod(float sm) noexcept;
ox::Error buildShader() noexcept;
void draw(bool update, ox::Vec2 const&scroll) noexcept;
void initBufferSet(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
void update(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
private:
static void setBufferObject(ox::Point pt1, ox::Point pt2, Color32 c, float *vbo, ox::Vec2 const&pixSize) noexcept;
void setBufferObjects(ox::Vec2 const&paneSize, TileSheet::SubSheet const&subsheet) noexcept;
[[nodiscard]]
ox::Vec2 pixelSize(ox::Vec2 const&paneSize) const noexcept;
};
}

View File

@ -0,0 +1,128 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/consts.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include "tilesheeteditormodel.hpp"
#include "tilesheetpixels.hpp"
namespace nostalgia::core {
TileSheetPixels::TileSheetPixels(TileSheetEditorModel *model) noexcept: m_model(model) {
}
void TileSheetPixels::setPixelSizeMod(float sm) noexcept {
m_pixelSizeMod = sm;
}
ox::Error TileSheetPixels::buildShader() noexcept {
const auto Vshad = ox::sfmt(VShad, gl::GlslVersion);
const auto Fshad = ox::sfmt(FShad, gl::GlslVersion);
return glutils::buildShaderProgram(Vshad, Fshad).moveTo(m_shader);
}
void TileSheetPixels::draw(bool update, ox::Vec2 const&scroll) noexcept {
glUseProgram(m_shader);
glBindVertexArray(m_bufferSet.vao);
if (update) {
glutils::sendVbo(m_bufferSet);
}
const auto uniformScroll = glGetUniformLocation(m_shader, "vScroll");
glUniform2f(uniformScroll, scroll.x, scroll.y);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(m_bufferSet.elements.size()), GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
glUseProgram(0);
}
void TileSheetPixels::initBufferSet(ox::Vec2 const&paneSize) noexcept {
m_bufferSet.vao = glutils::generateVertexArrayObject();
m_bufferSet.vbo = glutils::generateBuffer();
m_bufferSet.ebo = glutils::generateBuffer();
update(paneSize);
// vbo layout
const auto posAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vPosition"));
glEnableVertexAttribArray(posAttr);
glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float), nullptr);
const auto colorAttr = static_cast<GLuint>(glGetAttribLocation(m_shader, "vColor"));
glEnableVertexAttribArray(colorAttr);
glVertexAttribPointer(colorAttr, 3, GL_FLOAT, GL_FALSE, VertexVboRowLength * sizeof(float),
std::bit_cast<void*>(uintptr_t{2 * sizeof(float)}));
}
void TileSheetPixels::update(ox::Vec2 const&paneSize) noexcept {
glBindVertexArray(m_bufferSet.vao);
setBufferObjects(paneSize);
glutils::sendVbo(m_bufferSet);
glutils::sendEbo(m_bufferSet);
}
ox::Vec2 TileSheetPixels::pixelSize(ox::Vec2 const&paneSize) const noexcept {
const auto [sw, sh] = paneSize;
constexpr float ymod = 0.35f / 10.0f;
const auto xmod = ymod * sh / sw;
return {xmod * m_pixelSizeMod, ymod * m_pixelSizeMod};
}
void TileSheetPixels::setPixelBufferObject(
ox::Vec2 const&paneSize,
unsigned vertexRow,
float x, float y,
Color16 color,
float *vbo,
GLuint *ebo) const noexcept {
const auto [xmod, ymod] = pixelSize(paneSize);
x *= xmod;
y *= -ymod;
x -= 1.0f;
y += 1.0f - ymod;
const auto r = redf(color), g = greenf(color), b = bluef(color);
// don't worry, these memcpys gets optimized to something much more ideal
const ox::Array<float, VertexVboLength> vertices = {
x, y, r, g, b, // bottom left
x + xmod, y, r, g, b, // bottom right
x + xmod, y + ymod, r, g, b, // top right
x, y + ymod, r, g, b, // top left
};
memcpy(vbo, vertices.data(), sizeof(vertices));
const ox::Array<GLuint, VertexEboLength> elms = {
vertexRow + 0, vertexRow + 1, vertexRow + 2,
vertexRow + 2, vertexRow + 3, vertexRow + 0,
};
memcpy(ebo, elms.data(), sizeof(elms));
}
void TileSheetPixels::setBufferObjects(ox::Vec2 const&paneSize) noexcept {
// set buffer lengths
const auto subSheet = m_model->activeSubSheet();
const auto pal = m_model->pal();
const auto width = subSheet->columns * TileWidth;
const auto height = subSheet->rows * TileHeight;
const auto pixels = static_cast<unsigned>(width * height);
m_bufferSet.vertices.resize(pixels * VertexVboLength);
m_bufferSet.elements.resize(pixels * VertexEboLength);
// set pixels
subSheet->walkPixels(m_model->img().bpp, [&](std::size_t i, uint8_t p) {
auto color = pal->color(p);
const auto pt = idxToPt(static_cast<int>(i), subSheet->columns);
const auto fx = static_cast<float>(pt.x);
const auto fy = static_cast<float>(pt.y);
const auto vbo = &m_bufferSet.vertices[i * VertexVboLength];
const auto ebo = &m_bufferSet.elements[i * VertexEboLength];
if (i * VertexVboLength + VertexVboLength > m_bufferSet.vertices.size()) {
return;
}
if (i * VertexEboLength + VertexEboLength > m_bufferSet.elements.size()) {
return;
}
if (m_model->pixelSelected(i)) {
const auto r = red16(color) / 2;
const auto g = (green16(color) + 20) / 2;
const auto b = (blue16(color) + 31) / 2;
color = color16(r, g, b);
}
setPixelBufferObject(paneSize, static_cast<unsigned>(i * VertexVboRows), fx, fy, color, vbo, ebo);
});
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vec.hpp>
#include <glutils/glutils.hpp>
#include <studio/studio.hpp>
#include <nostalgia/core/color.hpp>
#include <nostalgia/core/gfx.hpp>
namespace nostalgia::core {
class TileSheetPixels {
private:
static constexpr auto VertexVboRows = 4;
static constexpr auto VertexVboRowLength = 5;
static constexpr auto VertexVboLength = VertexVboRows * VertexVboRowLength;
static constexpr auto VertexEboLength = 6;
static constexpr auto VShad = R"(
{}
in vec2 vPosition;
in vec3 vColor;
out vec3 fColor;
uniform vec2 vScroll;
void main() {
gl_Position = vec4(vPosition + vScroll, 0.0, 1.0);
fColor = vColor;
})";
static constexpr auto FShad = R"(
{}
in vec3 fColor;
out vec4 outColor;
void main() {
//outColor = vec4(0.0, 0.7, 1.0, 1.0);
outColor = vec4(fColor, 1.0);
})";
float m_pixelSizeMod = 1;
glutils::GLProgram m_shader;
glutils::BufferSet m_bufferSet;
const class TileSheetEditorModel *m_model = nullptr;
public:
explicit TileSheetPixels(class TileSheetEditorModel *model) noexcept;
void setPixelSizeMod(float sm) noexcept;
ox::Error buildShader() noexcept;
void draw(bool update, ox::Vec2 const&scroll) noexcept;
void initBufferSet(ox::Vec2 const&paneSize) noexcept;
void update(ox::Vec2 const&paneSize) noexcept;
[[nodiscard]]
ox::Vec2 pixelSize(ox::Vec2 const&paneSize) const noexcept;
private:
void setPixelBufferObject(ox::Vec2 const&paneS, unsigned vertexRow, float x, float y, Color16 color, float *vbo, GLuint *ebo) const noexcept;
void setBufferObjects(ox::Vec2 const&paneS) noexcept;
};
}

View File

@ -0,0 +1,380 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/size.hpp>
#include <ox/std/vector.hpp>
#include <nostalgia/core/ptidxconv.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::core {
TileSheet::SubSheet::SubSheet(SubSheet &&other) noexcept:
id (other.id),
name (std::move(other.name)),
columns (other.columns),
rows (other.rows),
subsheets(std::move(other.subsheets)),
pixels (std::move(other.pixels)) {
other.name = "";
other.columns = {};
other.rows = {};
}
TileSheet::SubSheet::SubSheet(
SubSheetId pId,
ox::CRStringView pName,
int pColumns,
int pRows,
int bpp) noexcept:
id(pId),
name(pName),
columns(pColumns),
rows(pRows),
pixels(static_cast<std::size_t>(columns * rows * PixelsPerTile) / (bpp == 4 ? 2u : 1u)) {
}
TileSheet::SubSheet::SubSheet(
SubSheetId pId,
ox::CRStringView pName,
int pColumns,
int pRows,
ox::Vector<uint8_t> pPixels) noexcept:
id(pId),
name(pName),
columns(pColumns),
rows(pRows),
pixels(std::move(pPixels)) {
}
TileSheet::SubSheet &TileSheet::SubSheet::operator=(TileSheet::SubSheet &&other) noexcept {
name = std::move(other.name);
columns = other.columns;
rows = other.rows;
subsheets = std::move(other.subsheets);
pixels = std::move(other.pixels);
return *this;
}
std::size_t TileSheet::SubSheet::idx(ox::Point const&pt) const noexcept {
return ptToIdx(pt, columns);
}
void TileSheet::SubSheet::readPixelsTo(ox::Vector<uint8_t> *pPixels, int8_t pBpp) const noexcept {
if (!subsheets.empty()) {
for (auto &s: subsheets) {
s.readPixelsTo(pPixels);
}
} else {
if (pBpp == 4) {
for (auto p: this->pixels) {
pPixels->emplace_back(static_cast<uint8_t>(p & 0b1111));
pPixels->emplace_back(static_cast<uint8_t>(p >> 4));
}
} else {
for (auto p: this->pixels) {
pPixels->emplace_back(p);
}
}
}
}
void TileSheet::SubSheet::readPixelsTo(ox::Vector<uint8_t> *pPixels) const noexcept {
if (!subsheets.empty()) {
for (auto &s: subsheets) {
s.readPixelsTo(pPixels);
}
} else {
for (auto p : this->pixels) {
pPixels->emplace_back(p);
}
}
}
std::size_t TileSheet::SubSheet::unusedPixels() const noexcept {
std::size_t childrenSize = 0;
for (auto &c : subsheets) {
childrenSize += c.size();
}
return size() - childrenSize;
}
uint8_t TileSheet::SubSheet::getPixel4Bpp(std::size_t idx) const noexcept {
if (idx & 1) {
return this->pixels[idx / 2] >> 4;
} else {
return this->pixels[idx / 2] & 0b0000'1111;
}
}
uint8_t TileSheet::SubSheet::getPixel8Bpp(std::size_t idx) const noexcept {
return this->pixels[idx];
}
uint8_t TileSheet::SubSheet::getPixel(int8_t pBpp, std::size_t idx) const noexcept {
if (pBpp == 4) {
return getPixel4Bpp(idx);
} else {
return getPixel8Bpp(idx);
}
}
uint8_t TileSheet::SubSheet::getPixel4Bpp(ox::Point const&pt) const noexcept {
const auto idx = ptToIdx(pt, columns);
return getPixel4Bpp(idx);
}
uint8_t TileSheet::SubSheet::getPixel8Bpp(ox::Point const&pt) const noexcept {
const auto idx = ptToIdx(pt, columns);
return getPixel8Bpp(idx);
}
uint8_t TileSheet::SubSheet::getPixel(int8_t pBpp, ox::Point const&pt) const noexcept {
const auto idx = ptToIdx(pt, columns);
return getPixel(pBpp, idx);
}
void TileSheet::SubSheet::setPixel(int8_t pBpp, uint64_t idx, uint8_t palIdx) noexcept {
auto &pixel = this->pixels[static_cast<std::size_t>(idx / 2)];
if (pBpp == 4) {
if (idx & 1) {
pixel = static_cast<uint8_t>((pixel & 0b0000'1111) | (palIdx << 4));
} else {
pixel = (pixel & 0b1111'0000) | (palIdx);
}
} else {
pixel = palIdx;
}
}
void TileSheet::SubSheet::setPixel(int8_t pBpp, ox::Point const&pt, uint8_t palIdx) noexcept {
const auto idx = ptToIdx(pt, columns);
setPixel(pBpp, idx, palIdx);
}
ox::Error TileSheet::SubSheet::setPixelCount(int8_t pBpp, std::size_t cnt) noexcept {
switch (pBpp) {
case 4:
pixels.resize(cnt / 2);
return OxError(0);
case 8:
pixels.resize(cnt);
return OxError(0);
default:
return OxError(1, "Invalid pBpp used for TileSheet::SubSheet::setPixelCount");
}
}
unsigned TileSheet::SubSheet::pixelCnt(int8_t pBpp) const noexcept {
const auto pixelsSize = static_cast<unsigned>(pixels.size());
return pBpp == 4 ? pixelsSize * 2 : pixelsSize;
}
ox::Result<unsigned> TileSheet::SubSheet::getTileOffset(
ox::SpanView<ox::StringView> const&pNamePath,
int8_t pBpp,
std::size_t pIt,
unsigned pCurrentTotal) const noexcept {
// pIt == pNamePath.size() - 1 &&
if (name != pNamePath[pIt]) {
return OxError(2, "Wrong branch");
}
if (pIt == pNamePath.size() - 1) {
return pCurrentTotal;
}
for (auto &sub : subsheets) {
auto [offset, err] = sub.getTileOffset(
pNamePath, pBpp, pIt + 1, pCurrentTotal);
if (!err) {
return offset;
}
pCurrentTotal += sub.pixelCnt(pBpp) / PixelsPerTile;
}
return OxError(1, "SubSheet not found");
}
ox::Result<SubSheetId> TileSheet::SubSheet::getIdFor(
ox::SpanView<ox::StringView> const&pNamePath,
std::size_t pIt) const noexcept {
for (auto &sub : subsheets) {
if (sub.name == pNamePath[pIt]) {
if (pIt == pNamePath.size()) {
return id;
}
return getIdFor(pNamePath, pIt + 1);
}
}
return OxError(1, "SubSheet not found");
}
ox::Result<ox::StringView> TileSheet::SubSheet::getNameFor(SubSheetId pId) const noexcept {
if (id == pId) {
return ox::StringView(name);
}
for (const auto &sub : subsheets) {
const auto [name, err] = sub.getNameFor(pId);
if (!err) {
return name;
}
}
return OxError(1, "SubSheet not found");
}
TileSheet &TileSheet::operator=(TileSheet const&other) noexcept {
if (this != &other) {
bpp = other.bpp;
idIt = other.idIt;
defaultPalette = other.defaultPalette;
subsheet = other.subsheet;
}
return *this;
}
TileSheet &TileSheet::operator=(TileSheet &&other) noexcept {
bpp = other.bpp;
idIt = other.idIt;
defaultPalette = std::move(other.defaultPalette);
subsheet = std::move(other.subsheet);
return *this;
}
TileSheet::SubSheetIdx TileSheet::validateSubSheetIdx(
const SubSheetIdx &pIdx,
std::size_t pIdxIt,
const SubSheet *pSubsheet) noexcept {
if (pIdxIt == pIdx.size()) {
return pIdx;
}
const auto currentIdx = pIdx[pIdxIt];
if (pSubsheet->subsheets.size() <= currentIdx) {
auto out = pIdx;
if (!pSubsheet->subsheets.empty()) {
*out.back().value = pSubsheet->subsheets.size() - 1;
} else {
out.pop_back();
}
return out;
}
return validateSubSheetIdx(pIdx, pIdxIt + 1, &pSubsheet->subsheets[pIdx[pIdxIt]]);
}
TileSheet::SubSheetIdx TileSheet::validateSubSheetIdx(const SubSheetIdx &idx) noexcept {
return validateSubSheetIdx(idx, 0, &subsheet);
}
const TileSheet::SubSheet &TileSheet::getSubSheet(
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
SubSheet const*pSubsheet) noexcept {
if (idxIt == idx.size()) {
return *pSubsheet;
}
const auto currentIdx = idx[idxIt];
if (pSubsheet->subsheets.size() < currentIdx) {
return *pSubsheet;
}
return getSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[currentIdx]);
}
TileSheet::SubSheet &TileSheet::getSubSheet(
TileSheet::SubSheetIdx const&idx,
std::size_t idxIt,
TileSheet::SubSheet *pSubsheet) noexcept {
if (idxIt == idx.size()) {
return *pSubsheet;
}
return getSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[idx[idxIt]]);
}
const TileSheet::SubSheet &TileSheet::getSubSheet(TileSheet::SubSheetIdx const&idx) const noexcept {
return getSubSheet(idx, 0, &subsheet);
}
TileSheet::SubSheet &TileSheet::getSubSheet(TileSheet::SubSheetIdx const&idx) noexcept {
return getSubSheet(idx, 0, &subsheet);
}
ox::Error TileSheet::addSubSheet(TileSheet::SubSheetIdx const&idx) noexcept {
auto &parent = getSubSheet(idx);
if (parent.subsheets.size() < 2) {
parent.subsheets.emplace_back(idIt++, ox::sfmt("Subsheet {}", parent.subsheets.size()), 1, 1, bpp);
} else {
parent.subsheets.emplace_back(idIt++, "Subsheet 0", parent.columns, parent.rows, bpp);
parent.subsheets.emplace_back(idIt++, "Subsheet 1", 1, 1, bpp);
}
return OxError(0);
}
ox::Error TileSheet::rmSubSheet(
SubSheetIdx const&idx,
std::size_t idxIt,
SubSheet *pSubsheet) noexcept {
if (idxIt == idx.size() - 1) {
return pSubsheet->subsheets.erase(idx[idxIt]).error;
}
return rmSubSheet(idx, idxIt + 1, &pSubsheet->subsheets[idx[idxIt]]);
}
ox::Error TileSheet::rmSubSheet(TileSheet::SubSheetIdx const&idx) noexcept {
return rmSubSheet(idx, 0, &subsheet);
}
uint8_t TileSheet::getPixel4Bpp(
ox::Point const&pt,
TileSheet::SubSheetIdx const&subsheetIdx) const noexcept {
oxAssert(bpp == 4, "TileSheet::getPixel4Bpp: wrong bpp");
auto &s = this->getSubSheet(subsheetIdx);
const auto idx = ptToIdx(pt, s.columns);
return s.getPixel4Bpp(idx);
}
uint8_t TileSheet::getPixel8Bpp(
ox::Point const&pt,
TileSheet::SubSheetIdx const&subsheetIdx) const noexcept {
oxAssert(bpp == 8, "TileSheet::getPixel8Bpp: wrong bpp");
auto &s = this->getSubSheet(subsheetIdx);
const auto idx = ptToIdx(pt, s.columns);
return s.getPixel8Bpp(idx);
}
ox::Result<SubSheetId> TileSheet::getIdFor(ox::CRStringView path) const noexcept {
return subsheet.getIdFor(ox::split<8>(path, '.'));
}
ox::Result<unsigned> TileSheet::getTileOffset(ox::CRStringView pNamePath) const noexcept {
return subsheet.getTileOffset(ox::split<8>(pNamePath, '.'), bpp);
}
ox::Result<ox::StringView> TileSheet::getNameFor(SubSheetId pId) const noexcept {
return subsheet.getNameFor(pId);
}
ox::Vector<uint8_t> TileSheet::pixels() const noexcept {
ox::Vector<uint8_t> out;
subsheet.readPixelsTo(&out);
return out;
}
ox::Vector<uint32_t> resizeTileSheetData(
ox::Vector<uint32_t> const&srcPixels,
ox::Size const&srcSize,
int scale) noexcept {
ox::Vector<uint32_t> dst;
auto dstWidth = srcSize.width * scale;
auto dstHeight = srcSize.height * scale;
const auto pixelCnt = dstWidth * dstHeight;
dst.resize(static_cast<std::size_t>(pixelCnt));
for (auto i = 0; i < pixelCnt; ++i) {
const auto dstPt = idxToPt(i, 1, scale);
const auto srcPt = dstPt / ox::Point{scale, scale};
const auto srcIdx = ptToIdx(srcPt, 1);
const auto srcPixel = srcPixels[srcIdx];
dst[static_cast<std::size_t>(i)] = srcPixel;
}
return dst;
}
}

View File

@ -0,0 +1,11 @@
add_executable(
NostalgiaCoreTest
tests.cpp
)
target_link_libraries(
NostalgiaCoreTest
NostalgiaCore
)
add_test("[NostalgiaCore] readWriteTileSheet" ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/NostalgiaCoreTest readWriteTileSheet)

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#undef NDEBUG
#include <map>
#include <ox/std/error.hpp>
#include <ox/mc/mc.hpp>
#include <nostalgia/core/core.hpp>
using namespace nostalgia;
static std::map<ox::StringView, ox::Error(*)()> tests = {
{
"readWriteTileSheet",
[]() -> ox::Error {
core::TileSheet in;
oxRequire(buff, ox::writeMC(in));
oxRequire(out, ox::readMC<core::TileSheet>(buff));
oxAssert(in.subsheet.name == out.subsheet.name, "subsheet.name serialization broken");
return {};
}
},
};
int main(int argc, const char **args) {
int retval = -1;
if (argc > 0) {
auto const testName = ox::StringView(args[1]);
if (tests.find(testName) != tests.end()) {
retval = static_cast<int>(tests[testName]());
} else {
retval = 1;
}
}
return retval;
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/keelmodule.hpp>
#include <nostalgia/scene/keelmodule.hpp>
namespace nostalgia {
static bool modulesRegistered = false;
void registerKeelModules() noexcept {
if (modulesRegistered) {
return;
}
modulesRegistered = true;
keel::registerModule(core::keelModule());
keel::registerModule(scene::keelModule());
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace nostalgia {
void registerKeelModules() noexcept;
}

View File

@ -0,0 +1,13 @@
add_subdirectory(src)
target_include_directories(
NostalgiaScene PUBLIC
include
)
install(
DIRECTORY
include/nostalgia
DESTINATION
include
)

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <keel/module.hpp>
namespace nostalgia::scene {
const keel::Module *keelModule() noexcept;
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <nostalgia/core/context.hpp>
#include "scenestatic.hpp"
namespace nostalgia::scene {
class Scene {
private:
SceneStatic const&m_sceneStatic;
public:
explicit Scene(SceneStatic const&sceneStatic) noexcept;
ox::Error setupDisplay(core::Context &ctx) const noexcept;
private:
void setupLayer(core::Context&, ox::Vector<uint16_t> const&layer, unsigned layerNo) const noexcept;
};
}

View File

@ -0,0 +1,228 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/std/error.hpp>
#include <ox/std/size.hpp>
#include <ox/std/types.hpp>
#include <ox/std/vector.hpp>
#include <nostalgia/core/tilesheet.hpp>
namespace nostalgia::scene {
struct SpriteDoc {
constexpr static auto TypeName = "net.drinkingtea.nostalgia.scene.SpriteDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ox::String tilesheetPath;
ox::Vector<core::SubSheetId> subsheetId;
};
struct TileDoc {
constexpr static auto TypeName = "net.drinkingtea.nostalgia.scene.TileDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
core::SubSheetId subsheetId = -1;
ox::String subsheetPath;
uint8_t type = 0;
ox::Array<uint8_t, 4> layerAttachments;
[[nodiscard]]
constexpr ox::Result<core::SubSheetId> getSubsheetId(core::TileSheet const&ts) const noexcept {
// prefer the already present ID
if (subsheetId > -1) {
return subsheetId;
}
return ts.getIdFor(subsheetPath);
}
[[nodiscard]]
constexpr ox::Result<ox::StringView> getSubsheetPath(
core::TileSheet const&ts) const noexcept {
// prefer the already present path
if (!subsheetPath.len()) {
return ts.getNameFor(subsheetId);
}
return ox::StringView(subsheetPath);
}
};
oxModelBegin(TileDoc)
oxModelFieldRename(subsheet_id, subsheetId)
oxModelFieldRename(subsheet_path, subsheetPath)
oxModelField(type)
oxModelFieldRename(layer_attachments, layerAttachments)
oxModelEnd()
struct SceneDoc {
using TileMapRow = ox::Vector<TileDoc>;
using TileMapLayer = ox::Vector<TileMapRow>;
using TileMap = ox::Vector<TileMapLayer>;
constexpr static auto TypeName = "net.drinkingtea.nostalgia.scene.SceneDoc";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
ox::String tilesheet; // path
ox::Vector<ox::String> palettes; // paths
TileMap tiles;
[[nodiscard]]
constexpr ox::Size size(std::size_t layerIdx) const noexcept {
const auto &layer = this->tiles[layerIdx];
const auto rowCnt = static_cast<int>(layer.size());
if (!rowCnt) {
return {};
}
auto colCnt = layer[0].size();
// find shortest row (they should all be the same, but you know this data
// could come from a file)
for (auto const&row : layer) {
colCnt = ox::min(colCnt, row.size());
}
return {static_cast<int>(colCnt), rowCnt};
}
};
oxModelBegin(SceneDoc)
oxModelField(tilesheet)
oxModelField(palettes)
oxModelField(tiles)
oxModelEnd()
constexpr void setTopEdge(uint8_t &layerAttachments, unsigned val) noexcept {
const auto val8 = static_cast<uint8_t>(val);
layerAttachments = (layerAttachments & 0b11111100) | val8;
}
constexpr void setBottomEdge(uint8_t &layerAttachments, unsigned val) noexcept {
const auto val8 = static_cast<uint8_t>(val);
layerAttachments = (layerAttachments & 0b11110011) | static_cast<uint8_t>(val8 << 2);
}
constexpr void setLeftEdge(uint8_t &layerAttachments, unsigned val) noexcept {
const auto val8 = static_cast<uint8_t>(val);
layerAttachments = (layerAttachments & 0b11001111) | static_cast<uint8_t>(val8 << 4);
}
constexpr void setRightEdge(uint8_t &layerAttachments, unsigned val) noexcept {
const auto val8 = static_cast<uint8_t>(val);
layerAttachments = (layerAttachments & 0b00111111) | static_cast<uint8_t>(val8 << 6);
}
[[nodiscard]]
constexpr unsigned topEdge(uint8_t layerAttachments) noexcept {
return layerAttachments & 0b11;
}
[[nodiscard]]
constexpr unsigned bottomEdge(uint8_t layerAttachments) noexcept {
return (layerAttachments >> 2) & 0b11;
}
[[nodiscard]]
constexpr unsigned leftEdge(uint8_t layerAttachments) noexcept {
return (layerAttachments >> 4) & 0b11;
}
[[nodiscard]]
constexpr unsigned rightEdge(uint8_t layerAttachments) noexcept {
return (layerAttachments >> 6) & 0b11;
}
struct SceneStatic {
constexpr static auto TypeName = "net.drinkingtea.nostalgia.scene.SceneStatic";
constexpr static auto TypeVersion = 1;
constexpr static auto Preloadable = true;
struct Tile {
uint16_t &tileMapIdx;
uint8_t &tileType;
uint8_t &layerAttachments;
constexpr Tile(uint16_t &pTileMapIdx, uint8_t &pTileType, uint8_t &pLayerAttachments) noexcept:
tileMapIdx(pTileMapIdx),
tileType(pTileType),
layerAttachments(pLayerAttachments) {
}
};
struct Layer {
uint16_t &columns;
uint16_t &rows;
ox::Vector<uint16_t> &tileMapIdx;
ox::Vector<uint8_t> &tileType;
ox::Vector<uint8_t> &layerAttachments;
constexpr Layer(
uint16_t &pColumns,
uint16_t &pRows,
ox::Vector<uint16_t> &pTileMapIdx,
ox::Vector<uint8_t> &pTileType,
ox::Vector<uint8_t> &pLayerAttachments) noexcept:
columns(pColumns),
rows(pRows),
tileMapIdx(pTileMapIdx),
tileType(pTileType),
layerAttachments(pLayerAttachments) {
}
[[nodiscard]]
constexpr Tile tile(std::size_t i) noexcept {
return {tileMapIdx[i], tileType[i], layerAttachments[i]};
}
constexpr auto setDimensions(ox::Size dim) noexcept {
columns = static_cast<uint16_t>(dim.width);
rows = static_cast<uint16_t>(dim.height);
const auto tileCnt = static_cast<unsigned>(columns * rows);
tileMapIdx.resize(tileCnt);
tileType.resize(tileCnt);
layerAttachments.resize(tileCnt);
}
};
ox::FileAddress tilesheet;
ox::Vector<ox::FileAddress> palettes;
// tile layer data
ox::Vector<uint16_t> columns;
ox::Vector<uint16_t> rows;
ox::Vector<ox::Vector<uint16_t>> tileMapIdx;
ox::Vector<ox::Vector<uint8_t>> tileType;
ox::Vector<ox::Vector<uint8_t>> layerAttachments;
[[nodiscard]]
constexpr Layer layer(std::size_t i) noexcept {
return {
columns[i],
rows[i],
tileMapIdx[i],
tileType[i],
layerAttachments[i],
};
}
constexpr auto setLayerCnt(std::size_t layerCnt) noexcept {
this->layerAttachments.resize(layerCnt);
this->columns.resize(layerCnt);
this->rows.resize(layerCnt);
this->tileMapIdx.resize(layerCnt);
this->tileType.resize(layerCnt);
}
};
oxModelBegin(SceneStatic)
oxModelField(tilesheet)
oxModelField(palettes)
oxModelField(columns)
oxModelField(rows)
oxModelField(tileMapIdx)
oxModelField(tileType)
oxModelField(layerAttachments)
oxModelEnd()
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
namespace nostalgia::scene {
const studio::Module *studioModule() noexcept;
}

View File

@ -0,0 +1,29 @@
add_library(
NostalgiaScene
scene.cpp
scenestatic.cpp
)
target_include_directories(
NostalgiaScene PUBLIC
../include
)
target_link_libraries(
NostalgiaScene PUBLIC
NostalgiaCore
)
add_subdirectory(keel)
if(NOSTALGIA_BUILD_STUDIO)
add_subdirectory(studio)
endif()
install(
TARGETS
NostalgiaScene
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@ -0,0 +1,17 @@
add_library(
NostalgiaScene-Keel
keelmodule.cpp
typeconv.cpp
)
target_link_libraries(
NostalgiaScene-Keel PUBLIC
Keel
NostalgiaScene
)
install(
TARGETS
NostalgiaScene-Keel
LIBRARY DESTINATION
${NOSTALGIA_DIST_MODULE}
)

View File

@ -0,0 +1,52 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/module.hpp>
#include <nostalgia/scene/scenestatic.hpp>
#include "typeconv.hpp"
namespace nostalgia::scene {
class SceneModule: public keel::Module {
private:
SceneDocToSceneStaticConverter m_sceneDocToSceneStaticConverter;
public:
[[nodiscard]]
ox::String id() const noexcept override {
return ox::String("net.drinkingtea.nostalgia.scene");
}
[[nodiscard]]
ox::Vector<keel::TypeDescGenerator> types() const noexcept override {
return {
keel::generateTypeDesc<SceneDoc>,
keel::generateTypeDesc<SceneStatic>,
};
}
[[nodiscard]]
ox::Vector<const keel::BaseConverter*> converters() const noexcept override {
return {
&m_sceneDocToSceneStaticConverter,
};
}
[[nodiscard]]
ox::Vector<keel::PackTransform> packTransforms() const noexcept override {
return {
keel::transformRule<SceneDoc, SceneStatic>,
};
}
};
static const SceneModule mod;
const keel::Module *keelModule() noexcept {
return &mod;
}
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/gfx.hpp>
#include <keel/media.hpp>
#include "typeconv.hpp"
namespace nostalgia::scene {
[[nodiscard]]
constexpr unsigned adjustLayerAttachment(unsigned layer, unsigned attachment) noexcept {
if (attachment == 0) {
return layer;
} else {
return attachment - 1;
}
}
constexpr void setLayerAttachments(unsigned layer, TileDoc const&srcTile, SceneStatic::Tile &dstTile) noexcept {
setTopEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[0]));
setBottomEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[1]));
setLeftEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[2]));
setRightEdge(
dstTile.layerAttachments,
adjustLayerAttachment(layer, srcTile.layerAttachments[3]));
}
ox::Error SceneDocToSceneStaticConverter::convert(
keel::Context &ctx,
SceneDoc &src,
SceneStatic &dst) const noexcept {
oxRequire(ts, keel::readObj<core::TileSheet>(ctx, src.tilesheet));
const auto layerCnt = src.tiles.size();
dst.setLayerCnt(layerCnt);
dst.tilesheet = ox::FileAddress(src.tilesheet);
dst.palettes.reserve(src.palettes.size());
for (const auto &pal : src.palettes) {
dst.palettes.emplace_back(pal);
}
for (auto layerIdx = 0u; const auto &layer : src.tiles) {
const auto layerDim = src.size(layerIdx);
auto dstLayer = dst.layer(layerIdx);
dstLayer.setDimensions(layerDim);
for (auto tileIdx = 0u; const auto &row : layer) {
for (const auto &srcTile : row) {
auto dstTile = dstLayer.tile(tileIdx);
dstTile.tileType = srcTile.type;
oxRequire(path, srcTile.getSubsheetPath(*ts));
oxRequire(mapIdx, ts->getTileOffset(path));
dstTile.tileMapIdx = static_cast<uint16_t>(mapIdx);
setLayerAttachments(layerIdx, srcTile, dstTile);
++tileIdx;
}
}
++layerIdx;
}
return {};
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <keel/typeconv.hpp>
#include <nostalgia/scene/scenestatic.hpp>
namespace nostalgia::scene {
class SceneDocToSceneStaticConverter: public keel::Converter<SceneDoc, SceneStatic> {
ox::Error convert(keel::Context&, SceneDoc &src, SceneStatic &dst) const noexcept final;
};
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/scene/scene.hpp>
namespace nostalgia::scene {
Scene::Scene(SceneStatic const&sceneStatic) noexcept:
m_sceneStatic(sceneStatic) {
}
ox::Error Scene::setupDisplay(core::Context &ctx) const noexcept {
if (m_sceneStatic.palettes.empty()) {
return OxError(1, "Scene has no palettes");
}
auto const&palette = m_sceneStatic.palettes[0];
oxReturnError(core::loadBgTileSheet(ctx, 0, m_sceneStatic.tilesheet));
oxReturnError(core::loadBgPalette(ctx, palette));
// disable all backgrounds
core::setBgStatus(ctx, 0);
for (auto layerNo = 0u; auto const&layer : m_sceneStatic.tileMapIdx) {
setupLayer(ctx, layer, layerNo);
++layerNo;
}
return {};
}
void Scene::setupLayer(
core::Context &ctx,
ox::Vector<uint16_t> const&layer,
unsigned layerNo) const noexcept {
core::setBgStatus(ctx, layerNo, true);
core::setBgCbb(ctx, layerNo, 0);
auto x = 0;
auto y = 0;
const auto width = m_sceneStatic.rows[layerNo];
for (auto const&tile : layer) {
const auto tile8 = static_cast<uint8_t>(tile);
core::setBgTile(ctx, layerNo, x, y, tile8);
core::setBgTile(ctx, layerNo, x + 1, y, tile8 + 1);
core::setBgTile(ctx, layerNo, x, y + 1, tile8 + 2);
core::setBgTile(ctx, layerNo, x + 1, y + 1, tile8 + 3);
x += 2;
if (x >= width * 2) {
x = 0;
y += 2;
}
}
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/scene/scenestatic.hpp>
namespace nostalgia::scene {
}

View File

@ -0,0 +1,20 @@
add_library(
NostalgiaScene-Studio
studiomodule.cpp
sceneeditor-imgui.cpp
sceneeditor.cpp
sceneeditorview.cpp
)
target_link_libraries(
NostalgiaScene-Studio PUBLIC
NostalgiaScene
Studio
)
install(
TARGETS
NostalgiaScene-Studio
LIBRARY DESTINATION
${NOSTALGIA_DIST_MODULE}
)

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <keel/media.hpp>
#include "sceneeditor-imgui.hpp"
namespace nostalgia::scene {
SceneEditorImGui::SceneEditorImGui(turbine::Context &ctx, ox::StringView path):
Editor(path),
m_ctx(ctx),
m_editor(m_ctx, path),
m_view(m_ctx, m_editor.scene()) {
setRequiresConstantRefresh(false);
}
void SceneEditorImGui::draw(turbine::Context&) noexcept {
auto const paneSize = ImGui::GetContentRegionAvail();
m_view.draw(ox::Size{static_cast<int>(paneSize.x), static_cast<int>(paneSize.y)});
auto &fb = m_view.framebuffer();
auto const srcH = static_cast<float>(fb.height) / static_cast<float>(fb.width);
auto const dstH = paneSize.y / paneSize.x;
float xScale{}, yScale{};
if (dstH > srcH) {
// crop off width
xScale = srcH / dstH;
yScale = 1;
} else {
auto const srcW = static_cast<float>(fb.width) / static_cast<float>(fb.height);
auto const dstW = (paneSize.x / paneSize.y);
xScale = 1;
yScale = srcW / dstW;
}
uintptr_t const buffId = fb.color.id;
ImGui::Image(
std::bit_cast<void*>(buffId),
paneSize,
ImVec2(0, 1),
ImVec2(xScale, 1 - yScale));
}
void SceneEditorImGui::onActivated() noexcept {
oxLogError(m_view.setupScene());
}
ox::Error SceneEditorImGui::saveItem() noexcept {
const auto sctx = applicationData<studio::StudioContext>(m_ctx);
oxReturnError(sctx->project->writeObj(itemPath(), m_editor.scene()));
oxReturnError(keelCtx(m_ctx).assetManager.setAsset(itemPath(), m_editor.scene()));
return {};
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/studio.hpp>
#include <turbine/context.hpp>
#include "sceneeditor.hpp"
#include "sceneeditorview.hpp"
namespace nostalgia::scene {
class SceneEditorImGui: public studio::Editor {
private:
turbine::Context &m_ctx;
SceneEditor m_editor;
SceneEditorView m_view;
public:
SceneEditorImGui(turbine::Context &ctx, ox::StringView path);
void draw(turbine::Context&) noexcept final;
void onActivated() noexcept override;
protected:
ox::Error saveItem() noexcept final;
};
}

View File

@ -0,0 +1,16 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/keel.hpp>
#include "sceneeditor.hpp"
namespace nostalgia::scene {
SceneEditor::SceneEditor(turbine::Context &ctx, ox::CRStringView path):
m_ctx(ctx),
m_scene(*keel::readObj<SceneStatic>(keelCtx(m_ctx), path).unwrapThrow()) {
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <turbine/context.hpp>
#include <nostalgia/scene/scene.hpp>
namespace nostalgia::scene {
class SceneEditor {
private:
turbine::Context &m_ctx;
ox::String m_itemName;
ox::String m_itemPath;
SceneStatic m_scene;
public:
SceneEditor(turbine::Context &ctx, ox::CRStringView path);
[[nodiscard]]
SceneStatic const&scene() const noexcept {
return m_scene;
}
protected:
ox::Error saveItem() noexcept;
};
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nostalgia/core/gfx.hpp>
#include "sceneeditorview.hpp"
namespace nostalgia::scene {
SceneEditorView::SceneEditorView(turbine::Context &tctx, SceneStatic const&sceneStatic):
m_cctx(core::init(tctx, {.glInstallDrawer = false}).unwrapThrow()),
m_sceneStatic(sceneStatic),
m_scene(m_sceneStatic) {
}
ox::Error SceneEditorView::setupScene() noexcept {
glutils::resizeInitFrameBuffer(m_frameBuffer, core::gl::drawSize(m_scale));
return m_scene.setupDisplay(*m_cctx);
}
void SceneEditorView::draw(ox::Size const&targetSz) noexcept {
auto const scaleSz = targetSz / core::gl::drawSize(1);
if (m_scaleSz != scaleSz) [[unlikely]] {
m_scale = ox::max(1, ox::max(scaleSz.width, scaleSz.height));
glutils::resizeInitFrameBuffer(m_frameBuffer, core::gl::drawSize(m_scale));
}
glutils::FrameBufferBind const frameBufferBind(m_frameBuffer);
core::gl::draw(*m_cctx, m_scale);
}
glutils::FrameBuffer const&SceneEditorView::framebuffer() const noexcept {
return m_frameBuffer;
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <glutils/glutils.hpp>
#include <nostalgia/core/context.hpp>
#include <nostalgia/core/gfx.hpp>
#include <nostalgia/scene/scene.hpp>
namespace nostalgia::scene {
class SceneEditorView {
private:
core::ContextUPtr m_cctx;
SceneStatic const&m_sceneStatic;
Scene m_scene;
glutils::FrameBuffer m_frameBuffer;
int m_scale = 1;
ox::Size m_scaleSz = core::gl::drawSize(m_scale);
public:
SceneEditorView(turbine::Context &ctx, SceneStatic const&sceneStatic);
ox::Error setupScene() noexcept;
void draw(ox::Size const&targetSz) noexcept;
[[nodiscard]]
glutils::FrameBuffer const&framebuffer() const noexcept;
};
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/studio.hpp>
#include "sceneeditor-imgui.hpp"
namespace nostalgia::scene {
constexpr ox::StringLiteral FileExt_nscn("nscn");
class StudioModule: public studio::Module {
public:
ox::Vector<studio::EditorMaker> editors(turbine::Context &ctx) const noexcept override {
return {
studio::editorMaker<SceneEditorImGui>(ctx, FileExt_nscn),
};
}
ox::Vector<ox::UPtr<studio::ItemMaker>> itemMakers(turbine::Context&) const noexcept override {
ox::Vector<ox::UPtr<studio::ItemMaker>> out;
return out;
}
};
static StudioModule const mod;
const studio::Module *studioModule() noexcept {
return &mod;
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studioapp/studioapp.hpp>
#include <nostalgia/core/studiomodule.hpp>
#include <nostalgia/scene/studiomodule.hpp>
namespace nostalgia {
static bool modulesRegistered = false;
void registerStudioModules() noexcept {
if (modulesRegistered) {
return;
}
modulesRegistered = true;
studio::registerModule(core::studioModule());
studio::registerModule(scene::studioModule());
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace nostalgia {
void registerStudioModules() noexcept;
}

View File

@ -0,0 +1,35 @@
add_executable(
nostalgia WIN32
app.cpp
main.cpp
)
# enable LTO
if(NOT WIN32)
set_property(TARGET nostalgia PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
if(COMMAND OBJCOPY_FILE)
set_target_properties(nostalgia
PROPERTIES
LINK_FLAGS ${LINKER_FLAGS}
COMPILER_FLAGS "-mthumb -mthumb-interwork"
)
OBJCOPY_FILE(nostalgia)
#PADBIN_FILE(nostalgia)
endif()
target_link_libraries(
nostalgia
NostalgiaKeelModules
OxLogConn
)
install(
TARGETS
nostalgia
DESTINATION
bin
)

View File

@ -0,0 +1,160 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/media.hpp>
#include <turbine/turbine.hpp>
#include <nostalgia/core/core.hpp>
#include <nostalgia/scene/scene.hpp>
using namespace nostalgia;
static int spriteX{};
static int spriteY{};
static bool s_paused = false;
[[maybe_unused]]
static int testUpdateHandler(turbine::Context &tctx) noexcept {
auto &cctx = *turbine::applicationData<core::Context>(tctx);
constexpr auto sleepTime = 16;
if (s_paused) {
return sleepTime;
}
int xmod = 0;
int ymod = 0;
if (buttonDown(tctx, turbine::Alpha_D) || buttonDown(tctx, turbine::GamePad_Right)) {
xmod = 2;
} else if (buttonDown(tctx, turbine::Alpha_A) || buttonDown(tctx, turbine::GamePad_Left)) {
xmod = -2;
}
if (buttonDown(tctx, turbine::Alpha_S) || buttonDown(tctx, turbine::GamePad_Down)) {
ymod = 2;
} else if (buttonDown(tctx, turbine::Alpha_W) || buttonDown(tctx, turbine::GamePad_Up)) {
ymod = -2;
}
if (!xmod && !ymod) {
spriteX += 1;
}
spriteX += xmod;
spriteY += ymod;
constexpr ox::StringView sprites = "nostalgia";
for (unsigned i = 0; i < sprites.len(); ++i) {
auto const c = static_cast<unsigned>(sprites[i] - ('a' - 1));
core::setSprite(cctx, i, {
.enabled = true,
.x = spriteX + 8 * (static_cast<int>(i) + 1),
.y = spriteY,
.tileIdx = c,
.priority = 1,
});
}
return sleepTime;
}
[[maybe_unused]]
static void testKeyEventHandler(turbine::Context &tctx, turbine::Key key, bool down) noexcept {
if (down) {
if (key == turbine::Key::Alpha_Q) {
turbine::requestShutdown(tctx);
} else if (key == turbine::Key::Alpha_P) {
s_paused = !s_paused;
}
}
}
[[maybe_unused]]
static ox::Error runTest(turbine::Context &tctx) {
constexpr ox::FileAddress TileSheetAddr = ox::StringLiteral("/TileSheets/Charset.ng");
constexpr ox::FileAddress PaletteAddr = ox::StringLiteral("/Palettes/Chester.npal");
oxRequireM(cctx, core::init(tctx));
turbine::setApplicationData(tctx, cctx.get());
oxRequire(tsStat, turbine::rom(tctx)->stat(PaletteAddr));
oxReturnError(core::loadSpriteTileSheet(*cctx, TileSheetAddr));
oxReturnError(core::loadSpritePalette(*cctx, PaletteAddr));
oxReturnError(core::initConsole(*cctx));
core::puts(*cctx, 10, 9, "DOPENESS!!!");
turbine::setUpdateHandler(tctx, testUpdateHandler);
turbine::setKeyEventHandler(tctx, testKeyEventHandler);
return turbine::run(tctx);
}
[[maybe_unused]]
static ox::Error runTileSheetSetTest(turbine::Context &tctx) {
// this should make the screen display 'ABCDB'
constexpr ox::FileAddress PaletteAddr = ox::StringLiteral("/Palettes/Charset.npal");
oxRequireM(cctx, core::init(tctx));
turbine::setApplicationData(tctx, cctx.get());
oxRequire(tsStat, turbine::rom(tctx)->stat(PaletteAddr));
core::TileSheetSet const set{
.bpp = 4,
.entries = {
{ .tilesheet = ox::StringLiteral("/TileSheets/Chester.ng"), .sections{{.begin = 0, .tiles = 1}} },
{ .tilesheet = ox::StringLiteral("/TileSheets/AB.ng"), .sections{{.begin = 0, .tiles = 2}} },
{ .tilesheet = ox::StringLiteral("/TileSheets/CD.ng"), .sections{{.begin = 0, .tiles = 2}} },
{ .tilesheet = ox::StringLiteral("/TileSheets/AB.ng"), .sections{{.begin = 1, .tiles = 1}} },
},
};
oxReturnError(core::loadBgTileSheet(*cctx, 0, set));
oxReturnError(core::loadSpriteTileSheet(*cctx, set));
oxReturnError(core::loadBgPalette(*cctx, PaletteAddr));
oxReturnError(core::loadSpritePalette(*cctx, PaletteAddr));
core::setBgStatus(*cctx, 0, true);
core::setBgTile(*cctx, 0, 10, 9, 1);
core::setBgTile(*cctx, 0, 11, 9, 2);
core::setBgTile(*cctx, 0, 13, 9, 4);
core::setSprite(*cctx, 16, {
.enabled = true,
.x = 12 * 8,
.y = 9 * 8,
.tileIdx = 3,
.bpp = static_cast<unsigned>(set.bpp),
});
core::setSprite(*cctx, 17, {
.enabled = true,
.x = 14 * 8,
.y = 9 * 8,
.tileIdx = 5,
.bpp = static_cast<unsigned>(set.bpp),
});
turbine::setKeyEventHandler(tctx, testKeyEventHandler);
return turbine::run(tctx);
}
static int sceneUpdateHandler(turbine::Context&) noexcept {
constexpr auto sleepTime = 16;
if (s_paused) {
return sleepTime;
}
// do stuff
return sleepTime;
}
static void sceneKeyEventHandler(turbine::Context &tctx, turbine::Key key, bool down) noexcept {
if (down) {
if (key == turbine::Key::Alpha_Q) {
turbine::requestShutdown(tctx);
} else if (key == turbine::Key::Alpha_P) {
s_paused = !s_paused;
}
}
}
[[maybe_unused]]
static ox::Error runScene(turbine::Context &tctx) {
constexpr ox::FileAddress SceneAddr = ox::StringLiteral("/Scenes/Chester.nscn");
oxRequireM(cctx, core::init(tctx));
oxRequire(scn, keel::readObj<scene::SceneStatic>(keelCtx(tctx), SceneAddr));
turbine::setUpdateHandler(tctx, sceneUpdateHandler);
turbine::setKeyEventHandler(tctx, sceneKeyEventHandler);
scene::Scene const scene(*scn);
oxReturnError(scene.setupDisplay(*cctx));
return turbine::run(tctx);
}
ox::Error run(ox::UniquePtr<ox::FileSystem> &&fs) noexcept {
oxRequireM(tctx, turbine::init(std::move(fs), "Nostalgia"));
return runTileSheetSetTest(*tctx);
}

View File

@ -0,0 +1,8 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/memory.hpp>
#include <ox/fs/fs.hpp>
typename ox::Error run(ox::UniquePtr<ox::FileSystem> &&fs) noexcept;

View File

@ -0,0 +1,43 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/logconn/def.hpp>
#include <ox/logconn/logconn.hpp>
#include <keel/media.hpp>
#include "../modules/keelmodules.hpp"
#include "app.hpp"
static ox::Error run(int argc, const char **argv) noexcept {
#ifndef OX_BARE_METAL
// GBA doesn't need the modules and calling this doubles the size of the
// binary.
nostalgia::registerKeelModules();
#endif
if (argc < 2) {
return OxError(1, "Please provide path to project directory or OxFS file.");
}
const auto path = argv[1];
oxRequireM(fs, keel::loadRomFs(path));
return run(std::move(fs));
}
#ifdef _WIN32
int WinMain() {
auto const argc = __argc;
auto const argv = const_cast<const char**>(__argv);
#else
int main(int argc, const char **argv) {
#endif
OX_INIT_DEBUG_LOGGER(loggerConn, "Nostalgia Player")
ox::Error err;
err = run(argc, argv);
oxAssert(err, "Something went wrong...");
if (err) {
oxErrf("Failure: {}\n", toStr(err));
}
return static_cast<int>(err);
}

View File

@ -0,0 +1,34 @@
add_executable(NostalgiaStudio WIN32 MACOSX_BUNDLE)
target_link_libraries(
NostalgiaStudio
NostalgiaProfile
NostalgiaStudioModules
NostalgiaKeelModules
StudioAppLib
OlympicApplib
)
install(
TARGETS
NostalgiaStudio
RUNTIME DESTINATION
${NOSTALGIA_DIST_BIN}
BUNDLE DESTINATION .
)
if(CMAKE_BUILD_TYPE STREQUAL "Release" AND NOT WIN32)
# enable LTO
set_property(TARGET NostalgiaStudio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
if(APPLE)
set_target_properties(NostalgiaStudio PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
endif()
install(
FILES
ns.icns
DESTINATION
${NOSTALGIA_DIST_RESOURCES}/icons
)

Some files were not shown because too many files have changed in this diff Show More