From 0c866d1b9627de22194aa779a45ea32fd257e6ae Mon Sep 17 00:00:00 2001 From: Gary Talent Date: Thu, 24 Jul 2025 01:25:32 -0500 Subject: [PATCH] [studio,nostalgia/gfx] Add system for adding sub-commands in Modules, add export-tilesheet command --- .../gfx/include/nostalgia/gfx/tilesheet.hpp | 8 + .../modules/gfx/src/studio/CMakeLists.txt | 1 + .../modules/gfx/src/studio/studiomodule.cpp | 18 ++- .../gfx/src/studio/subcommands/CMakeLists.txt | 10 ++ .../export-tilesheet/export-tilesheet.cpp | 142 ++++++++++++++++++ .../export-tilesheet/export-tilesheet.hpp | 21 +++ src/nostalgia/modules/gfx/src/tilesheet.cpp | 35 +++++ .../modules/sound/src/studio/studiomodule.cpp | 6 +- .../applib/include/studioapp/studioapp.hpp | 3 + src/olympic/studio/applib/src/app.cpp | 46 +++++- src/olympic/studio/applib/src/studioui.cpp | 10 +- .../studio/modlib/include/studio/module.hpp | 25 ++- .../studio/modlib/include/studio/project.hpp | 12 +- src/olympic/studio/modlib/src/module.cpp | 4 + src/olympic/studio/modlib/src/project.cpp | 2 +- 15 files changed, 326 insertions(+), 17 deletions(-) create mode 100644 src/nostalgia/modules/gfx/src/studio/subcommands/CMakeLists.txt create mode 100644 src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.cpp create mode 100644 src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.hpp diff --git a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp index c8b5feb4..983e976b 100644 --- a/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp +++ b/src/nostalgia/modules/gfx/include/nostalgia/gfx/tilesheet.hpp @@ -428,6 +428,14 @@ TileSheet::SubSheetIdx validateSubSheetIdx(TileSheet const&ts, TileSheet::SubShe ox::Result getSubSheetIdx(TileSheet const &ts, SubSheetId pId) noexcept; +ox::Result getSubSheet( + ox::SpanView const &idx, + TileSheet &ts) noexcept; + +ox::Result getSubSheet( + ox::SpanView const &idx, + TileSheet const &ts) noexcept; + [[nodiscard]] TileSheet::SubSheet &getSubSheet( ox::SpanView const&idx, diff --git a/src/nostalgia/modules/gfx/src/studio/CMakeLists.txt b/src/nostalgia/modules/gfx/src/studio/CMakeLists.txt index 75c008cf..c6201fac 100644 --- a/src/nostalgia/modules/gfx/src/studio/CMakeLists.txt +++ b/src/nostalgia/modules/gfx/src/studio/CMakeLists.txt @@ -26,3 +26,4 @@ install( add_subdirectory(paletteeditor) add_subdirectory(tilesheeteditor) +add_subdirectory(subcommands) diff --git a/src/nostalgia/modules/gfx/src/studio/studiomodule.cpp b/src/nostalgia/modules/gfx/src/studio/studiomodule.cpp index 58b03a20..d6e7681a 100644 --- a/src/nostalgia/modules/gfx/src/studio/studiomodule.cpp +++ b/src/nostalgia/modules/gfx/src/studio/studiomodule.cpp @@ -8,10 +8,25 @@ #include "paletteeditor/paletteeditor-imgui.hpp" #include "tilesheeteditor/tilesheeteditor-imgui.hpp" +#include "subcommands/export-tilesheet/export-tilesheet.hpp" namespace nostalgia::gfx { -static class: public studio::Module { +static struct: studio::Module { + + ox::String id() const noexcept final { + return ox::String{"net.drinkingtea.nostalgia.gfx"}; + } + + ox::Vector commands() const final { + return { + { + "export-tilesheet", + cmdExportTilesheet, + } + }; + } + ox::Vector editors(studio::Context &ctx) const noexcept final { return { studio::editorMaker(ctx, {FileExt_ng, FileExt_nts}), @@ -28,6 +43,7 @@ static class: public studio::Module { }, ox::ClawFormat::Organic)); return out; } + } const mod; studio::Module const *studioModule() noexcept { diff --git a/src/nostalgia/modules/gfx/src/studio/subcommands/CMakeLists.txt b/src/nostalgia/modules/gfx/src/studio/subcommands/CMakeLists.txt new file mode 100644 index 00000000..64d97066 --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/subcommands/CMakeLists.txt @@ -0,0 +1,10 @@ + +target_sources( + NostalgiaGfx-Studio PRIVATE + export-tilesheet/export-tilesheet.cpp +) + +target_link_libraries( + NostalgiaGfx-Studio PUBLIC + OxClArgs +) \ No newline at end of file diff --git a/src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.cpp b/src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.cpp new file mode 100644 index 00000000..4eff3854 --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.cpp @@ -0,0 +1,142 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#include + +#include +#include + +#include + +#include +#include + +#include "export-tilesheet.hpp" + +#include "studio/context.hpp" + + +namespace nostalgia::gfx { + +static ox::Vector normalizePixelSizes( + ox::Vector const &inPixels) noexcept { + ox::Vector outPixels; + outPixels.reserve(inPixels.size()); + outPixels.resize(inPixels.size()); + for (size_t i{}; i < inPixels.size(); ++i) { + outPixels[i] = inPixels[i]; + } + return outPixels; +} + +static ox::Vector normalizePixelArrangement( + ox::Vector const &inPixels, + int const cols, + int const scale) { + auto const scalePt = ox::Point{scale, scale}; + auto const width = cols * TileWidth; + auto const height = static_cast(inPixels.size()) / width; + auto const dstWidth = width * scale; + ox::Vector outPixels(static_cast((width * scale) * (height * scale))); + for (size_t dstIdx{}; dstIdx < outPixels.size(); ++dstIdx) { + auto const dstPt = ox::Point{ + static_cast(dstIdx) % dstWidth, + static_cast(dstIdx) / dstWidth}; + auto const srcPt = dstPt / scalePt; + auto const srcIdx = ptToIdx(srcPt, cols); + outPixels[dstIdx] = inPixels[srcIdx]; + } + return outPixels; +} + +static ox::Error toPngFile( + ox::CStringViewCR path, + ox::Vector &&pixels, + Palette const &pal, + size_t const page, + unsigned const width, + unsigned const height) noexcept { + for (auto &c : pixels) { + c = color32(color(pal, page, c)) | static_cast(0XFF << 24); + } + constexpr auto fmt = LCT_RGBA; + return ox::Error(static_cast( + lodepng_encode_file( + path.c_str(), + reinterpret_cast(pixels.data()), + width, + height, + fmt, + 8))); +} + +ox::Error exportSubsheetToPng( + TileSheet const &src, + Palette const &pal, + ox::StringViewCR subsheetPath, + size_t const palPage, + ox::CStringViewCR dstPath, + int const scale) noexcept { + // subsheet to png + auto const [s, ssErr] = getSubSheet( + ox::split(subsheetPath, '.'), src); + if (ssErr) { + return ox::Error{6, "failed to find SubSheet"}; + } + auto const width = s->columns * TileWidth; + auto const height = s->rows * TileHeight; + auto const err = toPngFile( + dstPath, + normalizePixelArrangement( + normalizePixelSizes(s->pixels), + s->columns, + scale), + pal, + palPage, + static_cast(width * scale), + static_cast(height * scale)); + if (err) { + oxErrorf("TileSheet export failed: {}", toStr(err)); + return ox::Error{7, "TileSheet export failed"}; + } + return {}; +} + +ox::Error cmdExportTilesheet(studio::Project &project, ox::SpanView const args) noexcept { + // parse args + ox::ClArgs const clargs{args}; + bool showUsage = true; + OX_DEFER [&showUsage] { + if (showUsage) { + oxErr("usage: export-tilesheet " + "-src-path " + "-dst-path " + "[-pal-path ] " + "[-subsheet-path ] " + "[-scale ]\n"); + } + }; + OX_REQUIRE(srcPath, clargs.getString("src-path").transformError(1, "no src path specified")); + OX_REQUIRE(dstPath, clargs.getString("dst-path").transformError(2, "no dst path specified")); + auto const palPath = clargs.getString("pal-path", ""); + auto const subsheetPath = clargs.getString("subsheet-path").or_value({}); + auto const palPage = static_cast(clargs.getInt("pal-page", 0)); + auto const scale = clargs.getInt("scale", 1); + showUsage = false; + // load objects + auto &kctx = project.kctx(); + OX_REQUIRE(ts, keel::readObj(kctx, srcPath).transformError(4, "could not load TileSheet")); + OX_REQUIRE(pal, keel::readObj(kctx, palPath.len() ? palPath : ts->defaultPalette) + .transformError(5, "could not load Palette")); + // export to the destination file + return exportSubsheetToPng( + *ts, + *pal, + subsheetPath, + palPage, + dstPath, + scale); +} + +} diff --git a/src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.hpp b/src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.hpp new file mode 100644 index 00000000..0224619f --- /dev/null +++ b/src/nostalgia/modules/gfx/src/studio/subcommands/export-tilesheet/export-tilesheet.hpp @@ -0,0 +1,21 @@ +/* + * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. + */ + +#pragma once + +#include + +namespace nostalgia::gfx { + +ox::Error exportSubsheetToPng( + TileSheet const &src, + Palette const &pal, + ox::StringViewCR subsheetPath, + size_t palPage, + ox::CStringViewCR dstPath, + int scale) noexcept; + +ox::Error cmdExportTilesheet(studio::Project& project, ox::SpanView args) noexcept; + +} diff --git a/src/nostalgia/modules/gfx/src/tilesheet.cpp b/src/nostalgia/modules/gfx/src/tilesheet.cpp index 37909190..3853cddc 100644 --- a/src/nostalgia/modules/gfx/src/tilesheet.cpp +++ b/src/nostalgia/modules/gfx/src/tilesheet.cpp @@ -211,6 +211,41 @@ ox::Result getSubSheetIdx(TileSheet const &ts, SubSheetI return out; } + +template +static ox::Result getSubSheet( + ox::SpanView const &idx, + std::size_t const idxIt, + SubSheet &pSubSheet) noexcept { + if (idxIt == idx.size()) { + return &pSubSheet; + } + auto const ¤tIdx = idx[idxIt]; + auto const next = ox::find_if( + pSubSheet.subsheets.begin(), + pSubSheet.subsheets.end(), + [¤tIdx](TileSheet::SubSheet const &ss) { + return ss.name == currentIdx; + }); + if (next == pSubSheet.subsheets.end()) { + return ox::Error{1, "SubSheet not found"}; + } + return getSubSheet(idx, idxIt + 1, *next); +} + +ox::Result getSubSheet( + ox::SpanView const &idx, + TileSheet const &ts) noexcept { + return getSubSheet(idx, 1, ts.subsheet); +} + +ox::Result getSubSheet( + ox::SpanView const &idx, + TileSheet &ts) noexcept { + return getSubSheet(idx, 1, ts.subsheet); +} + + #if defined(__GNUC__) && __GNUC__ >= 13 #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdangling-reference" diff --git a/src/nostalgia/modules/sound/src/studio/studiomodule.cpp b/src/nostalgia/modules/sound/src/studio/studiomodule.cpp index 0806dfe0..93eafd1a 100644 --- a/src/nostalgia/modules/sound/src/studio/studiomodule.cpp +++ b/src/nostalgia/modules/sound/src/studio/studiomodule.cpp @@ -10,6 +10,10 @@ namespace nostalgia::sound { static struct: studio::Module { + ox::String id() const noexcept final { + return ox::String{"net.drinkingtea.nostalgia.sound"}; + } + ox::Vector editors(studio::Context&) const noexcept final { return { }; @@ -22,7 +26,7 @@ static struct: studio::Module { } const mod; -const studio::Module *studioModule() noexcept { +studio::Module const *studioModule() noexcept { return &mod; } diff --git a/src/olympic/studio/applib/include/studioapp/studioapp.hpp b/src/olympic/studio/applib/include/studioapp/studioapp.hpp index 8defebe1..ec9ec5c1 100644 --- a/src/olympic/studio/applib/include/studioapp/studioapp.hpp +++ b/src/olympic/studio/applib/include/studioapp/studioapp.hpp @@ -10,6 +10,9 @@ namespace studio { +[[nodiscard]] +ox::Vector const &modules() noexcept; + void registerModule(Module const*) noexcept; } diff --git a/src/olympic/studio/applib/src/app.cpp b/src/olympic/studio/applib/src/app.cpp index d3010860..6729bc12 100644 --- a/src/olympic/studio/applib/src/app.cpp +++ b/src/olympic/studio/applib/src/app.cpp @@ -2,15 +2,18 @@ * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved. */ +#include #include #include #include #include -#include +#include #include #include +#include + #include "studioui.hpp" namespace studio { @@ -40,7 +43,7 @@ static void mouseButtonEventHandler(turbine::Context &ctx, int const btn, bool c [[nodiscard]] ox::Vector> WindowIcons() noexcept; -static ox::Error runApp( +static ox::Error runStudio( ox::StringViewCR appName, ox::StringViewCR projectDataDir, ox::UPtr &&fs) noexcept { @@ -61,17 +64,46 @@ static ox::Error runApp( static ox::Error run( ox::StringViewCR appName, ox::StringViewCR projectDataDir, - ox::SpanView) { + ox::SpanView const &args) { // seed UUID generator auto const time = std::time(nullptr); ox::UUID::seedGenerator({ static_cast(time), static_cast(time << 1) }); - // run app - auto const err = runApp(appName, projectDataDir, ox::UPtr{}); - oxAssert(err, "Something went wrong..."); - return err; + if (args.size() > 1 && ox::StringView{args[1]} == "cmd") { + if (args.size() < 5) { + return ox::Error{1, "insufficient arguments for sub-command"}; + } + auto constexpr numCmdArgs = 5; + ox::StringView const projectDir = args[2]; + ox::StringView const moduleId = args[3]; + ox::StringView const subCmd = args[4]; + for (auto const m : modules()) { + if (m->id() == moduleId) { + for (auto const &c : m->commands()) { + if (c.name == subCmd) { + auto kctx = keel::init( + ox::make_unique(projectDir), + c.name); + if (kctx.error) { + return ox::Error{2, "failed to load project directory"}; + } + Project project{*kctx.value, projectDir, projectDataDir}; + return c.func(project, args + numCmdArgs); + } + } + return ox::Error{1, "command not found"}; + } + } + return ox::Error{1, "module not found"}; + } else { + // run app + auto const err = runStudio(appName, projectDataDir, ox::UPtr{}); + oxAssert(err, "Something went wrong..."); + return err; + } + return {}; } } diff --git a/src/olympic/studio/applib/src/studioui.cpp b/src/olympic/studio/applib/src/studioui.cpp index 04c97e88..16ecb968 100644 --- a/src/olympic/studio/applib/src/studioui.cpp +++ b/src/olympic/studio/applib/src/studioui.cpp @@ -37,11 +37,15 @@ static bool shutdownHandler(turbine::Context &ctx) { namespace ig { extern bool s_mainWinHasFocus; } -static ox::Vector modules; +static ox::Vector g_modules; + +ox::Vector const &modules() noexcept { + return g_modules; +} void registerModule(Module const*mod) noexcept { if (mod) { - modules.emplace_back(mod); + g_modules.emplace_back(mod); } } @@ -443,7 +447,7 @@ void StudioUI::loadModule(Module const &mod) noexcept { } void StudioUI::loadModules() noexcept { - for (auto const mod : modules) { + for (auto const mod : g_modules) { loadModule(*mod); } } diff --git a/src/olympic/studio/modlib/include/studio/module.hpp b/src/olympic/studio/modlib/include/studio/module.hpp index dbefd616..5434d103 100644 --- a/src/olympic/studio/modlib/include/studio/module.hpp +++ b/src/olympic/studio/modlib/include/studio/module.hpp @@ -23,15 +23,34 @@ struct EditorMaker { Func make; }; +struct Command { + ox::String name; + std::function)> func; + Command( + ox::StringParam name, + std::function)> func) noexcept: + name(std::move(name)), + func(std::move(func)) {} +}; + class Module { public: virtual ~Module() noexcept = default; - virtual ox::Vector editors(studio::Context &ctx) const; + [[nodiscard]] + virtual ox::String id() const noexcept = 0; - virtual ox::Vector> itemMakers(studio::Context&) const; + [[nodiscard]] + virtual ox::Vector commands() const; - virtual ox::Vector> itemTemplates(studio::Context&) const; + [[nodiscard]] + virtual ox::Vector editors(Context &ctx) const; + + [[nodiscard]] + virtual ox::Vector> itemMakers(Context&) const; + + [[nodiscard]] + virtual ox::Vector> itemTemplates(Context&) const; }; diff --git a/src/olympic/studio/modlib/include/studio/project.hpp b/src/olympic/studio/modlib/include/studio/project.hpp index dda1eabf..f730e8b9 100644 --- a/src/olympic/studio/modlib/include/studio/project.hpp +++ b/src/olympic/studio/modlib/include/studio/project.hpp @@ -59,7 +59,7 @@ class Project: public ox::SignalHandler { ox::HashMap> m_fileExtFileMap; public: - explicit Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir); + explicit Project(keel::Context &ctx, ox::StringParam path, ox::StringViewCR projectDataDir); ox::Error create() noexcept; @@ -101,6 +101,16 @@ class Project: public ox::SignalHandler { ox::Error deleteItem(ox::StringViewCR path) noexcept; + [[nodiscard]] + keel::Context &kctx() noexcept { + return m_kctx; + } + + [[nodiscard]] + keel::Context const &kctx() const noexcept { + return m_kctx; + } + [[nodiscard]] bool exists(ox::StringViewCR path) const noexcept; diff --git a/src/olympic/studio/modlib/src/module.cpp b/src/olympic/studio/modlib/src/module.cpp index 9f57342e..2398d9aa 100644 --- a/src/olympic/studio/modlib/src/module.cpp +++ b/src/olympic/studio/modlib/src/module.cpp @@ -6,6 +6,10 @@ namespace studio { +ox::Vector Module::commands() const { + return {}; +} + ox::Vector Module::editors(Context&) const { return {}; } diff --git a/src/olympic/studio/modlib/src/project.cpp b/src/olympic/studio/modlib/src/project.cpp index 0ccb2f48..73122252 100644 --- a/src/olympic/studio/modlib/src/project.cpp +++ b/src/olympic/studio/modlib/src/project.cpp @@ -54,7 +54,7 @@ static constexpr bool isParentOf(ox::StringViewCR parent, ox::StringViewCR child return beginsWith(sfmt("{}/", child), parent); } -Project::Project(keel::Context &ctx, ox::String path, ox::StringViewCR projectDataDir): +Project::Project(keel::Context &ctx, ox::StringParam path, ox::StringViewCR projectDataDir): m_kctx(ctx), m_path(std::move(path)), m_projectDataDir(projectDataDir),