Merge commit '587dd924142c959b812ca340eab52af35ac4096c' as 'deps/nostalgia'

This commit is contained in:
2023-12-23 14:17:05 -06:00
1112 changed files with 283489 additions and 0 deletions

View File

@ -0,0 +1,21 @@
if(BUILDCORE_TARGET STREQUAL "gba")
project(Olympic ASM CXX)
else()
project(Olympic CXX)
endif()
set(OLYMPIC_BUILD_STUDIO ON CACHE BOOL "Build Studio")
if(BUILDCORE_TARGET STREQUAL "gba")
set(OLYMPIC_BUILD_STUDIO OFF)
set(TURBINE_BUILD_TYPE "GBA")
else()
set(TURBINE_BUILD_TYPE "Native")
endif()
add_subdirectory(applib)
add_subdirectory(keel)
add_subdirectory(turbine)
if(${OLYMPIC_BUILD_STUDIO})
add_subdirectory(studio)
endif()

View File

@ -0,0 +1,9 @@
add_library(
OlympicApplib INTERFACE
)
target_sources(
OlympicApplib INTERFACE
applib.cpp
)

View File

@ -0,0 +1,68 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/logconn/def.hpp>
#include <ox/logconn/logconn.hpp>
#ifndef OLYMPIC_PROJECT_NAME
#define OLYMPIC_PROJECT_NAME "OlympicProject"
#endif
#ifndef OLYMPIC_APP_NAME
#define OLYMPIC_APP_NAME "App"
#endif
#ifndef OLYMPIC_PROJECT_NAMESPACE
#define OLYMPIC_PROJECT_NAMESPACE project
#endif
#ifndef OLYMPIC_PROJECT_DATADIR
#define OLYMPIC_PROJECT_DATADIR ".keel"
#endif
#ifndef OLYMPIC_LOAD_KEEL_MODULES
#define OLYMPIC_LOAD_KEEL_MODULES 1
#endif
#ifndef OLYMPIC_LOAD_STUDIO_MODULES
#define OLYMPIC_LOAD_STUDIO_MODULES 0
#endif
namespace olympic {
ox::Error run(
ox::StringView project,
ox::StringView appName,
ox::StringView projectDataDir,
int argc,
char const**argv) noexcept;
}
namespace OLYMPIC_PROJECT_NAMESPACE {
void registerKeelModules() noexcept;
void registerStudioModules() noexcept;
}
#ifdef _WIN32
int WinMain() {
auto const argc = __argc;
auto const argv = const_cast<const char**>(__argv);
#else
int main(int argc, char const**argv) {
#endif
OX_INIT_DEBUG_LOGGER(loggerConn, OLYMPIC_PROJECT_NAME " " OLYMPIC_APP_NAME)
#if OLYMPIC_LOAD_KEEL_MODULES
OLYMPIC_PROJECT_NAMESPACE::registerKeelModules();
#endif
#if OLYMPIC_LOAD_STUDIO_MODULES
OLYMPIC_PROJECT_NAMESPACE::registerStudioModules();
#endif
auto const err = olympic::run(OLYMPIC_PROJECT_NAME, OLYMPIC_APP_NAME, OLYMPIC_PROJECT_DATADIR, argc, argv);
oxAssert(err, "Something went wrong...");
if (err) {
oxErrf("Failure: {}\n", toStr(err));
}
return static_cast<int>(err);
}

View File

@ -0,0 +1,5 @@
add_subdirectory(src)
if(TURBINE_BUILD_TYPE STREQUAL "Native")
add_subdirectory(test)
endif()

View File

@ -0,0 +1,45 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <ox/fs/fs.hpp>
namespace keel {
constexpr auto K1HdrSz = 40;
ox::Result<ox::UUID> readUuidHeader(ox::Buffer const&buff) noexcept;
ox::Result<ox::UUID> readUuidHeader(const char *buff, std::size_t buffLen) noexcept;
ox::Error writeUuidHeader(ox::Writer_c auto &writer, ox::UUID const&uuid) noexcept {
oxReturnError(write(writer, "K1;"));
oxReturnError(uuid.toString(writer));
return writer.put(';');
}
template<typename T>
ox::Result<T> readAsset(ox::Buffer const&buff) noexcept {
std::size_t offset = 0;
const auto err = readUuidHeader(buff).error;
if (!err) {
offset = K1HdrSz; // the size of K1 headers
}
return ox::readClaw<T>(buff.data() + offset, buff.size() - offset);
}
ox::Result<ox::ModelObject> readAsset(ox::TypeStore &ts, ox::Buffer const&buff) noexcept;
struct AssetHdr {
ox::UUID uuid;
ox::ClawHeader clawHdr;
};
ox::Result<AssetHdr> readAssetHeader(char const*buff, std::size_t buffLen) noexcept;
ox::Result<AssetHdr> readAssetHeader(ox::Buffer const&buff) noexcept;
}

View File

@ -0,0 +1,301 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/fs/fs.hpp>
#include <ox/model/typenamecatcher.hpp>
#include <ox/std/hashmap.hpp>
#include <ox/std/utility.hpp>
namespace keel {
class AssetManager;
template<typename T>
class AssetRef;
#ifndef OX_BARE_METAL
template<typename T>
class AssetContainer {
friend AssetManager;
friend AssetRef<T>;
protected:
ox::Signal<ox::Error()> updated;
private:
T m_obj;
mutable int m_references = 0;
public:
template<class... Args>
explicit constexpr AssetContainer(Args&&... args): m_obj(ox::forward<Args>(args)...) {
}
AssetContainer(AssetContainer const&) = delete;
AssetContainer(AssetContainer&&) = delete;
AssetContainer &operator=(AssetContainer const&) = delete;
AssetContainer &operator=(AssetContainer&&) = delete;
[[nodiscard]]
constexpr T *get() noexcept {
return &m_obj;
}
[[nodiscard]]
constexpr T const*get() const noexcept {
return &m_obj;
}
constexpr void set(T &&val) {
m_obj = std::move(val);
}
constexpr void set(T const&val) {
m_obj = val;
}
protected:
constexpr void incRefs() const noexcept {
++m_references;
}
constexpr void decRefs() const noexcept {
--m_references;
}
[[nodiscard]]
constexpr int references() const noexcept {
return m_references;
}
};
template<typename T>
class AssetRef: public ox::SignalHandler {
private:
AssetContainer<T> const*m_ctr = nullptr;
public:
ox::Signal<ox::Error()> updated;
explicit constexpr AssetRef(AssetContainer<T> const*c = nullptr) noexcept;
constexpr AssetRef(AssetRef const&h) noexcept;
constexpr AssetRef(AssetRef &&h) noexcept;
~AssetRef() noexcept override {
if (m_ctr) {
m_ctr->decRefs();
}
}
[[nodiscard]]
constexpr T const*get() const noexcept {
if (m_ctr) {
return m_ctr->get();
}
return nullptr;
}
constexpr T const&operator*() const noexcept {
return *m_ctr->get();
}
constexpr T const*operator->() const noexcept {
return m_ctr->get();
}
AssetRef &operator=(AssetRef const&h) noexcept {
if (this == &h) {
return *this;
}
if (m_ctr) {
m_ctr->decRefs();
oxIgnoreError(m_ctr->updated.disconnectObject(this));
}
m_ctr = h.m_ctr;
if (m_ctr) {
m_ctr->updated.connect(&updated, &ox::Signal<ox::Error()>::emitCheckError);
m_ctr->incRefs();
}
return *this;
}
AssetRef &operator=(AssetRef &&h) noexcept {
if (this == &h) {
return *this;
}
if (m_ctr) {
m_ctr->decRefs();
oxIgnoreError(m_ctr->updated.disconnectObject(this));
}
m_ctr = h.m_ctr;
if (m_ctr) {
oxIgnoreError(m_ctr->updated.disconnectObject(&h));
m_ctr->updated.connect(this, &AssetRef::emitUpdated);
}
h.m_ctr = nullptr;
return *this;
}
explicit constexpr operator bool() const noexcept {
return m_ctr;
}
private:
constexpr ox::Error emitUpdated() const noexcept {
updated.emit();
return OxError(0);
}
};
template<typename T>
constexpr AssetRef<T>::AssetRef(AssetContainer<T> const*c) noexcept: m_ctr(c) {
if (m_ctr) {
m_ctr->updated.connect(this, &AssetRef::emitUpdated);
}
}
template<typename T>
constexpr AssetRef<T>::AssetRef(AssetRef const&h) noexcept: m_ctr(h.m_ctr) {
if (m_ctr) {
m_ctr->updated.connect(this, &AssetRef::emitUpdated);
m_ctr->incRefs();
}
}
template<typename T>
constexpr AssetRef<T>::AssetRef(AssetRef &&h) noexcept: m_ctr(h.m_ctr) {
if (m_ctr) {
m_ctr->updated.connect(this, &AssetRef::emitUpdated);
}
h.m_ctr = nullptr;
}
class AssetManager {
private:
class AssetTypeManagerBase {
public:
virtual ~AssetTypeManagerBase() = default;
virtual void gc() noexcept = 0;
};
template<typename T>
class AssetTypeManager: public AssetTypeManagerBase {
private:
ox::HashMap<ox::String, ox::UPtr<AssetContainer<T>>> m_cache;
public:
ox::Result<AssetRef<T>> getAsset(ox::StringView const&assetId) const noexcept {
auto out = m_cache.at(assetId);
oxReturnError(out);
return AssetRef<T>(out.value->get());
}
ox::Result<AssetRef<T>> setAsset(ox::StringView const&assetId, T const&obj) noexcept {
auto &p = m_cache[assetId];
if (!p) {
p = ox::make_unique<AssetContainer<T>>(obj);
} else {
p->set(obj);
p->updated.emit();
}
return AssetRef<T>(p.get());
}
ox::Result<AssetRef<T>> setAsset(ox::StringView const&assetId, T &&obj) noexcept {
auto &p = m_cache[assetId];
if (!p) {
p = ox::make_unique<AssetContainer<T>>(obj);
} else {
p->set(std::move(obj));
p->updated.emit();
}
return AssetRef<T>(p.get());
}
void gc() noexcept final {
for (auto const&ack : m_cache.keys()) {
auto &ac = m_cache[ack];
if (!ac->references()) {
m_cache.erase(ack);
}
}
}
};
ox::HashMap<ox::String, ox::UPtr<AssetTypeManagerBase>> m_assetTypeManagers;
template<typename T>
AssetTypeManager<T> *getTypeManager() noexcept {
constexpr auto typeName = ox::requireModelTypeName<T>();
static_assert(ox::StringView(typeName) != "", "Types must have TypeName to use AssetManager");
auto &am = m_assetTypeManagers[typeName];
if (!am) {
am = ox::make_unique<AssetTypeManager<T>>();
}
return dynamic_cast<AssetTypeManager<T>*>(am.get());
}
public:
template<typename T>
ox::Result<AssetRef<T>> getAsset(ox::CRStringView assetId) noexcept {
auto m = getTypeManager<T>();
return m->getAsset(assetId);
}
template<typename T>
ox::Result<AssetRef<T>> setAsset(ox::CRStringView assetId, T const&obj) noexcept {
auto m = getTypeManager<T>();
return m->setAsset(assetId, obj);
}
void gc() noexcept {
for (auto const&amk : m_assetTypeManagers.keys()) {
auto &am = m_assetTypeManagers[amk];
am->gc();
}
}
};
#else
template<typename T>
class AssetRef {
private:
T const*const m_obj = nullptr;
public:
constexpr AssetRef() noexcept = default;
explicit constexpr AssetRef(T const*obj) noexcept: m_obj(obj) {
}
constexpr T const*get() const noexcept {
return m_obj;
}
constexpr T const&operator*() const & noexcept {
return *m_obj;
}
constexpr T const&&operator*() const && noexcept {
return *m_obj;
}
constexpr T const*operator->() const noexcept {
return m_obj;
}
explicit constexpr operator bool() const noexcept {
return m_obj;
}
};
#endif
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/std/memory.hpp>
#include "assetmanager.hpp"
namespace keel {
class Context;
using PackTransform = ox::Error(*)(Context&, ox::Buffer &clawData);
class Context {
public:
ox::UPtr<ox::FileSystem> rom;
ox::BasicString<32> appName{"Keel App"};
#ifndef OX_BARE_METAL
AssetManager assetManager;
ox::HashMap<ox::String, ox::UUID> pathToUuid;
ox::HashMap<ox::UUIDStr, ox::String> uuidToPath;
ox::Vector<class BaseConverter const*> converters;
ox::Vector<PackTransform> packTransforms;
#else
std::size_t preloadSectionOffset = 0;
#endif
constexpr Context() noexcept = default;
Context(Context const&) noexcept = delete;
Context(Context&&) noexcept = delete;
Context &operator=(Context const&) noexcept = delete;
Context &operator=(Context&&) noexcept = delete;
constexpr virtual ~Context() noexcept = default;
};
}

View File

@ -0,0 +1,26 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/memory.hpp>
#include "context.hpp"
#include "media.hpp"
#include "module.hpp"
#include "pack.hpp"
#include "typestore.hpp"
namespace keel {
ox::Error init(
keel::Context &ctx,
ox::UPtr<ox::FileSystem> &&fs,
ox::CRStringView appName) noexcept;
ox::Result<ox::UPtr<Context>> init(
ox::UPtr<ox::FileSystem> &&fs,
ox::CRStringView appName) noexcept;
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/defines.hpp>
#include <ox/claw/claw.hpp>
#include <ox/fs/fs.hpp>
#include <ox/model/metadata.hpp>
#include "asset.hpp"
#include "context.hpp"
#include "typeconv.hpp"
namespace keel {
// Pointer to preloaded data that can be stored in FS in place of the actual
// data.
struct PreloadPtr {
static constexpr auto TypeName = "net.drinkingtea.keel.PreloadPtr";
static constexpr auto TypeVersion = 2;
uint64_t preloadAddr = 0;
};
oxModelBegin(PreloadPtr)
oxModelField(preloadAddr)
oxModelEnd()
ox::Result<std::size_t> getPreloadAddr(keel::Context &ctx, ox::FileAddress const&file) noexcept;
ox::Result<std::size_t> getPreloadAddr(keel::Context &ctx, ox::CRStringView file) noexcept;
#ifndef OX_BARE_METAL
template<typename T>
ox::Result<keel::AssetRef<T>> readObjFile(
keel::Context &ctx,
ox::StringView assetId,
bool forceLoad) noexcept {
constexpr auto readConvert = [](Context &ctx, const ox::Buffer &buff) -> ox::Result<T> {
auto [obj, err] = readAsset<T>(buff);
if (err) {
if (err != ox::Error_ClawTypeVersionMismatch && err != ox::Error_ClawTypeMismatch) {
return err;
}
oxReturnError(convert<T>(ctx, buff, &obj));
}
return std::move(obj);
};
ox::StringView path;
if (beginsWith(assetId, "uuid://")) {
assetId = substr(assetId, 7);
oxRequire(p, ctx.uuidToPath.at(assetId));
path = *p;
} else {
path = assetId;
}
if (forceLoad) {
oxRequire(buff, ctx.rom->read(path));
oxRequire(obj, readConvert(ctx, buff));
oxRequire(cached, ctx.assetManager.setAsset(assetId, obj));
return cached;
} else {
auto [cached, err] = ctx.assetManager.getAsset<T>(assetId);
if (err) {
oxRequire(buff, ctx.rom->read(path));
oxRequire(obj, readConvert(ctx, buff));
oxReturnError(ctx.assetManager.setAsset(assetId, obj).moveTo(cached));
}
return cached;
}
}
#else
template<typename T>
ox::Result<keel::AssetRef<T>> readObjNoCache(
keel::Context &ctx,
ox::CRStringView assetId) noexcept {
if constexpr(ox::preloadable<T>::value) {
oxRequire(addr, getPreloadAddr(ctx, assetId));
return keel::AssetRef<T>(std::bit_cast<T const*>(uintptr_t{addr}));
} else {
return OxError(1);
}
}
#endif
void createUuidMapping(Context &ctx, ox::StringView filePath, ox::UUID const&uuid) noexcept;
ox::Error buildUuidMap(Context &ctx) noexcept;
ox::Result<ox::String> uuidToPath(Context &ctx, ox::CRStringView uuid) noexcept;
ox::Result<ox::String> uuidToPath(Context &ctx, ox::UUID const&uuid) noexcept;
ox::Error performPackTransforms(Context &ctx, ox::Buffer &clawData) noexcept;
template<typename T>
ox::Result<AssetRef<T>> setAsset(keel::Context &ctx, ox::StringView assetId, T const&asset) noexcept {
#ifndef OX_BARE_METAL
if (assetId.len() == 0) {
return OxError(1, "Invalid asset ID");
}
ox::UUIDStr idStr;
if (assetId[0] == '/') {
auto const [id, err] = ctx.pathToUuid.at(assetId);
oxReturnError(err);
idStr = id->toString();
assetId = idStr;
}
return ctx.assetManager.setAsset(assetId, asset);
#else
return OxError(1, "Not supported on this platform");
#endif
}
template<typename T>
ox::Result<keel::AssetRef<T>> readObj(
keel::Context &ctx,
ox::CRStringView assetId,
[[maybe_unused]] bool forceLoad = false) noexcept {
#ifndef OX_BARE_METAL
return readObjFile<T>(ctx, assetId, forceLoad);
#else
return readObjNoCache<T>(ctx, assetId);
#endif
}
template<typename T>
ox::Result<keel::AssetRef<T>> readObj(
keel::Context &ctx,
ox::FileAddress const&file,
[[maybe_unused]] bool forceLoad = false) noexcept {
#ifndef OX_BARE_METAL
oxRequire(assetId, file.getPath());
return readObj<T>(ctx, ox::StringView(assetId), forceLoad);
#else
if constexpr(ox::preloadable<T>::value) {
oxRequire(addr, getPreloadAddr(ctx, file));
return keel::AssetRef<T>(std::bit_cast<T const*>(uintptr_t{addr}));
} else {
return OxError(1);
}
#endif
}
template<typename T>
ox::Error writeObj(
keel::Context &ctx,
ox::FileAddress const&file,
T const&obj,
ox::ClawFormat fmt = ox::ClawFormat::Metal) noexcept {
oxRequire(objBuff, ox::writeClaw(obj, fmt));
return ctx.rom->write(file, objBuff.data(), objBuff.size());
}
ox::Error setRomFs(Context &ctx, ox::UPtr<ox::FileSystem> &&fs) noexcept;
ox::Result<ox::UniquePtr<ox::FileSystem>> loadRomFs(ox::CRStringView path) noexcept;
ox::Result<char*> loadRom(ox::CRStringView path = "") noexcept;
void unloadRom(char*) noexcept;
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/vector.hpp>
#include <ox/model/descwrite.hpp>
#include "typeconv.hpp"
namespace keel {
using TypeDescGenerator = ox::Error(*)(ox::TypeStore&);
template<typename T>
ox::Error generateTypeDesc(ox::TypeStore &ts) noexcept {
return ox::buildTypeDef<T>(&ts).error;
}
class Module {
public:
constexpr Module() noexcept = default;
Module(Module const&) noexcept = delete;
Module(Module&&) noexcept = delete;
Module &operator=(Module const&) noexcept = delete;
Module &operator=(Module&&) noexcept = delete;
constexpr virtual ~Module() noexcept = default;
[[nodiscard]]
virtual ox::String id() const noexcept = 0;
[[nodiscard]]
virtual ox::Vector<TypeDescGenerator> types() const noexcept;
[[nodiscard]]
virtual ox::Vector<const keel::BaseConverter*> converters() const noexcept;
[[nodiscard]]
virtual ox::Vector<PackTransform> packTransforms() const noexcept;
};
void registerModule(Module const*mod) noexcept;
[[nodiscard]]
ox::Vector<keel::Module const*> const&modules() noexcept;
}

View File

@ -0,0 +1,166 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/preloader/preloader.hpp>
#include "asset.hpp"
#include "media.hpp"
namespace keel {
class Context;
struct GbaPlatSpec {
using PtrType = uint32_t;
using size_t = uint32_t;
static constexpr PtrType RomStart = 0x08000000;
[[nodiscard]]
static constexpr std::size_t alignOf(const bool) noexcept {
return 1;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const uint8_t) noexcept {
return 1;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const uint16_t) noexcept {
return 2;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const uint32_t) noexcept {
return 4;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const uint64_t) noexcept {
return 8;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const int8_t) noexcept {
return 1;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const int16_t) noexcept {
return 2;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const int32_t) noexcept {
return 4;
}
[[nodiscard]]
static constexpr std::size_t alignOf(const int64_t) noexcept {
return 8;
}
[[nodiscard]]
static constexpr std::size_t alignOf(auto const*) noexcept {
return 4;
}
[[nodiscard]]
static constexpr auto correctEndianness(auto v) noexcept {
return ox::toLittleEndian(v);
}
};
using GbaPreloader = ox::Preloader<GbaPlatSpec>;
namespace detail {
// transformations need to be done after the copy to the new FS is complete
template<typename PlatSpec>
ox::Error preloadObj(
ox::TypeStore &ts,
ox::FileSystem &romFs,
ox::Preloader<PlatSpec> &pl,
ox::CRStringView path) noexcept {
// load file
oxRequireM(buff, romFs.read(path));
oxRequireM(obj, keel::readAsset(ts, buff));
if (obj.type()->preloadable) {
oxOutf("preloading {}\n", path);
// preload
oxRequire(a, pl.startAlloc(ox::sizeOf<GbaPlatSpec>(&obj)));
auto const err = ox::preload<GbaPlatSpec, decltype(obj)>(&pl, &obj);
oxReturnError(pl.endAlloc());
oxReturnError(err);
keel::PreloadPtr const p{.preloadAddr = static_cast<uint32_t>(a)};
oxReturnError(ox::writeMC(p).moveTo(buff));
} else {
// strip the Claw header (it is not needed after preloading) and write back out to dest fs
oxReturnError(ox::writeMC(obj).moveTo(buff));
}
oxReturnError(romFs.write(path, buff.data(), buff.size()));
return {};
}
// claw file transformations are broken out because path to inode
// transformations need to be done after the copy to the new FS is complete
template<typename PlatSpec>
ox::Error preloadDir(
ox::TypeStore &ts,
ox::FileSystem &romFs,
ox::Preloader<PlatSpec> &pl,
ox::CRStringView path) noexcept {
// copy
oxTracef("pack.preload", "path: {}", path);
oxRequire(fileList, romFs.ls(path));
for (auto const&name : fileList) {
auto const filePath = ox::sfmt("{}{}", path, name);
oxRequire(stat, romFs.stat(filePath));
if (stat.fileType == ox::FileType::Directory) {
auto const dir = ox::sfmt("{}{}/", path, name);
oxReturnError(preloadDir(ts, romFs, pl, dir));
} else {
oxReturnError(preloadObj(ts, romFs, pl, filePath));
}
}
return {};
}
}
template<typename PlatSpec>
ox::Error appendBinary(ox::Buffer &binBuff, ox::SpanView<char> const&fsBuff, ox::Preloader<PlatSpec> &pl) noexcept {
constexpr auto padbin = [](ox::BufferWriter &w, unsigned factor) noexcept -> ox::Error {
return w.write(nullptr, factor - w.buff().size() % factor);
};
constexpr ox::StringView mediaHdr = "KEEL_MEDIA_HEADER_______________";
constexpr ox::StringView preloadHdr = "KEEL_PRELOAD_HEADER_____________";
constexpr auto hdrSize = 32u;
static_assert(mediaHdr.bytes() == hdrSize);
static_assert(preloadHdr.bytes() == hdrSize);
ox::BufferWriter w(&binBuff);
oxReturnError(padbin(w, hdrSize));
oxReturnError(w.write(mediaHdr.data(), mediaHdr.bytes()));
oxReturnError(w.write(fsBuff.data(), fsBuff.size()));
oxReturnError(padbin(w, hdrSize));
oxReturnError(w.write(preloadHdr.data(), preloadHdr.bytes()));
oxReturnError(pl.offsetPtrs(binBuff.size()));
auto const&plBuff = pl.buff();
oxReturnError(w.write(plBuff.data(), plBuff.size()));
return {};
}
template<typename PlatSpec>
ox::Error preload(ox::TypeStore &ts, ox::FileSystem &src, ox::Preloader<PlatSpec> &pl) noexcept {
oxOut("Preloading\n");
return detail::preloadDir(ts, src, pl, "/");
}
ox::Error pack(keel::Context &ctx, ox::TypeStore &ts, ox::FileSystem &dest) noexcept;
}

View File

@ -0,0 +1,170 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/def.hpp>
#include <ox/std/error.hpp>
#include <ox/std/string.hpp>
#include <ox/claw/read.hpp>
#include <ox/claw/write.hpp>
#include "asset.hpp"
#include "context.hpp"
namespace keel {
class Wrap {
public:
virtual ~Wrap() = default;
};
template<typename T>
class WrapInline: public Wrap {
private:
T m_obj;
public:
constexpr WrapInline() = default;
template<typename... Args>
constexpr explicit WrapInline(Args &&...args): m_obj(ox::forward<Args>(args)...) {
}
[[nodiscard]]
constexpr T &obj() noexcept {
return m_obj;
}
};
template<typename T, typename... Args>
constexpr auto makeWrap(Args &&...args) noexcept {
return ox::make_unique<WrapInline<T>>(ox::forward<Args>(args)...);
}
template<typename T>
constexpr T &wrapCast(Wrap &ptr) noexcept {
return static_cast<WrapInline<T>&>(ptr).obj();
}
class BaseConverter {
public:
virtual ~BaseConverter() noexcept = default;
[[nodiscard]]
virtual ox::StringView srcTypeName() const noexcept = 0;
[[nodiscard]]
virtual int srcTypeVersion() const noexcept = 0;
[[nodiscard]]
virtual bool srcMatches(ox::CRStringView pSrcTypeName, int pSrcTypeVersion) const noexcept = 0;
[[nodiscard]]
virtual bool dstMatches(ox::CRStringView dstTypeName, int dstTypeVersion) const noexcept = 0;
virtual ox::Result<ox::UniquePtr<Wrap>> convertPtrToPtr(keel::Context &ctx, Wrap &src) const noexcept = 0;
virtual ox::Result<ox::UniquePtr<Wrap>> convertBuffToPtr(keel::Context &ctx, ox::Buffer const&srcBuff) const noexcept = 0;
[[nodiscard]]
inline bool matches(
ox::CRStringView srcTypeName, int srcTypeVersion,
ox::CRStringView dstTypeName, int dstTypeVersion) const noexcept {
return srcMatches(srcTypeName, srcTypeVersion)
&& dstMatches(dstTypeName, dstTypeVersion);
}
};
template<typename SrcType, typename DstType>
class Converter: public BaseConverter {
public:
[[nodiscard]]
ox::StringView srcTypeName() const noexcept final {
return ox::requireModelTypeName<SrcType>();
}
[[nodiscard]]
int srcTypeVersion() const noexcept final {
return ox::requireModelTypeVersion<SrcType>();
}
[[nodiscard]]
bool srcMatches(ox::CRStringView pSrcTypeName, int pSrcTypeVersion) const noexcept final {
static const auto SrcTypeName = ox::requireModelTypeName<SrcType>();
static const auto SrcTypeVersion = ox::requireModelTypeVersion<SrcType>();
return pSrcTypeName == SrcTypeName
&& pSrcTypeVersion == SrcTypeVersion;
}
[[nodiscard]]
bool dstMatches(ox::CRStringView dstTypeName, int dstTypeVersion) const noexcept final {
static const auto DstTypeName = ox::StringView(ox::requireModelTypeName<DstType>());
static const auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
return dstTypeName == DstTypeName
&& dstTypeVersion == DstTypeVersion;
}
ox::Result<ox::UniquePtr<Wrap>> convertPtrToPtr(keel::Context &ctx, Wrap &src) const noexcept final {
auto dst = makeWrap<DstType>();
oxReturnError(convert(ctx, wrapCast<SrcType>(src), wrapCast<DstType>(*dst)));
return ox::Result<ox::UniquePtr<Wrap>>(std::move(dst));
}
ox::Result<ox::UniquePtr<Wrap>> convertBuffToPtr(keel::Context &ctx, ox::Buffer const&srcBuff) const noexcept final {
oxRequireM(src, readAsset<SrcType>(srcBuff));
auto dst = makeWrap<DstType>();
oxReturnError(convert(ctx, src, wrapCast<DstType>(*dst)));
return ox::Result<ox::UniquePtr<Wrap>>(std::move(dst));
}
protected:
virtual ox::Error convert(keel::Context &ctx, SrcType&, DstType&) const noexcept = 0;
};
ox::Result<ox::UniquePtr<Wrap>> convert(
keel::Context &ctx, ox::Buffer const&srcBuffer,
ox::CRStringView dstTypeName, int dstTypeVersion) noexcept;
template<typename DstType>
ox::Result<DstType> convert(keel::Context &ctx, ox::Buffer const&srcBuffer) noexcept {
static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
oxRequire(out, convert(ctx, srcBuffer, DstTypeName, DstTypeVersion));
return std::move(wrapCast<DstType>(out));
}
template<typename DstType>
ox::Error convert(keel::Context &ctx, ox::Buffer const&buff, DstType *outObj) noexcept {
static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
oxRequire(outPtr, convert(ctx, buff, DstTypeName, DstTypeVersion));
*outObj = std::move(wrapCast<DstType>(*outPtr));
return {};
}
template<typename DstType>
ox::Result<ox::Buffer> convertBuffToBuff(keel::Context &ctx, const ox::Buffer &srcBuffer, ox::ClawFormat fmt) noexcept {
static constexpr auto DstTypeName = ox::requireModelTypeName<DstType>();
static constexpr auto DstTypeVersion = ox::requireModelTypeVersion<DstType>();
oxRequire(out, convert(ctx, srcBuffer, DstTypeName, DstTypeVersion));
return ox::writeClaw<DstType>(wrapCast<DstType>(*out), fmt);
}
template<typename From, typename To, ox::ClawFormat fmt = ox::ClawFormat::Metal>
auto transformRule(keel::Context &ctx, ox::Buffer &buff) noexcept -> ox::Error {
oxRequire(hdr, readAssetHeader(buff));
const auto typeId = ox::buildTypeId(
hdr.clawHdr.typeName, hdr.clawHdr.typeVersion, hdr.clawHdr.typeParams);
if (typeId == ox::buildTypeId<From>()) {
oxReturnError(keel::convertBuffToBuff<To>(ctx, buff, fmt).moveTo(buff));
}
return {};
};
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <ox/fs/fs.hpp>
#include <ox/model/typestore.hpp>
namespace keel {
class TypeStore: public ox::TypeStore {
private:
ox::FileSystem &m_fs;
ox::String m_descPath;
public:
explicit TypeStore(ox::FileSystem &fs, ox::StringView descPath) noexcept;
protected:
ox::Result<ox::UPtr<ox::DescriptorType>> loadDescriptor(ox::CRStringView typeId) noexcept override;
};
}

View File

@ -0,0 +1,63 @@
add_library(
Keel
asset.cpp
keel.cpp
media.cpp
module.cpp
pack.cpp
typeconv.cpp
typestore.cpp
)
target_include_directories(
Keel PUBLIC
../include
)
target_link_libraries(
Keel PUBLIC
OxClaw
OxEvent
OxFS
OxModel
OxPreloader
)
install(
DIRECTORY
../include/keel
DESTINATION
include/keel
)
install(
TARGETS
Keel
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
if(TURBINE_BUILD_TYPE STREQUAL "Native")
add_library(
KeelPack-AppLib
pack-applib.cpp
)
target_include_directories(
KeelPack-AppLib PUBLIC
../include
)
target_compile_definitions(
KeelPack-AppLib PUBLIC
OLYMPIC_LOAD_KEEL_MODULES=1
OLYMPIC_APP_NAME="Keel Pack"
)
target_link_libraries(
KeelPack-AppLib
Keel
OxClArgs
OxClaw
OxLogConn
)
endif()

View File

@ -0,0 +1,46 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/asset.hpp>
namespace keel {
ox::Result<ox::UUID> readUuidHeader(ox::Buffer const&buff) noexcept {
return readUuidHeader(buff.data(), buff.size());
}
ox::Result<ox::UUID> readUuidHeader(char const*buff, std::size_t buffLen) noexcept {
if (buffLen < K1HdrSz) {
return OxError(1, "Insufficient data to contain complete Keel header");
}
constexpr ox::StringView k1Hdr = "K1;";
if (k1Hdr != ox::StringView(buff, k1Hdr.bytes())) {
return OxError(2, "No Keel asset header data");
}
return ox::UUID::fromString(ox::StringView(buff + k1Hdr.bytes(), 36));
}
ox::Result<ox::ModelObject> readAsset(ox::TypeStore &ts, ox::Buffer const&buff) noexcept {
std::size_t offset = 0;
if (!readUuidHeader(buff).error) {
offset = K1HdrSz;
}
return ox::readClaw(ts, buff.data() + offset, buff.size() - offset);
}
ox::Result<AssetHdr> readAssetHeader(char const*buff, std::size_t buffLen) noexcept {
AssetHdr out;
const auto err = readUuidHeader(buff, buffLen).moveTo(out.uuid);
const auto offset = err ? 0u : K1HdrSz;
buff = buff + offset;
buffLen = buffLen - offset;
oxReturnError(ox::readClawHeader(buff, buffLen).moveTo(out.clawHdr));
return out;
}
ox::Result<AssetHdr> readAssetHeader(ox::Buffer const&buff) noexcept {
return readAssetHeader(buff.data(), buff.size());
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/keel.hpp>
namespace keel {
ox::Error init(
keel::Context &ctx,
ox::UPtr<ox::FileSystem> &&fs,
ox::CRStringView appName) noexcept {
ctx.appName = appName;
oxIgnoreError(setRomFs(ctx, std::move(fs)));
#ifndef OX_BARE_METAL
auto const&mods = modules();
for (auto &mod : mods) {
// register type converters
for (auto c : mod->converters()) {
ctx.converters.emplace_back(c);
}
// register pack transforms
for (auto c : mod->packTransforms()) {
ctx.packTransforms.emplace_back(c);
}
}
#endif
return {};
}
ox::Result<ox::UPtr<Context>> init(ox::UPtr<ox::FileSystem> &&fs, ox::CRStringView appName) noexcept {
auto ctx = ox::make_unique<Context>();
oxReturnError(keel::init(*ctx, std::move(fs), appName));
return ctx;
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/media.hpp>
#ifndef OX_BARE_METAL
#include <fstream>
#include <ox/std/trace.hpp>
namespace keel {
ox::Result<char*> loadRom(ox::CRStringView path) noexcept {
std::ifstream file(std::string(toStdStringView(path)), std::ios::binary | std::ios::ate);
if (!file.good()) {
oxErrorf("Could not find ROM file: {}", path);
return OxError(1, "Could not find ROM file");
}
try {
auto const size = file.tellg();
file.seekg(0, std::ios::beg);
auto buff = new char[static_cast<std::size_t>(size)];
file.read(buff, size);
return buff;
} catch (std::ios_base::failure const&e) {
oxErrorf("Could not read ROM file due to file IO failure: {}", e.what());
return OxError(2, "Could not read ROM file");
} catch (std::bad_alloc const&e) {
oxErrorf("Could not read ROM file due to new failure: {}", e.what());
return OxError(2, "Could not allocate memory for ROM file");
}
}
void unloadRom(char *rom) noexcept {
ox::safeDelete(rom);
}
static void clearUuidMap(Context &ctx) noexcept {
ctx.uuidToPath.clear();
ctx.pathToUuid.clear();
}
void createUuidMapping(Context &ctx, ox::StringView filePath, ox::UUID const&uuid) noexcept {
ctx.pathToUuid[filePath] = uuid;
ctx.uuidToPath[uuid.toString()] = filePath;
}
static ox::Error buildUuidMap(Context &ctx, ox::CRStringView path) noexcept {
oxRequire(files, ctx.rom->ls(path));
for (auto const&f : files) {
oxRequireM(filePath, ox::join("/", ox::Array<ox::StringView, 2>{path, f}));
oxRequire(stat, ctx.rom->stat(filePath));
if (stat.fileType == ox::FileType::NormalFile) {
oxRequire(data, ctx.rom->read(filePath));
auto const [hdr, err] = readAssetHeader(data);
if (!err) {
createUuidMapping(ctx, filePath, hdr.uuid);
}
} else if (stat.fileType == ox::FileType::Directory) {
if (!beginsWith(f, ".")) {
oxReturnError(buildUuidMap(ctx, filePath));
}
}
}
return {};
}
ox::Error buildUuidMap(Context &ctx) noexcept {
if (!ctx.rom) {
return OxError(1, "No ROM FS");
}
return buildUuidMap(ctx, "");
}
ox::Result<ox::UUID> pathToUuid(Context &ctx, ox::CRStringView path) noexcept {
#ifndef OX_BARE_METAL
oxRequire(out, ctx.pathToUuid.at(path));
return *out;
#else
return OxError(1, "UUID to path conversion not supported on this platform");
#endif
}
ox::Result<ox::String> uuidToPath(Context &ctx, ox::CRStringView uuid) noexcept {
#ifndef OX_BARE_METAL
oxRequire(out, ctx.uuidToPath.at(uuid));
return *out;
#else
return OxError(1, "UUID to path conversion not supported on this platform");
#endif
}
ox::Result<ox::String> uuidToPath(Context &ctx, ox::UUID const&uuid) noexcept {
#ifndef OX_BARE_METAL
oxRequire(out, ctx.uuidToPath.at(uuid.toString()));
return *out;
#else
return OxError(1, "UUID to path conversion not supported on this platform");
#endif
}
ox::Error performPackTransforms(Context &ctx, ox::Buffer &clawData) noexcept {
#ifndef OX_BARE_METAL
for (auto tr : ctx.packTransforms) {
oxReturnError(tr(ctx, clawData));
}
return {};
#else
return OxError(1, "Transformations not supported on this platform");
#endif
}
}
#else
#include <keel/context.hpp>
#define MEM_ROM reinterpret_cast<char*>(0x0800'0000)
namespace keel {
static void clearUuidMap(Context&) noexcept {
}
ox::Error buildUuidMap(Context&) noexcept {
return {};
}
ox::Result<char*> loadRom(ox::CRStringView) noexcept {
// put the header in the wrong order to prevent mistaking this code for the
// media section
constexpr auto headerP2 = "R_______________";
constexpr auto headerP1 = "KEEL_MEDIA_HEADE";
constexpr auto headerP1Len = ox_strlen(headerP2);
constexpr auto headerP2Len = ox_strlen(headerP1);
constexpr auto headerLen = headerP1Len + headerP2Len;
for (auto current = MEM_ROM; current < reinterpret_cast<char*>(0x0a000000); current += headerLen) {
if (ox_memcmp(current, headerP1, headerP1Len) == 0 &&
ox_memcmp(current + headerP1Len, headerP2, headerP2Len) == 0) {
return current + headerLen;
}
}
return OxError(1);
}
void unloadRom(char*) noexcept {
}
ox::Result<std::size_t> getPreloadAddr(keel::Context &ctx, ox::CRStringView path) noexcept {
oxRequire(stat, ctx.rom->stat(path));
oxRequire(buff, static_cast<ox::MemFS*>(ctx.rom.get())->directAccess(path));
PreloadPtr p;
oxReturnError(ox::readMC(buff, static_cast<std::size_t>(stat.size), &p));
return static_cast<std::size_t>(p.preloadAddr) + ctx.preloadSectionOffset;
}
ox::Result<std::size_t> getPreloadAddr(keel::Context &ctx, ox::FileAddress const&file) noexcept {
oxRequire(stat, ctx.rom->stat(file));
oxRequire(buff, static_cast<ox::MemFS*>(ctx.rom.get())->directAccess(file));
PreloadPtr p;
oxReturnError(ox::readMC(buff, static_cast<std::size_t>(stat.size), &p));
return static_cast<std::size_t>(p.preloadAddr) + ctx.preloadSectionOffset;
}
}
#endif
namespace keel {
ox::Error setRomFs(Context &ctx, ox::UPtr<ox::FileSystem> &&fs) noexcept {
ctx.rom = std::move(fs);
clearUuidMap(ctx);
return buildUuidMap(ctx);
}
ox::Result<ox::UniquePtr<ox::FileSystem>> loadRomFs(ox::CRStringView path) noexcept {
auto const lastDot = ox::lastIndexOf(path, '.');
if (!lastDot.error && substr(path, lastDot.value) == ".oxfs") {
oxRequire(rom, loadRom(path));
return {ox::make_unique<ox::FileSystem32>(rom, 32 * ox::units::MB, unloadRom)};
} else {
#ifdef OX_HAS_PASSTHROUGHFS
return {ox::make_unique<ox::PassThroughFS>(path)};
#else
return OxError(2);
#endif
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/module.hpp>
namespace keel {
static ox::Vector<Module const*> mods;
void registerModule(Module const*mod) noexcept {
mods.emplace_back(mod);
}
[[nodiscard]]
ox::Vector<Module const*> const&modules() noexcept {
return mods;
}
ox::Vector<TypeDescGenerator> Module::types() const noexcept {
return {};
}
ox::Vector<keel::BaseConverter const*> Module::converters() const noexcept {
return {};
}
ox::Vector<PackTransform> Module::packTransforms() const noexcept {
return {};
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <fstream>
#include <ox/clargs/clargs.hpp>
#include <ox/fs/fs.hpp>
#include <ox/logconn/def.hpp>
#include <ox/logconn/logconn.hpp>
#include <keel/keel.hpp>
static ox::Error writeFileBuff(ox::StringView path, ox::Buffer const&buff) noexcept {
try {
std::ofstream f(std::string(toStdStringView(path)), std::ios::binary);
f.write(buff.data(), static_cast<intptr_t>(buff.size()));
} catch (std::fstream::failure const&) {
return OxError(2, "failed to write file");
}
return {};
}
static ox::Result<ox::Buffer> readFileBuff(ox::StringView path) noexcept {
std::ifstream file(std::string(toStdStringView(path)), std::ios::binary | std::ios::ate);
if (!file.good()) {
oxErrorf("Could not find OxFS file: {}", path);
return OxError(1, "Could not find OxFS file");
}
try {
auto const size = static_cast<std::size_t>(file.tellg());
ox::Buffer buff(size);
file.seekg(0, std::ios::beg);
file.read(buff.data(), static_cast<std::streamsize>(buff.size()));
return buff;
} catch (std::ios_base::failure const&e) {
oxErrorf("Could not read OxFS file: {}", e.what());
return OxError(2, "Could not read OxFS file");
}
}
static ox::Error generateTypes(ox::TypeStore *ts) noexcept {
for (auto const mod : keel::modules()) {
for (auto gen : mod->types()) {
oxReturnError(gen(*ts));
}
}
return {};
}
static ox::Error pack(ox::StringView argSrc, ox::StringView argRomBin, ox::StringView projectDataDir) noexcept {
ox::Buffer dstBuff(32 * ox::units::MB);
oxReturnError(ox::FileSystem32::format(dstBuff.data(), dstBuff.size()));
ox::FileSystem32 dst(dstBuff);
oxRequire(ctx, keel::init(ox::make_unique<ox::PassThroughFS>(argSrc), "keel-pack"));
keel::TypeStore ts(*ctx->rom, ox::sfmt("{}/type_descriptors", projectDataDir));
oxReturnError(generateTypes(&ts));
oxReturnError(keel::pack(*ctx, ts, dst));
oxRequireM(pl, keel::GbaPreloader::make());
oxReturnError(preload(ts, dst, *pl));
oxReturnError(dst.resize());
// resize buffer
oxRequire(dstSize, dst.size());
dstBuff.resize(dstSize);
oxRequireM(romBuff, readFileBuff(argRomBin));
oxReturnError(appendBinary(romBuff, dstBuff, *pl));
oxOutf("Dest FS size: {} bytes\n", dstSize);
oxOutf("Preload buff size: {} bytes\n", pl->buff().size());
oxOutf("ROM buff size: {} bytes\n", romBuff.size());
oxReturnError(writeFileBuff(argRomBin, romBuff));
return {};
}
static ox::Error run(int argc, char const**argv, ox::StringView projectDataDir) noexcept {
ox::ClArgs const args(argc, argv);
auto const argSrc = args.getString("src", "");
auto const argRomBin = args.getString("rom-bin", "");
if (argSrc == "") {
oxErr("\033[31;1;1merror:\033[0m must specify a source directory\n");
return OxError(1, "must specify a source directory");
}
if (argRomBin == "") {
oxErr("\033[31;1;1merror:\033[0m must specify a path for ROM file\n");
return OxError(1, "must specify a path for preload file");
}
return pack(argSrc, argRomBin, projectDataDir);
}
namespace olympic {
ox::Error run(
[[maybe_unused]] ox::StringView project,
[[maybe_unused]] ox::StringView appName,
ox::StringView projectDataDir,
int argc,
char const**argv) noexcept {
return ::run(argc, argv, projectDataDir);
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/fs/fs.hpp>
#include <ox/model/modelvalue.hpp>
#include <keel/media.hpp>
#include <keel/pack.hpp>
namespace keel {
static ox::Error pathToInode(
keel::Context &ctx, ox::FileSystem &dest, ox::ModelObject &obj) noexcept {
auto &o = obj;
auto type = static_cast<ox::FileAddressType>(o["type"].get<int8_t>());
auto &data = o["data"].get<ox::ModelUnion>();
ox::String path;
switch (type) {
case ox::FileAddressType::Path:
path = data["path"].get<ox::String>();
break;
case ox::FileAddressType::ConstPath:
path = data["constPath"].get<ox::String>();
break;
case ox::FileAddressType::Inode:
case ox::FileAddressType::None:
return {};
}
if (beginsWith(path, "uuid://")) {
auto const uuid = substr<ox::StringView>(path, 7);
oxReturnError(keel::uuidToPath(ctx, uuid).moveTo(path));
}
oxRequire(s, dest.stat(path));
oxReturnError(o["type"].set(static_cast<int8_t>(ox::FileAddressType::Inode)));
return data.set(2, s.inode);
}
static ox::Error transformFileAddressesObj(
keel::Context &ctx, ox::FileSystem &dest, ox::ModelObject &obj) noexcept;
static ox::Error transformFileAddressesVec(
keel::Context &ctx, ox::FileSystem &dest, ox::ModelValueVector &v) noexcept;
static ox::Error transformFileAddresses(
keel::Context &ctx,
ox::FileSystem &dest,
ox::ModelValue &v) noexcept {
if (v.type() == ox::ModelValue::Type::Object) {
auto &obj = v.get<ox::ModelObject>();
return transformFileAddressesObj(ctx, dest, obj);
} else if (v.type() == ox::ModelValue::Type::Vector) {
auto &vec = v.get<ox::ModelValueVector>();
return transformFileAddressesVec(ctx, dest, vec);
}
return {};
}
static ox::Error transformFileAddressesVec(
keel::Context &ctx,
ox::FileSystem &dest,
ox::ModelValueVector &v) noexcept {
for (auto &f : v) {
oxReturnError(transformFileAddresses(ctx, dest, f));
}
return {};
}
/**
* Convert path references in Claw data to inodes to save space
* @return error
*/
static ox::Error transformFileAddressesObj(
keel::Context &ctx,
ox::FileSystem &dest,
ox::ModelObject &obj) noexcept {
if (obj.typeName() == "net.drinkingtea.ox.FileAddress" && obj.typeVersion() == 1) {
return pathToInode(ctx, dest, obj);
}
for (auto &f : obj) {
auto &v = f->value;
oxReturnError(transformFileAddresses(ctx, dest, v));
}
return {};
}
static ox::Error doTransformations(
keel::Context &ctx,
ox::TypeStore &ts,
ox::FileSystem &dest,
ox::CRStringView filePath) noexcept {
// load file
oxRequire(s, dest.stat(filePath));
// do transformations
oxRequireM(buff, dest.read(s.inode));
oxReturnError(keel::performPackTransforms(ctx, buff));
// transform FileAddresses
oxRequireM(obj, keel::readAsset(ts, buff));
oxReturnError(transformFileAddressesObj(ctx, dest, obj));
oxReturnError(ox::writeClaw(obj).moveTo(buff));
// write file to dest
oxReturnError(dest.write(s.inode, buff));
return {};
}
// claw file transformations are broken out from copy because path to inode
// transformations need to be done after the copy to the new FS is complete
static ox::Error transformClaw(
keel::Context &ctx,
ox::TypeStore &ts,
ox::FileSystem &dest,
ox::CRStringView path) noexcept {
// copy
oxTracef("pack.transformClaw", "path: {}", path);
oxRequire(fileList, dest.ls(path));
for (auto const&name : fileList) {
auto const filePath = ox::sfmt("{}{}", path, name);
oxRequire(stat, dest.stat(filePath));
if (stat.fileType == ox::FileType::Directory) {
auto const dir = ox::sfmt("{}{}/", path, name);
oxReturnError(transformClaw(ctx, ts, dest, dir));
} else {
oxReturnError(doTransformations(ctx, ts, dest, filePath));
}
}
return {};
}
static ox::Error copy(
ox::FileSystem &src,
ox::FileSystem &dest,
ox::CRStringView path) noexcept {
oxOutf("copying directory: {}\n", path);
// copy
oxRequire(fileList, src.ls(path));
for (auto const&name : fileList) {
auto currentFile = ox::sfmt("{}{}", path, name);
if (beginsWith(name, ".")) {
continue;
}
oxOutf("reading {}\n", currentFile);
oxRequire(stat, src.stat(currentFile));
if (stat.fileType == ox::FileType::Directory) {
oxReturnError(dest.mkdir(currentFile, true));
oxReturnError(copy(src, dest, currentFile + '/'));
} else {
// load file
oxRequireM(buff, src.read(currentFile));
// write file to dest
oxOutf("writing {}\n", currentFile);
oxReturnError(dest.write(currentFile, buff));
}
}
return {};
}
static ox::Error copyFS(ox::FileSystem &src, ox::FileSystem &dest) noexcept {
oxReturnError(copy(src, dest, "/"));
return {};
}
ox::Error pack(keel::Context &ctx, ox::TypeStore &ts, ox::FileSystem &dest) noexcept {
oxReturnError(copyFS(*ctx.rom, dest));
oxOut("Doing transforms\n");
oxReturnError(transformClaw(ctx, ts, dest, "/"));
return {};
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/claw/read.hpp>
#include <keel/media.hpp>
#include <keel/typeconv.hpp>
namespace keel {
[[nodiscard]]
static ox::Result<const BaseConverter*> findConverter(
ox::Vector<const BaseConverter*> const&converters,
ox::CRStringView srcTypeName,
int srcTypeVersion,
ox::CRStringView dstTypeName,
int dstTypeVersion) noexcept {
for (auto &c : converters) {
if (c->matches(srcTypeName, srcTypeVersion, dstTypeName, dstTypeVersion)) {
return c;
}
}
return OxError(1, "Could not find converter");
};
static ox::Result<ox::UniquePtr<Wrap>> convert(
[[maybe_unused]] keel::Context &ctx,
ox::Vector<const BaseConverter*> const&converters,
[[maybe_unused]] ox::Buffer const&srcBuffer,
[[maybe_unused]] ox::CRStringView srcTypeName,
[[maybe_unused]] int srcTypeVersion,
[[maybe_unused]] ox::CRStringView dstTypeName,
[[maybe_unused]] int dstTypeVersion) noexcept {
// look for direct converter
auto [c, err] = findConverter(converters, srcTypeName, srcTypeVersion, dstTypeName, dstTypeVersion);
if (!err) {
return c->convertBuffToPtr(ctx, srcBuffer);
}
// try to chain multiple converters
for (const auto &subConverter : converters) {
if (!subConverter->dstMatches(dstTypeName, dstTypeVersion)) {
continue;
}
const auto [intermediate, chainErr] =
convert(ctx, converters, srcBuffer, srcTypeName, srcTypeVersion,
subConverter->srcTypeName(), subConverter->srcTypeVersion());
if (!chainErr) {
return subConverter->convertPtrToPtr(ctx, *intermediate);
}
}
return OxError(1, "Could not convert between types");
}
ox::Result<ox::UniquePtr<Wrap>> convert(
[[maybe_unused]] keel::Context &ctx,
[[maybe_unused]] ox::Buffer const&srcBuffer,
[[maybe_unused]] ox::CRStringView dstTypeName,
[[maybe_unused]] int dstTypeVersion) noexcept {
#ifndef OX_BARE_METAL
oxRequire(hdr, readAssetHeader(srcBuffer));
return convert(
ctx,
ctx.converters,
srcBuffer,
hdr.clawHdr.typeName,
hdr.clawHdr.typeVersion,
dstTypeName,
dstTypeVersion);
#else
return OxError(1, "Operation not supported on this platform");
#endif
}
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <keel/typestore.hpp>
namespace keel {
TypeStore::TypeStore(ox::FileSystem &fs, ox::StringView descPath) noexcept:
m_fs(fs),
m_descPath(descPath) {
}
ox::Result<ox::UPtr<ox::DescriptorType>> TypeStore::loadDescriptor(ox::CRStringView typeId) noexcept {
auto path = ox::sfmt("{}/{}", m_descPath, typeId);
oxRequire(buff, m_fs.read(path));
auto dt = ox::make_unique<ox::DescriptorType>();
oxReturnError(ox::readClaw<ox::DescriptorType>(buff, dt.get()));
return dt;
}
}

View File

@ -0,0 +1,11 @@
add_executable(
KeelTest
tests.cpp
)
target_link_libraries(
KeelTest
Keel
)
add_test("[keel] writeUuidHeader" ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/KeelTest writeUuidHeader)

View File

@ -0,0 +1,39 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#undef NDEBUG
#include <map>
#include <ox/std/std.hpp>
#include <keel/keel.hpp>
static std::map<ox::StringView, ox::Error(*)()> tests = {
{
"writeUuidHeader",
[]() -> ox::Error {
constexpr ox::StringView uuidStr = "8d814442-f46e-4cc3-8edc-ca3c01cc86db";
constexpr ox::StringView hdr = "K1;8d814442-f46e-4cc3-8edc-ca3c01cc86db;";
oxRequire(uuid, ox::UUID::fromString(uuidStr));
ox::Array<char, hdr.len()> buff;
ox::CharBuffWriter bw(buff);
oxReturnError(keel::writeUuidHeader(bw, uuid));
oxExpect(ox::StringView(buff.data(), buff.size()), hdr);
return {};
}
},
};
int main(int argc, const char **args) {
int retval = -1;
if (argc > 0) {
auto testName = args[1];
if (tests.find(testName) != tests.end()) {
retval = static_cast<int>(tests[testName]());
} else {
retval = 1;
}
}
return retval;
}

View File

@ -0,0 +1,2 @@
add_subdirectory(applib)
add_subdirectory(modlib)

View File

@ -0,0 +1 @@
add_subdirectory(src)

View File

@ -0,0 +1,15 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <studio/module.hpp>
namespace studio {
void registerModule(studio::Module const*) noexcept;
}

View File

@ -0,0 +1,42 @@
add_library(
StudioAppLib
aboutpopup.cpp
clawviewer.cpp
filedialogmanager.cpp
main.cpp
newmenu.cpp
projectexplorer.cpp
projecttreemodel.cpp
studioapp.cpp
)
target_compile_definitions(
StudioAppLib PUBLIC
OLYMPIC_LOAD_STUDIO_MODULES=1
OLYMPIC_APP_NAME="Studio"
)
target_link_libraries(
StudioAppLib PUBLIC
OxClArgs
OxLogConn
Studio
)
target_include_directories(
StudioAppLib PUBLIC
../include
)
install(
DIRECTORY
../include/studioapp
DESTINATION
include
)
install(
TARGETS
StudioAppLib
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@ -0,0 +1,61 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <studio/imguiuitl.hpp>
#include "aboutpopup.hpp"
namespace studio {
AboutPopup::AboutPopup(turbine::Context &ctx) noexcept {
m_text = ox::sfmt("{} - dev build", keelCtx(ctx).appName);
}
void AboutPopup::open() noexcept {
m_stage = Stage::Opening;
}
void AboutPopup::close() noexcept {
m_stage = Stage::Closed;
}
bool AboutPopup::isOpen() const noexcept {
return m_stage == Stage::Open;
}
void AboutPopup::draw(turbine::Context &ctx) noexcept {
switch (m_stage) {
case Stage::Closed:
break;
case Stage::Opening:
ImGui::OpenPopup("About");
m_stage = Stage::Open;
[[fallthrough]];
case Stage::Open: {
constexpr auto modalFlags =
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
ImGui::SetNextWindowSize(ImVec2(215, 90));
studio::ig::centerNextWindow(ctx);
auto open = true;
if (ImGui::BeginPopupModal("About", &open, modalFlags)) {
ImGui::Text("%s", m_text.c_str());
ImGui::NewLine();
ImGui::Dummy(ImVec2(148.0f, 0.0f));
ImGui::SameLine();
if (ImGui::Button("Close")) {
ImGui::CloseCurrentPopup();
open = false;
}
ImGui::EndPopup();
}
if (!open) {
m_stage = Stage::Closed;
}
break;
}
}
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
#include <studio/popup.hpp>
namespace studio {
class AboutPopup: public studio::Popup {
public:
enum class Stage {
Closed,
Opening,
Open,
};
private:
Stage m_stage = Stage::Closed;
ox::String m_text;
public:
explicit AboutPopup(turbine::Context &ctx) noexcept;
void open() noexcept override;
void close() noexcept override;
[[nodiscard]]
bool isOpen() const noexcept override;
void draw(turbine::Context &ctx) noexcept override;
};
}

View File

@ -0,0 +1,174 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include "clawviewer.hpp"
namespace studio {
ClawEditor::ClawEditor(ox::CRStringView path, ox::ModelObject obj) noexcept:
Editor(path),
m_obj(std::move(obj)) {
}
void ClawEditor::draw(turbine::Context&) noexcept {
ImGui::BeginChild("PaletteEditor");
static constexpr auto flags = ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody;
if (ImGui::BeginTable("ObjTree", 3, flags)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 250);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_NoHide);
ImGui::TableHeadersRow();
ObjPath objPath;
drawTree(objPath, m_obj);
ImGui::EndTable();
}
ImGui::EndChild();
}
void ClawEditor::drawRow(ox::ModelValue const&value) noexcept {
using Str = ox::BasicString<100>;
Str val, type;
switch (value.type()) {
case ox::ModelValue::Type::Undefined:
val = "undefined";
type = "undefined";
break;
case ox::ModelValue::Type::Bool:
val = value.get<bool>() ? "true" : "false";
type = "bool";
break;
case ox::ModelValue::Type::UnsignedInteger8:
val = ox::sfmt<Str>("{}", value.get<uint8_t>());
type = "uint8";
break;
case ox::ModelValue::Type::UnsignedInteger16:
val = ox::sfmt<Str>("{}", value.get<uint16_t>());
type = "uint16";
break;
case ox::ModelValue::Type::UnsignedInteger32:
val = ox::sfmt<Str>("{}", value.get<uint32_t>());
type = "uint32";
break;
case ox::ModelValue::Type::UnsignedInteger64:
val = ox::sfmt<Str>("{}", value.get<uint64_t>());
type = "uint64";
break;
case ox::ModelValue::Type::SignedInteger8:
val = ox::sfmt<Str>("{}", value.get<int8_t>());
type = "int8";
break;
case ox::ModelValue::Type::SignedInteger16:
val = ox::sfmt<Str>("{}", value.get<int16_t>());
type = "int16";
break;
case ox::ModelValue::Type::SignedInteger32:
val = ox::sfmt<Str>("{}", value.get<int32_t>());
type = "int32";
break;
case ox::ModelValue::Type::SignedInteger64:
val = ox::sfmt<Str>("{}", value.get<int64_t>());
type = "int64";
break;
case ox::ModelValue::Type::String:
val = ox::sfmt<Str>("\"{}\"", value.get<ox::String>());
type = "string";
break;
case ox::ModelValue::Type::Object:
type = value.get<ox::ModelObject>().type()->typeName.c_str();
break;
case ox::ModelValue::Type::Union:
type = "union";
break;
case ox::ModelValue::Type::Vector:
type = "list";
break;
}
ImGui::TableNextColumn();
ImGui::Text("%s", type.c_str());
ImGui::TableNextColumn();
ImGui::Text("%s", val.c_str());
}
void ClawEditor::drawVar(ObjPath &path, ox::CRStringView name, ox::ModelValue const&value) noexcept {
using Str = ox::BasicString<100>;
path.push_back(name);
if (value.type() == ox::ModelValue::Type::Object) {
drawTree(path, value.get<ox::ModelObject>());
} else if (value.type() == ox::ModelValue::Type::Vector) {
auto const&vec = value.get<ox::ModelValueVector>();
auto const pathStr = ox::join<Str>("##", path).unwrap();
auto const lbl = ox::sfmt<Str>("{}##{}", name, pathStr);
auto const flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| (vec.size() ? 0 : ImGuiTreeNodeFlags_Leaf)
| (false ? ImGuiTreeNodeFlags_Selected : 0);
auto const open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
drawRow(value);
if (open) {
for (auto i = 0lu; auto const&e: vec) {
auto const iStr = ox::sfmt<Str>("{}", i);
path.push_back(iStr);
ImGui::TableNextRow(0, 5);
ImGui::TableNextColumn();
drawVar(path, ox::sfmt<Str>("[{}]", i), e);
path.pop_back();
++i;
}
ImGui::TreePop();
}
} else {
auto const pathStr = ox::join<Str>("##", path).unwrap();
auto const lbl = ox::sfmt<Str>("{}##{}", name, pathStr);
auto const flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_Leaf
| (false ? ImGuiTreeNodeFlags_Selected : 0);
auto const open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
drawRow(value);
if (open) {
ImGui::TreePop();
}
}
path.pop_back();
}
void ClawEditor::drawTree(ObjPath &path, ox::ModelObject const&obj) noexcept {
using Str = ox::BasicString<100>;
for (const auto &c : obj) {
ImGui::TableNextRow(0, 5);
auto pathStr = ox::join<Str>("##", path).unwrap();
auto lbl = ox::sfmt<Str>("{}##{}", c->name, pathStr);
const auto rowSelected = false;
const auto hasChildren = c->value.type() == ox::ModelValue::Type::Object
|| c->value.type() == ox::ModelValue::Type::Vector;
const auto flags = ImGuiTreeNodeFlags_SpanFullWidth
| ImGuiTreeNodeFlags_OpenOnArrow
| (hasChildren ? 0 : ImGuiTreeNodeFlags_Leaf)
| (rowSelected ? ImGuiTreeNodeFlags_Selected : 0);
ImGui::TableNextColumn();
//if (ImGui::IsItemClicked()) {
//}
//if (ImGui::IsMouseDoubleClicked(0) && ImGui::IsItemHovered()) {
//}
path.push_back(c->name);
if (c->value.type() == ox::ModelValue::Type::Object) {
const auto open = ImGui::TreeNodeEx(lbl.c_str(), flags);
ImGui::SameLine();
drawRow(c->value);
if (open) {
drawTree(path, c->value.get<ox::ModelObject>());
ImGui::TreePop();
}
} else {
drawVar(path, c->name, c->value);
}
path.pop_back();
}
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/model/modelvalue.hpp>
#include <turbine/context.hpp>
#include <studio/editor.hpp>
namespace studio {
class ClawEditor: public studio::Editor {
private:
using ObjPath = ox::Vector<ox::StringView, 8>;
ox::ModelObject m_obj;
public:
ClawEditor(ox::CRStringView path, ox::ModelObject obj) noexcept;
void draw(turbine::Context&) noexcept final;
private:
static void drawRow(ox::ModelValue const&value) noexcept;
void drawVar(ObjPath &path, ox::CRStringView name, ox::ModelValue const&value) noexcept;
void drawTree(ObjPath &path, ox::ModelObject const&obj) noexcept;
};
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <turbine/gfx.hpp>
#include "filedialogmanager.hpp"
namespace studio {
studio::TaskState FileDialogManager::update(turbine::Context &ctx) noexcept {
switch (m_state) {
case UpdateProjectPathState::EnableSystemCursor: {
// switch to system cursor in this update and open file dialog in the next
// the cursor state has to be set before the ImGui frame starts
m_state = UpdateProjectPathState::RunFileDialog;
break;
}
case UpdateProjectPathState::RunFileDialog: {
// switch to system cursor
auto [path, err] = studio::chooseDirectory();
// Mac file dialog doesn't restore focus to main window when closed...
turbine::focusWindow(ctx);
if (!err) {
err = pathChosen.emitCheckError(path);
oxAssert(err, "Path chosen response failed");
}
m_state = UpdateProjectPathState::None;
return studio::TaskState::Done;
}
case UpdateProjectPathState::None:
break;
}
return studio::TaskState::Running;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
#include <studio/filedialog.hpp>
#include <studio/task.hpp>
namespace studio {
class FileDialogManager: public studio::Task {
private:
enum class UpdateProjectPathState {
None,
EnableSystemCursor,
RunFileDialog,
Start = EnableSystemCursor,
} m_state = UpdateProjectPathState::Start;
public:
FileDialogManager() noexcept = default;
template<class... Args>
explicit FileDialogManager(Args ...args) noexcept {
pathChosen.connect(args...);
}
~FileDialogManager() noexcept override = default;
studio::TaskState update(turbine::Context &ctx) noexcept final;
// signals
ox::Signal<ox::Error(ox::String const&)> pathChosen;
};
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ctime>
#include <ox/logconn/logconn.hpp>
#include <ox/logconn/def.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/uuid.hpp>
#include <keel/media.hpp>
#include <turbine/turbine.hpp>
#include <studio/context.hpp>
#include <studioapp/studioapp.hpp>
#include "studioapp.hpp"
namespace studio {
class StudioUIDrawer: public turbine::gl::Drawer {
private:
StudioUI &m_ui;
public:
explicit StudioUIDrawer(StudioUI &ui) noexcept: m_ui(ui) {
}
protected:
void draw(turbine::Context&) noexcept final {
m_ui.draw();
}
};
static int updateHandler(turbine::Context &ctx) noexcept {
auto sctx = turbine::applicationData<studio::StudioContext>(ctx);
auto ui = dynamic_cast<StudioUI*>(sctx->ui);
ui->update();
return 16;
}
static void keyEventHandler(turbine::Context &ctx, turbine::Key key, bool down) noexcept {
auto sctx = turbine::applicationData<studio::StudioContext>(ctx);
auto ui = dynamic_cast<StudioUI*>(sctx->ui);
ui->handleKeyEvent(key, down);
}
static ox::Error runApp(
ox::CRStringView appName,
ox::CRStringView projectDataDir,
ox::UniquePtr<ox::FileSystem> fs) noexcept {
oxRequireM(ctx, turbine::init(std::move(fs), appName));
turbine::setWindowTitle(*ctx, keelCtx(*ctx).appName);
turbine::setUpdateHandler(*ctx, updateHandler);
turbine::setKeyEventHandler(*ctx, keyEventHandler);
turbine::setConstantRefresh(*ctx, false);
studio::StudioContext studioCtx;
turbine::setApplicationData(*ctx, &studioCtx);
StudioUI ui(*ctx, projectDataDir);
studioCtx.ui = &ui;
StudioUIDrawer drawer(ui);
turbine::gl::addDrawer(*ctx, &drawer);
auto const err = turbine::run(*ctx);
turbine::gl::removeDrawer(*ctx, &drawer);
return err;
}
ox::Error run(
ox::CRStringView appName,
ox::CRStringView projectDataDir,
int,
char const**) {
// seed UUID generator
auto const time = std::time(nullptr);
ox::UUID::seedGenerator({
static_cast<uint64_t>(time),
static_cast<uint64_t>(time << 1)
});
// run app
auto const err = runApp(appName, projectDataDir, ox::UniquePtr<ox::FileSystem>(nullptr));
oxAssert(err, "Something went wrong...");
return err;
}
}
namespace olympic {
ox::Error run(
ox::StringView project,
ox::StringView appName,
ox::StringView projectDataDir,
int argc,
char const**argv) noexcept {
return studio::run(ox::sfmt("{} {}", project, appName), projectDataDir, argc, argv);
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <imgui.h>
#include <studio/imguiuitl.hpp>
#include "newmenu.hpp"
namespace studio {
NewMenu::NewMenu() noexcept {
setTitle(ox::String("New Item"));
setSize({230, 140});
}
void NewMenu::open() noexcept {
m_stage = Stage::Opening;
m_selectedType = 0;
m_itemName = "";
m_typeName = "";
}
void NewMenu::close() noexcept {
m_stage = Stage::Closed;
m_open = false;
}
bool NewMenu::isOpen() const noexcept {
return m_open;
}
void NewMenu::draw(turbine::Context &ctx) noexcept {
switch (m_stage) {
case Stage::Opening:
ImGui::OpenPopup(title().c_str());
m_stage = Stage::NewItemType;
m_open = true;
[[fallthrough]];
case Stage::NewItemType:
drawNewItemType(ctx);
break;
case Stage::NewItemName:
drawNewItemName(ctx);
break;
case Stage::Closed:
m_open = false;
break;
}
}
void NewMenu::addItemMaker(ox::UniquePtr<studio::ItemMaker> &&im) noexcept {
m_types.emplace_back(std::move(im));
std::sort(
m_types.begin(), m_types.end(),
[](ox::UPtr<ItemMaker> const&im1, ox::UPtr<ItemMaker> const&im2) {
return im1->name < im2->name;
});
}
void NewMenu::drawNewItemType(turbine::Context &ctx) noexcept {
drawWindow(ctx, &m_open, [this] {
auto items = ox_malloca(m_types.size() * sizeof(char const*), char const*, nullptr);
for (auto i = 0u; auto const&im : m_types) {
items.get()[i] = im->name.c_str();
++i;
}
ImGui::ListBox("Item Type", &m_selectedType, items.get(), static_cast<int>(m_types.size()));
drawFirstPageButtons();
});
}
void NewMenu::drawNewItemName(turbine::Context &ctx) noexcept {
drawWindow(ctx, &m_open, [this, &ctx] {
auto const typeIdx = static_cast<std::size_t>(m_selectedType);
if (typeIdx < m_types.size()) {
ImGui::InputText("Name", m_itemName.data(), m_itemName.cap());
}
drawLastPageButtons(ctx);
});
}
void NewMenu::drawFirstPageButtons() noexcept {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 130);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20);
auto const btnSz = ImVec2(60, 20);
if (ImGui::Button("Next", btnSz)) {
m_stage = Stage::NewItemName;
}
ImGui::SameLine();
if (ImGui::Button("Cancel", btnSz)) {
ImGui::CloseCurrentPopup();
m_stage = Stage::Closed;
}
}
void NewMenu::drawLastPageButtons(turbine::Context &ctx) noexcept {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - 138);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y - 20);
if (ImGui::Button("Back")) {
m_stage = Stage::NewItemType;
}
ImGui::SameLine();
if (ImGui::Button("Finish")) {
finish(ctx);
}
ImGui::SameLine();
if (ImGui::Button("Quit")) {
ImGui::CloseCurrentPopup();
m_stage = Stage::Closed;
}
}
void NewMenu::finish(turbine::Context &ctx) noexcept {
auto const err = m_types[static_cast<std::size_t>(m_selectedType)]->write(ctx, m_itemName);
if (err) {
oxLogError(err);
return;
}
finished.emit("");
m_stage = Stage::Closed;
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <studio/itemmaker.hpp>
#include <studio/popup.hpp>
namespace studio {
class NewMenu: public studio::Popup {
public:
enum class Stage {
Closed,
Opening,
NewItemType,
NewItemName,
};
// emits path parameter
ox::Signal<ox::Error(ox::StringView)> finished;
private:
Stage m_stage = Stage::Closed;
ox::String m_typeName;
ox::BString<255> m_itemName;
ox::Vector<ox::UniquePtr<studio::ItemMaker>> m_types;
int m_selectedType = 0;
bool m_open = false;
public:
NewMenu() noexcept;
void open() noexcept override;
void close() noexcept override;
[[nodiscard]]
bool isOpen() const noexcept override;
void draw(turbine::Context &ctx) noexcept override;
template<typename T>
void addItemType(ox::String name, ox::String parentDir, ox::String fileExt, T itemTempl, ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept;
template<typename T>
void addItemType(ox::String name, ox::String parentDir, ox::String fileExt, ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept;
void addItemMaker(ox::UniquePtr<studio::ItemMaker> &&im) noexcept;
private:
void drawNewItemType(turbine::Context &ctx) noexcept;
void drawNewItemName(turbine::Context &ctx) noexcept;
void drawFirstPageButtons() noexcept;
void drawLastPageButtons(turbine::Context &ctx) noexcept;
void finish(turbine::Context &ctx) noexcept;
};
template<typename T>
void NewMenu::addItemType(ox::String displayName, ox::String parentDir, ox::String fileExt, T itemTempl, ox::ClawFormat pFmt) noexcept {
m_types.emplace_back(ox::make<studio::ItemMakerT<T>>(std::move(displayName), std::move(parentDir), std::move(fileExt), std::move(itemTempl), pFmt));
}
template<typename T>
void NewMenu::addItemType(ox::String displayName, ox::String parentDir, ox::String fileExt, ox::ClawFormat pFmt) noexcept {
m_types.emplace_back(ox::make<studio::ItemMakerT<T>>(std::move(displayName), std::move(parentDir), std::move(fileExt), pFmt));
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <imgui.h>
#include "projectexplorer.hpp"
namespace studio {
static ox::Result<ox::UniquePtr<ProjectTreeModel>>
buildProjectTreeModel(ProjectExplorer &explorer, ox::StringView name, ox::CRStringView path, ProjectTreeModel *parent) noexcept {
auto const fs = explorer.romFs();
oxRequire(stat, fs->stat(path));
auto out = ox::make_unique<ProjectTreeModel>(explorer, ox::String(name), parent);
if (stat.fileType == ox::FileType::Directory) {
oxRequireM(children, fs->ls(path));
std::sort(children.begin(), children.end());
ox::Vector<ox::UniquePtr<ProjectTreeModel>> outChildren;
for (auto const&childName : children) {
if (childName[0] != '.') {
auto const childPath = ox::sfmt("{}/{}", path, childName);
oxRequireM(child, buildProjectTreeModel(explorer, childName, childPath, out.get()));
outChildren.emplace_back(std::move(child));
}
}
out->setChildren(std::move(outChildren));
}
return out;
}
ProjectExplorer::ProjectExplorer(turbine::Context &ctx) noexcept: m_ctx(ctx) {
}
void ProjectExplorer::draw(turbine::Context &ctx) noexcept {
auto const viewport = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ProjectExplorer", ImVec2(300, viewport.y), true);
ImGui::SetNextItemOpen(true);
if (m_treeModel) {
m_treeModel->draw(ctx);
}
ImGui::EndChild();
}
void ProjectExplorer::setModel(ox::UPtr<ProjectTreeModel> &&model) noexcept {
m_treeModel = std::move(model);
}
ox::Error ProjectExplorer::refreshProjectTreeModel(ox::CRStringView) noexcept {
oxRequireM(model, buildProjectTreeModel(*this, "Project", "/", nullptr));
setModel(std::move(model));
return OxError(0);
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/memory.hpp>
#include <studio/widget.hpp>
#include "projecttreemodel.hpp"
namespace studio {
class ProjectExplorer: public studio::Widget {
private:
ox::UPtr<ProjectTreeModel> m_treeModel;
turbine::Context &m_ctx;
public:
explicit ProjectExplorer(turbine::Context &ctx) noexcept;
void draw(turbine::Context &ctx) noexcept override;
void setModel(ox::UPtr<ProjectTreeModel> &&model) noexcept;
ox::Error refreshProjectTreeModel(ox::CRStringView = {}) noexcept;
[[nodiscard]]
inline ox::FileSystem *romFs() noexcept {
return rom(m_ctx);
}
// slots
public:
ox::Signal<ox::Error(ox::StringView const&)> fileChosen;
};
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include "projectexplorer.hpp"
#include "projecttreemodel.hpp"
namespace studio {
ProjectTreeModel::ProjectTreeModel(ProjectExplorer &explorer, ox::String name,
ProjectTreeModel *parent) noexcept:
m_explorer(explorer),
m_parent(parent),
m_name(std::move(name)) {
}
ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept:
m_explorer(other.m_explorer),
m_parent(other.m_parent),
m_name(std::move(other.m_name)),
m_children(std::move(other.m_children)) {
}
void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (!m_children.empty()) {
if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
for (auto const&child : m_children) {
child->draw(ctx);
}
ImGui::TreePop();
}
} else {
auto const path = fullPath();
auto const name = ox::sfmt<ox::BasicString<255>>("{}##{}", m_name, path);
if (ImGui::TreeNodeEx(name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
m_explorer.fileChosen.emit(path);
}
ImGui::TreePop();
}
}
}
void ProjectTreeModel::setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> children) noexcept {
m_children = std::move(children);
}
ox::BasicString<255> ProjectTreeModel::fullPath() const noexcept {
if (m_parent) {
return m_parent->fullPath() + "/" + ox::StringView(m_name);
}
return {};
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
namespace studio {
class ProjectTreeModel {
private:
class ProjectExplorer &m_explorer;
ProjectTreeModel *m_parent = nullptr;
ox::String m_name;
ox::Vector<ox::UPtr<ProjectTreeModel>> m_children;
public:
explicit ProjectTreeModel(class ProjectExplorer &explorer, ox::String name,
ProjectTreeModel *parent = nullptr) noexcept;
ProjectTreeModel(ProjectTreeModel &&other) noexcept;
void draw(turbine::Context &ctx) const noexcept;
void setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> children) noexcept;
private:
[[nodiscard]]
ox::BasicString<255> fullPath() const noexcept;
};
}

View File

@ -0,0 +1,404 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <keel/media.hpp>
#include <glutils/glutils.hpp>
#include <turbine/turbine.hpp>
#include <studio/configio.hpp>
#include "clawviewer.hpp"
#include "filedialogmanager.hpp"
#include "studioapp.hpp"
namespace studio {
static ox::Vector<studio::Module const*> modules;
void registerModule(studio::Module const*mod) noexcept {
modules.emplace_back(mod);
}
struct StudioConfig {
static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig";
static constexpr auto TypeVersion = 1;
ox::String projectPath;
ox::String activeTabItemName;
ox::Vector<ox::String> openFiles;
bool showProjectExplorer = true;
};
oxModelBegin(StudioConfig)
oxModelFieldRename(active_tab_item_name, activeTabItemName)
oxModelFieldRename(project_path, projectPath)
oxModelFieldRename(open_files, openFiles)
oxModelFieldRename(show_project_explorer, showProjectExplorer)
oxModelEnd()
StudioUI::StudioUI(turbine::Context &ctx, ox::StringView projectDataDir) noexcept:
m_ctx(ctx),
m_projectDataDir(projectDataDir),
m_projectExplorer(m_ctx),
m_aboutPopup(m_ctx) {
m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
ImGui::GetIO().IniFilename = nullptr;
loadModules();
// open project and files
auto const [config, err] = studio::readConfig<StudioConfig>(keelCtx(m_ctx));
m_showProjectExplorer = config.showProjectExplorer;
if (!err) {
auto const openProjErr = openProject(config.projectPath);
if (!openProjErr) {
for (auto const&f: config.openFiles) {
auto openFileErr = openFileActiveTab(f, config.activeTabItemName == f);
if (openFileErr) {
oxErrorf("\nCould not open editor for file:\n\t{}\nReason:\n\t{}\n", f, toStr(openFileErr));
}
}
}
} else {
if constexpr(!ox::defines::Debug) {
oxErrf("Could not open studio config file: {}: {}\n", err.errCode, toStr(err));
} else {
oxErrf(
"Could not open studio config file: {}: {} ({}:{})\n",
err.errCode, toStr(err), err.file, err.line);
}
}
}
void StudioUI::update() noexcept {
m_taskRunner.update(m_ctx);
}
void StudioUI::handleKeyEvent(turbine::Key key, bool down) noexcept {
auto const ctrlDown = turbine::buttonDown(m_ctx, turbine::Key::Mod_Ctrl);
for (auto p : m_popups) {
if (p->isOpen()) {
if (key == turbine::Key::Escape) {
p->close();
}
return;
}
}
if (down && ctrlDown) {
switch (key) {
case turbine::Key::Num_1:
toggleProjectExplorer();
break;
case turbine::Key::Alpha_C:
if (m_activeEditor && m_activeEditor->copyEnabled()) {
m_activeEditor->copy();
}
break;
case turbine::Key::Alpha_N:
m_newMenu.open();
break;
case turbine::Key::Alpha_O:
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProject));
break;
case turbine::Key::Alpha_Q:
turbine::requestShutdown(m_ctx);
break;
case turbine::Key::Alpha_S:
save();
break;
case turbine::Key::Alpha_V:
if (m_activeEditor && m_activeEditor->pasteEnabled()) {
m_activeEditor->paste();
}
break;
case turbine::Key::Alpha_X:
if (m_activeEditor && m_activeEditor->cutEnabled()) {
m_activeEditor->cut();
}
break;
case turbine::Key::Alpha_Y:
redo();
break;
case turbine::Key::Alpha_Z:
undo();
break;
default:
if (m_activeEditor) {
m_activeEditor->keyStateChanged(key, down);
}
break;
}
} else if (m_activeEditor && !ctrlDown) {
m_activeEditor->keyStateChanged(key, down);
}
}
void StudioUI::draw() noexcept {
glutils::clearScreen();
drawMenu();
auto const viewport = ImGui::GetMainViewport();
constexpr auto menuHeight = 18;
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + menuHeight));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, viewport->Size.y - menuHeight));
constexpr auto windowFlags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoSavedSettings;
ImGui::Begin("MainWindow##Studio", nullptr, windowFlags);
{
if (m_showProjectExplorer) {
m_projectExplorer.draw(m_ctx);
ImGui::SameLine();
}
drawTabBar();
for (auto &w: m_widgets) {
w->draw(m_ctx);
}
for (auto p: m_popups) {
p->draw(m_ctx);
}
}
ImGui::End();
}
void StudioUI::drawMenu() noexcept {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New...", "Ctrl+N")) {
m_newMenu.open();
}
if (ImGui::MenuItem("Open Project...", "Ctrl+O")) {
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProject));
}
if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) {
m_activeEditor->save();
}
if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
turbine::requestShutdown(m_ctx);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, undoStack && undoStack->canUndo())) {
undoStack->undo();
}
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) {
undoStack->redo();
}
ImGui::Separator();
if (ImGui::MenuItem("Copy", "Ctrl+C", false, m_activeEditor && m_activeEditor->copyEnabled())) {
m_activeEditor->copy();
}
if (ImGui::MenuItem("Cut", "Ctrl+X", false, m_activeEditor && m_activeEditor->cutEnabled())) {
m_activeEditor->cut();
}
if (ImGui::MenuItem("Paste", "Ctrl+V", false, m_activeEditor && m_activeEditor->pasteEnabled()) && m_activeEditor) {
m_activeEditor->paste();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
if (ImGui::MenuItem("Project Explorer", "Ctrl+1", m_showProjectExplorer)) {
toggleProjectExplorer();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About")) {
m_aboutPopup.open();
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void StudioUI::drawTabBar() noexcept {
auto const viewport = ImGui::GetContentRegionAvail();
ImGui::BeginChild("TabWindow##MainWindow##Studio", ImVec2(viewport.x, viewport.y));
constexpr auto tabBarFlags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_TabListPopupButton;
if (ImGui::BeginTabBar("TabBar##TabWindow##MainWindow##Studio", tabBarFlags)) {
drawTabs();
ImGui::EndTabBar();
}
ImGui::EndChild();
}
void StudioUI::drawTabs() noexcept {
for (auto it = m_editors.begin(); it != m_editors.end();) {
auto const &e = *it;
auto open = true;
auto const unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0;
auto const selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0;
auto const flags = unsavedChanges | selected;
if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) {
if (m_activeEditor != e.get()) {
m_activeEditor = e.get();
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
config->activeTabItemName = m_activeEditor->itemPath();
});
}
if (m_activeEditorUpdatePending == e.get()) {
m_activeEditorUpdatePending = nullptr;
}
if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
m_activeEditor->onActivated();
turbine::setConstantRefresh(m_ctx, m_activeEditor->requiresConstantRefresh());
}
e->draw(m_ctx);
m_activeEditorOnLastDraw = e.get();
ImGui::EndTabItem();
}
if (!open) {
e->close();
if (m_activeEditor == (*it).get()) {
m_activeEditor = nullptr;
}
try {
oxThrowError(m_editors.erase(it).moveTo(it));
} catch (ox::Exception const&ex) {
oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.file, ex.line);
} catch (std::exception const&ex) {
oxErrf("Editor tab deletion failed: {}\n", ex.what());
}
} else {
++it;
}
}
}
void StudioUI::loadEditorMaker(studio::EditorMaker const&editorMaker) noexcept {
for (auto const&ext : editorMaker.fileTypes) {
m_editorMakers[ext] = editorMaker.make;
}
}
void StudioUI::loadModule(studio::Module const*mod) noexcept {
for (auto const&editorMaker : mod->editors(m_ctx)) {
loadEditorMaker(editorMaker);
}
for (auto &im : mod->itemMakers(m_ctx)) {
m_newMenu.addItemMaker(std::move(im));
}
}
void StudioUI::loadModules() noexcept {
for (auto const mod : modules) {
loadModule(mod);
}
}
void StudioUI::toggleProjectExplorer() noexcept {
m_showProjectExplorer = !m_showProjectExplorer;
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
config->showProjectExplorer = m_showProjectExplorer;
});
}
void StudioUI::redo() noexcept {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack && undoStack->canRedo()) {
m_activeEditor->undoStack()->redo();
}
}
void StudioUI::undo() noexcept {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (undoStack && undoStack->canUndo()) {
m_activeEditor->undoStack()->undo();
}
}
void StudioUI::save() noexcept {
if (m_activeEditor && m_activeEditor->unsavedChanges()) {
m_activeEditor->save();
}
}
ox::Error StudioUI::openProject(ox::CRStringView path) noexcept {
oxRequireM(fs, keel::loadRomFs(path));
oxReturnError(keel::setRomFs(keelCtx(m_ctx), std::move(fs)));
turbine::setWindowTitle(m_ctx, ox::sfmt("{} - {}", keelCtx(m_ctx).appName, path));
m_project = ox::make_unique<studio::Project>(keelCtx(m_ctx), ox::String(path), m_projectDataDir);
auto sctx = applicationData<studio::StudioContext>(m_ctx);
sctx->project = m_project.get();
m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_openFiles.clear();
m_editors.clear();
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
config->projectPath = ox::String(path);
config->openFiles.clear();
});
return m_projectExplorer.refreshProjectTreeModel();
}
ox::Error StudioUI::openFile(ox::CRStringView path) noexcept {
return openFileActiveTab(path, true);
}
ox::Error StudioUI::openFileActiveTab(ox::CRStringView path, bool makeActiveTab) noexcept {
if (!m_project) {
return OxError(1, "No project open to open a file from");
}
if (m_openFiles.contains(path)) {
for (auto &e : m_editors) {
if (makeActiveTab && e->itemPath() == path) {
m_activeEditor = e.get();
m_activeEditorUpdatePending = e.get();
break;
}
}
return {};
}
oxRequire(ext, studio::fileExt(path).to<ox::String>([](auto const&v) {return ox::String(v);}));
// create Editor
studio::BaseEditor *editor = nullptr;
if (!m_editorMakers.contains(ext)) {
auto [obj, err] = m_project->loadObj<ox::ModelObject>(path);
if (err) {
return OxError(1, "There is no editor for this file extension");
}
editor = ox::make<ClawEditor>(path, std::move(obj));
} else {
auto const err = m_editorMakers[ext](path).moveTo(editor);
if (err) {
if constexpr(!ox::defines::Debug) {
oxErrf("Could not open Editor: {}\n", toStr(err));
} else {
oxErrf("Could not open Editor: {} ({}:{})\n", err.errCode, err.file, err.line);
}
return err;
}
}
editor->closed.connect(this, &StudioUI::closeFile);
m_editors.emplace_back(editor);
m_openFiles.emplace_back(path);
if (makeActiveTab) {
m_activeEditor = m_editors.back().value->get();
m_activeEditorUpdatePending = editor;
}
// save to config
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
if (!config->openFiles.contains(path)) {
config->openFiles.emplace_back(path);
}
});
return {};
}
ox::Error StudioUI::closeFile(ox::CRStringView path) noexcept {
if (!m_openFiles.contains(path)) {
return {};
}
oxIgnoreError(m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path)));
// save to config
studio::editConfig<StudioConfig>(keelCtx(m_ctx), [&](StudioConfig *config) {
oxIgnoreError(config->openFiles.erase(std::remove(config->openFiles.begin(), config->openFiles.end(), path)));
});
return {};
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <studio/editor.hpp>
#include <studio/module.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#include "aboutpopup.hpp"
#include "newmenu.hpp"
#include "projectexplorer.hpp"
#include "projecttreemodel.hpp"
namespace studio {
class StudioUI: public ox::SignalHandler {
friend class StudioUIDrawer;
private:
turbine::Context &m_ctx;
ox::String m_projectDataDir;
ox::UPtr<studio::Project> m_project;
studio::TaskRunner m_taskRunner;
ox::Vector<ox::UPtr<studio::BaseEditor>> m_editors;
ox::Vector<ox::UPtr<studio::Widget>> m_widgets;
ox::HashMap<ox::String, studio::EditorMaker::Func> m_editorMakers;
ProjectExplorer m_projectExplorer;
ox::Vector<ox::String> m_openFiles;
studio::BaseEditor *m_activeEditorOnLastDraw = nullptr;
studio::BaseEditor *m_activeEditor = nullptr;
studio::BaseEditor *m_activeEditorUpdatePending = nullptr;
NewMenu m_newMenu;
AboutPopup m_aboutPopup;
ox::Array<studio::Popup*, 2> const m_popups = {
&m_newMenu,
&m_aboutPopup
};
bool m_showProjectExplorer = true;
public:
explicit StudioUI(turbine::Context &ctx, ox::StringView projectDataDir) noexcept;
void update() noexcept;
void handleKeyEvent(turbine::Key, bool down) noexcept;
[[nodiscard]]
constexpr studio::Project *project() noexcept {
return m_project.get();
}
protected:
void draw() noexcept;
private:
void drawMenu() noexcept;
void drawTabBar() noexcept;
void drawTabs() noexcept;
void loadEditorMaker(studio::EditorMaker const&editorMaker) noexcept;
void loadModule(studio::Module const*mod) noexcept;
void loadModules() noexcept;
void toggleProjectExplorer() noexcept;
void redo() noexcept;
void undo() noexcept;
void save() noexcept;
ox::Error openProject(ox::CRStringView path) noexcept;
ox::Error openFile(ox::CRStringView path) noexcept;
ox::Error openFileActiveTab(ox::CRStringView path, bool makeActiveTab) noexcept;
ox::Error closeFile(ox::CRStringView path) noexcept;
};
}

View File

@ -0,0 +1 @@
add_subdirectory(src)

View File

@ -0,0 +1,95 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/fs/fs.hpp>
#include <ox/model/typenamecatcher.hpp>
#include <ox/oc/oc.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/defines.hpp>
#include <ox/std/fmt.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/string.hpp>
#include <keel/context.hpp>
namespace studio {
[[nodiscard]]
ox::String configPath(keel::Context const&ctx) noexcept;
template<typename T>
ox::Result<T> readConfig(keel::Context &ctx, ox::CRStringView name) noexcept {
oxAssert(name != "", "Config type has no TypeName");
auto const path = ox::sfmt("/{}.json", name);
ox::PassThroughFS fs(configPath(ctx));
auto const [buff, err] = fs.read(path);
if (err) {
oxErrf("Could not read config file: {}\n", toStr(err));
return err;
}
return ox::readOC<T>(buff);
}
template<typename T>
ox::Result<T> readConfig(keel::Context &ctx) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
return readConfig<T>(ctx, TypeName);
}
template<typename T>
ox::Error writeConfig(keel::Context &ctx, ox::CRStringView name, T *data) noexcept {
oxAssert(name != "", "Config type has no TypeName");
auto const path = ox::sfmt("/{}.json", name);
ox::PassThroughFS fs(configPath(ctx));
if (auto const err = fs.mkdir("/", true)) {
oxErrf("Could not create config directory: {}\n", toStr(err));
return err;
}
oxRequireM(buff, ox::writeOC(*data));
*buff.back().value = '\n';
if (auto const err = fs.write(path, buff.data(), buff.size())) {
oxErrf("Could not read config file: {}\n", toStr(err));
return OxError(2, "Could not read config file");
}
return {};
}
template<typename T>
ox::Error writeConfig(keel::Context &ctx, T *data) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
return writeConfig(ctx, TypeName, data);
}
template<typename T, typename Func>
void openConfig(keel::Context &ctx, ox::CRStringView name, Func f) noexcept {
oxAssert(name != "", "Config type has no TypeName");
auto const [c, err] = readConfig<T>(ctx, name);
oxLogError(err);
f(&c);
}
template<typename T, typename Func>
void openConfig(keel::Context &ctx, Func f) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
openConfig<T>(ctx, TypeName, f);
}
template<typename T, typename Func>
void editConfig(keel::Context &ctx, ox::CRStringView name, Func f) noexcept {
oxAssert(name != "", "Config type has no TypeName");
auto [c, err] = readConfig<T>(ctx, name);
oxLogError(err);
f(&c);
oxLogError(writeConfig(ctx, name, &c));
}
template<typename T, typename Func>
void editConfig(keel::Context &ctx, Func f) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>();
editConfig<T>(ctx, TypeName, f);
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include "project.hpp"
namespace studio {
struct StudioContext {
ox::SignalHandler *ui = nullptr;
Project *project = nullptr;
};
}

View File

@ -0,0 +1,139 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include "undostack.hpp"
#include "widget.hpp"
namespace studio {
class StudioUI;
class BaseEditor: public Widget {
friend StudioUI;
private:
bool m_unsavedChanges = false;
bool m_exportable = false;
bool m_cutEnabled = false;
bool m_copyEnabled = false;
bool m_pasteEnabled = false;
bool m_requiresConstantRefresh = false;
public:
~BaseEditor() override = default;
/**
* Returns the name of item being edited.
*/
[[nodiscard]]
virtual ox::CStringView itemPath() const noexcept = 0;
[[nodiscard]]
virtual ox::CStringView itemDisplayName() const noexcept;
virtual void cut();
virtual void copy();
virtual void paste();
virtual void exportFile();
virtual void keyStateChanged(turbine::Key key, bool down);
virtual void onActivated() noexcept;
[[nodiscard]]
bool requiresConstantRefresh() const noexcept;
void close() const;
/**
* Save changes to item being edited.
*/
void save() noexcept;
/**
* Sets indication of item being edited has unsaved changes. Also emits
* unsavedChangesChanged signal.
*/
void setUnsavedChanges(bool);
[[nodiscard]]
bool unsavedChanges() const noexcept;
void setExportable(bool);
[[nodiscard]]
bool exportable() const;
void setCutEnabled(bool);
[[nodiscard]]
bool cutEnabled() const noexcept;
void setCopyEnabled(bool);
[[nodiscard]]
bool copyEnabled() const noexcept;
void setPasteEnabled(bool);
[[nodiscard]]
bool pasteEnabled() const noexcept;
protected:
/**
* Save changes to item being edited.
*/
virtual ox::Error saveItem() noexcept;
/**
* Returns the undo stack holding changes to the item being edited.
*/
[[nodiscard]]
virtual UndoStack *undoStack() noexcept;
void setRequiresConstantRefresh(bool value) noexcept;
// signals
public:
ox::Signal<ox::Error(bool)> unsavedChangesChanged;
ox::Signal<ox::Error(bool)> exportableChanged;
ox::Signal<ox::Error(bool)> cutEnabledChanged;
ox::Signal<ox::Error(bool)> copyEnabledChanged;
ox::Signal<ox::Error(bool)> pasteEnabledChanged;
ox::Signal<ox::Error(ox::StringView)> closed;
};
class Editor: public studio::BaseEditor {
private:
studio::UndoStack m_undoStack;
ox::String m_itemPath;
ox::String m_itemName;
public:
Editor(ox::StringView itemPath) noexcept;
[[nodiscard]]
ox::CStringView itemPath() const noexcept final;
[[nodiscard]]
ox::CStringView itemDisplayName() const noexcept final;
[[nodiscard]]
UndoStack *undoStack() noexcept final;
private:
ox::Error markUnsavedChanges(UndoCommand const*) noexcept;
};
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/defines.hpp>
namespace studio {
struct FDFilterItem {
#ifdef OX_OS_Windows
using String = ox::Vector<wchar_t>;
#else
using String = ox::Vector<char>;
#endif
String name{};
String spec{};
constexpr FDFilterItem() noexcept = default;
FDFilterItem(ox::CRStringView pName, ox::CRStringView pSpec) noexcept;
};
ox::Result<ox::String> saveFile(ox::Vector<FDFilterItem> const&exts) noexcept;
ox::Result<ox::String> chooseDirectory() noexcept;
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <turbine/context.hpp>
namespace studio::ig {
void centerNextWindow(turbine::Context &ctx) noexcept;
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <keel/media.hpp>
#include <turbine/context.hpp>
#include "context.hpp"
namespace studio {
class ItemMaker {
public:
ox::String const name;
ox::String const parentDir;
ox::String const fileExt;
constexpr explicit ItemMaker(ox::StringView pName, ox::StringView pParentDir, ox::CRStringView pFileExt) noexcept:
name(pName),
parentDir(pParentDir),
fileExt(pFileExt) {
}
virtual ~ItemMaker() noexcept = default;
virtual ox::Error write(turbine::Context &ctx, ox::CRStringView pName) const noexcept = 0;
};
template<typename T>
class ItemMakerT: public ItemMaker {
private:
T const m_item;
ox::ClawFormat const m_fmt;
public:
constexpr ItemMakerT(
ox::StringView pDisplayName,
ox::StringView pParentDir,
ox::StringView fileExt,
ox::ClawFormat pFmt = ox::ClawFormat::Metal) noexcept:
ItemMaker(pDisplayName, pParentDir, fileExt),
m_fmt(pFmt) {
}
constexpr ItemMakerT(
ox::StringView pDisplayName,
ox::StringView pParentDir,
ox::StringView fileExt,
T pItem,
ox::ClawFormat pFmt) noexcept:
ItemMaker(pDisplayName, pParentDir, fileExt),
m_item(pItem),
m_fmt(pFmt) {
}
constexpr ItemMakerT(
ox::StringView pDisplayName,
ox::StringView pParentDir,
ox::StringView fileExt,
T &&pItem,
ox::ClawFormat pFmt) noexcept:
ItemMaker(pDisplayName, pParentDir, fileExt),
m_item(std::move(pItem)),
m_fmt(pFmt) {
}
ox::Error write(turbine::Context &ctx, ox::CRStringView pName) const noexcept override {
auto const path = ox::sfmt("/{}/{}.{}", parentDir, pName, fileExt);
auto sctx = turbine::applicationData<studio::StudioContext>(ctx);
keel::createUuidMapping(keelCtx(ctx), path, ox::UUID::generate().unwrap());
return sctx->project->writeObj(path, m_item, m_fmt);
}
};
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <functional>
#include <ox/std/string.hpp>
#include <ox/std/vector.hpp>
#include <turbine/context.hpp>
#include <studio/itemmaker.hpp>
namespace studio {
class ItemMaker;
struct EditorMaker {
using Func = std::function<ox::Result<class BaseEditor*>(ox::CRStringView)>;
ox::Vector<ox::String> fileTypes;
Func make;
};
class Module {
public:
virtual ~Module() noexcept = default;
virtual ox::Vector<EditorMaker> editors(turbine::Context &ctx) const;
virtual ox::Vector<ox::UPtr<ItemMaker>> itemMakers(turbine::Context&) const;
};
template<typename Editor>
[[nodiscard]]
studio::EditorMaker editorMaker(turbine::Context &ctx, ox::CRStringView ext) noexcept {
return {
{ox::String(ext)},
[&ctx](ox::CRStringView path) -> ox::Result<studio::BaseEditor*> {
return ox::makeCatch<Editor>(ctx, path);
}
};
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <functional>
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <ox/std/vec.hpp>
#include <turbine/context.hpp>
namespace studio {
class Popup {
private:
ox::Vec2 m_size;
ox::String m_title;
public:
// emits path parameter
ox::Signal<ox::Error(ox::String const&)> finished;
virtual ~Popup() noexcept = default;
virtual void open() noexcept = 0;
virtual void close() noexcept = 0;
[[nodiscard]]
virtual bool isOpen() const noexcept = 0;
virtual void draw(turbine::Context &ctx) noexcept = 0;
protected:
constexpr void setSize(ox::Size sz) noexcept {
m_size = {static_cast<float>(sz.width), static_cast<float>(sz.height)};
}
constexpr void setTitle(ox::String title) noexcept {
m_title = std::move(title);
}
constexpr ox::String const&title() const noexcept {
return m_title;
}
void drawWindow(turbine::Context &ctx, bool *open, std::function<void()> const&drawContents);
};
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/read.hpp>
#include <ox/claw/write.hpp>
#include <ox/event/signal.hpp>
#include <ox/fs/fs.hpp>
#include <ox/mc/mc.hpp>
#include <ox/model/descwrite.hpp>
#include <ox/std/hashmap.hpp>
#include <keel/typestore.hpp>
#include <keel/media.hpp>
namespace studio {
enum class ProjectEvent {
None,
FileAdded,
// FileRecognized is triggered for all matching files upon a new
// subscription to a section of the project and upon the addition of a file.
FileRecognized,
FileDeleted,
FileUpdated,
};
[[nodiscard]]
constexpr ox::Result<ox::StringView> fileExt(ox::CRStringView path) noexcept {
auto const extStart = ox::find(path.crbegin(), path.crend(), '.').offset();
if (!extStart) {
return OxError(1, "Cannot open a file without valid extension.");
}
return substr(path, extStart + 1);
}
class Project {
private:
keel::Context &m_ctx;
ox::String m_path;
ox::String m_projectDataDir;
mutable keel::TypeStore m_typeStore;
ox::FileSystem &m_fs;
ox::HashMap<ox::String, ox::Vector<ox::String>> m_fileExtFileMap;
public:
explicit Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDataDir) noexcept;
ox::Error create() noexcept;
[[nodiscard]]
ox::FileSystem *romFs() noexcept;
ox::Error mkdir(ox::CRStringView path) const noexcept;
/**
* Writes a MetalClaw object to the project at the given path.
*/
template<typename T>
ox::Error writeObj(
ox::CRStringView path,
T const&obj,
ox::ClawFormat fmt = ox::ClawFormat::Metal) noexcept;
template<typename T>
ox::Result<T> loadObj(ox::CRStringView path) const noexcept;
ox::Result<ox::FileStat> stat(ox::CRStringView path) const noexcept;
[[nodiscard]]
bool exists(ox::CRStringView path) const noexcept;
template<typename Functor>
ox::Error subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept;
[[nodiscard]]
ox::Vector<ox::String> const&fileList(ox::CRStringView ext) noexcept;
private:
void buildFileIndex() noexcept;
void indexFile(ox::CRStringView path) noexcept;
ox::Error writeBuff(ox::CRStringView path, ox::Buffer const&buff) noexcept;
ox::Result<ox::Buffer> loadBuff(ox::CRStringView path) const noexcept;
ox::Error lsProcDir(ox::Vector<ox::String> *paths, ox::CRStringView path) const noexcept;
ox::Result<ox::Vector<ox::String>> listFiles(ox::CRStringView path = "") const noexcept;
// signals
public:
ox::Signal<ox::Error(ProjectEvent, ox::CRStringView)> fileEvent;
ox::Signal<ox::Error(ox::CRStringView)> fileAdded;
// FileRecognized is triggered for all matching files upon a new
// subscription to a section of the project and upon the addition of a
// file.
ox::Signal<ox::Error(ox::CRStringView)> fileRecognized;
ox::Signal<ox::Error(ox::CRStringView)> fileDeleted;
ox::Signal<ox::Error(ox::CRStringView)> fileUpdated;
};
template<typename T>
ox::Error Project::writeObj(ox::CRStringView path, T const&obj, ox::ClawFormat fmt) noexcept {
oxRequireM(buff, ox::writeClaw(obj, fmt));
// write to FS
oxReturnError(writeBuff(path, buff));
// write type descriptor
if (m_typeStore.get<T>().error) {
oxReturnError(ox::buildTypeDef(&m_typeStore, &obj));
}
// write out type store
auto const descPath = ox::sfmt("/{}/type_descriptors", m_projectDataDir);
oxReturnError(mkdir(descPath));
for (auto const&t : m_typeStore.typeList()) {
oxRequireM(typeOut, ox::writeClaw(*t, ox::ClawFormat::Organic));
// replace garbage last character with new line
*typeOut.back().value = '\n';
// write to FS
auto const typePath = ox::sfmt("/{}/{}", descPath, buildTypeId(*t));
oxReturnError(writeBuff(typePath, typeOut));
}
oxReturnError(keel::setAsset(m_ctx, path, obj));
fileUpdated.emit(path);
return {};
}
template<typename T>
ox::Result<T> Project::loadObj(ox::CRStringView path) const noexcept {
oxRequire(buff, loadBuff(path));
if constexpr(ox::is_same_v<T, ox::ModelObject>) {
return keel::readAsset(m_typeStore, buff);
} else {
return keel::readAsset<T>(buff);
}
}
template<typename Functor>
ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept {
switch (e) {
case ProjectEvent::None:
break;
case ProjectEvent::FileAdded:
connect(this, &Project::fileAdded, tgt, slot);
break;
case ProjectEvent::FileRecognized:
{
oxRequire(files, listFiles());
for (auto const&f : files) {
slot(f);
}
connect(this, &Project::fileRecognized, tgt, slot);
break;
}
case ProjectEvent::FileDeleted:
connect(this, &Project::fileDeleted, tgt, slot);
break;
case ProjectEvent::FileUpdated:
connect(this, &Project::fileUpdated, tgt, slot);
break;
}
return {};
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <studio/context.hpp>
#include <studio/editor.hpp>
#include <studio/filedialog.hpp>
#include <studio/imguiuitl.hpp>
#include <studio/module.hpp>
#include <studio/itemmaker.hpp>
#include <studio/popup.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#include <studio/undostack.hpp>
#include <studio/widget.hpp>

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <turbine/context.hpp>
namespace studio {
enum class TaskState {
Running,
Done
};
class Task: public ox::SignalHandler {
public:
ox::Signal<ox::Error()> finished;
~Task() noexcept override = default;
virtual TaskState update(turbine::Context &ctx) noexcept = 0;
};
class TaskRunner {
private:
ox::Vector<ox::UniquePtr<studio::Task>> m_tasks;
public:
void update(turbine::Context &ctx) noexcept;
void add(Task &task) noexcept;
};
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/error.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/vector.hpp>
namespace studio {
class UndoCommand {
public:
virtual ~UndoCommand() noexcept = default;
virtual void redo() noexcept = 0;
virtual void undo() noexcept = 0;
[[nodiscard]]
virtual int commandId() const noexcept = 0;
virtual bool mergeWith(UndoCommand const*cmd) noexcept;
};
class UndoStack {
private:
ox::Vector<ox::UPtr<UndoCommand>> m_stack;
std::size_t m_stackIdx = 0;
public:
void push(ox::UPtr<UndoCommand> &&cmd) noexcept;
void redo() noexcept;
void undo() noexcept;
[[nodiscard]]
constexpr bool canRedo() const noexcept {
return m_stackIdx < m_stack.size();
}
[[nodiscard]]
constexpr bool canUndo() const noexcept {
return m_stackIdx;
}
ox::Signal<ox::Error(studio::UndoCommand const*)> redoTriggered;
ox::Signal<ox::Error(studio::UndoCommand const*)> undoTriggered;
ox::Signal<ox::Error(studio::UndoCommand const*)> changeTriggered;
};
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <turbine/context.hpp>
namespace studio {
class Widget: public ox::SignalHandler {
public:
~Widget() noexcept override = default;
virtual void draw(turbine::Context&) noexcept = 0;
};
}

View File

@ -0,0 +1,45 @@
add_library(
Studio
configio.cpp
editor.cpp
imguiutil.cpp
module.cpp
popup.cpp
project.cpp
task.cpp
undostack.cpp
filedialog_nfd.cpp
)
target_include_directories(
Studio PUBLIC
../include
)
include_directories(
SYSTEM
${GTK3_INCLUDE_DIRS}
)
target_link_libraries(
Studio PUBLIC
nfd
OxEvent
GlUtils
Turbine
)
install(
TARGETS
Studio
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
install(
DIRECTORY
../include/studio
DESTINATION
include/studio/
)

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/configio.hpp>
namespace studio {
constexpr auto ConfigDir = [] {
switch (ox::defines::OS) {
case ox::OS::Darwin:
return "{}/Library/Preferences/{}";
case ox::OS::DragonFlyBSD:
case ox::OS::FreeBSD:
case ox::OS::Linux:
case ox::OS::NetBSD:
case ox::OS::OpenBSD:
return "{}/.config/{}";
case ox::OS::Windows:
return R"({}/AppData/Local/{})";
case ox::OS::BareMetal:
return "";
}
}();
ox::String configPath(keel::Context const&ctx) noexcept {
auto const homeDir = std::getenv(ox::defines::OS == ox::OS::Windows ? "USERPROFILE" : "HOME");
return ox::sfmt(ConfigDir, homeDir, ctx.appName);
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <ox/std/string.hpp>
#include <studio/editor.hpp>
namespace studio {
ox::CStringView BaseEditor::itemDisplayName() const noexcept {
return itemPath();
}
void BaseEditor::cut() {
}
void BaseEditor::copy() {
}
void BaseEditor::paste() {
}
void BaseEditor::exportFile() {
}
void BaseEditor::keyStateChanged(turbine::Key, bool) {
}
void BaseEditor::onActivated() noexcept {
}
bool BaseEditor::requiresConstantRefresh() const noexcept {
return m_requiresConstantRefresh;
}
void BaseEditor::close() const {
this->closed.emit(itemPath());
}
void BaseEditor::save() noexcept {
auto const err = saveItem();
if (!err) {
setUnsavedChanges(false);
} else {
if constexpr(ox::defines::Debug) {
oxErrorf("Could not save file {}: {} ({}:{})", itemPath(), toStr(err), err.file, err.line);
} else {
oxErrorf("Could not save file {}: {}", itemPath(), toStr(err));
}
}
}
void BaseEditor::setUnsavedChanges(bool uc) {
m_unsavedChanges = uc;
unsavedChangesChanged.emit(uc);
}
bool BaseEditor::unsavedChanges() const noexcept {
return m_unsavedChanges;
}
void BaseEditor::setExportable(bool exportable) {
m_exportable = exportable;
exportableChanged.emit(exportable);
}
bool BaseEditor::exportable() const {
return m_exportable;
}
void BaseEditor::setCutEnabled(bool v) {
m_cutEnabled = v;
cutEnabledChanged.emit(v);
}
bool BaseEditor::cutEnabled() const noexcept {
return m_cutEnabled;
}
void BaseEditor::setCopyEnabled(bool v) {
m_copyEnabled = v;
copyEnabledChanged.emit(v);
}
bool BaseEditor::copyEnabled() const noexcept {
return m_copyEnabled;
}
void BaseEditor::setPasteEnabled(bool v) {
m_pasteEnabled = v;
pasteEnabledChanged.emit(v);
}
bool BaseEditor::pasteEnabled() const noexcept {
return m_pasteEnabled;
}
ox::Error BaseEditor::saveItem() noexcept {
return {};
}
UndoStack *BaseEditor::undoStack() noexcept {
return nullptr;
}
void BaseEditor::setRequiresConstantRefresh(bool value) noexcept {
m_requiresConstantRefresh = value;
}
Editor::Editor(ox::StringView itemPath) noexcept:
m_itemPath(itemPath),
m_itemName(m_itemPath.substr(std::find(m_itemPath.rbegin(), m_itemPath.rend(), '/').offset() + 1)) {
m_undoStack.changeTriggered.connect(this, &Editor::markUnsavedChanges);
}
[[nodiscard]]
ox::CStringView Editor::itemPath() const noexcept {
return m_itemPath;
}
[[nodiscard]]
ox::CStringView Editor::itemDisplayName() const noexcept {
return m_itemName;
}
UndoStack *Editor::undoStack() noexcept {
return &m_undoStack;
}
ox::Error Editor::markUnsavedChanges(UndoCommand const*) noexcept {
setUnsavedChanges(true);
return {};
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <nfd.hpp>
#include <ox/std/error.hpp>
#include <ox/std/string.hpp>
#include <studio/filedialog.hpp>
namespace studio {
FDFilterItem::FDFilterItem(ox::CRStringView pName, ox::CRStringView pSpec) noexcept {
name.resize(pName.len() + 1);
ox_strncpy(name.data(), pName.data(), pName.len());
spec.resize(pSpec.len() + 1);
ox_strncpy(spec.data(), pSpec.data(), pSpec.len());
}
static ox::Result<ox::String> toResult(nfdresult_t r, NFD::UniquePathN const&path) noexcept {
switch (r) {
case NFD_OKAY: {
ox::String out;
for (auto i = 0u; path.get()[i]; ++i) {
auto const c = static_cast<char>(path.get()[i]);
oxIgnoreError(out.append(&c, 1));
}
return out;
}
case NFD_CANCEL:
return OxError(1, "Operation cancelled");
default:
return OxError(2, NFD::GetError());
}
}
ox::Result<ox::String> saveFile(ox::Vector<FDFilterItem> const&filters) noexcept {
NFD::Guard const guard;
NFD::UniquePathN path;
ox::Vector<nfdnfilteritem_t, 5> filterItems(filters.size());
for (auto i = 0u; auto const&f : filters) {
filterItems[i].name = f.name.data();
filterItems[i].spec = f.spec.data();
++i;
}
auto const filterItemsCnt = static_cast<nfdfiltersize_t>(filterItems.size());
return toResult(NFD::SaveDialog(path, filterItems.data(), filterItemsCnt), path);
}
ox::Result<ox::String> chooseDirectory() noexcept {
const NFD::Guard guard;
NFD::UniquePathN path;
return toResult(NFD::PickFolder(path), path);
}
}

View File

@ -0,0 +1,19 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <imgui.h>
#include <turbine/gfx.hpp>
namespace studio::ig {
void centerNextWindow(turbine::Context &ctx) noexcept {
auto const sz = turbine::getScreenSize(ctx);
auto const screenW = static_cast<float>(sz.width);
auto const screenH = static_cast<float>(sz.height);
auto const mod = ImGui::GetWindowDpiScale() * 2;
ImGui::SetNextWindowPos(ImVec2(screenW / mod, screenH / mod), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/module.hpp>
namespace studio {
ox::Vector<EditorMaker> Module::editors(turbine::Context&) const {
return {};
}
ox::Vector<ox::UPtr<ItemMaker>> Module::itemMakers(turbine::Context&) const {
return {};
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/imguiuitl.hpp>
#include <studio/popup.hpp>
namespace studio {
void Popup::drawWindow(turbine::Context &ctx, bool *open, std::function<void()> const&drawContents) {
studio::ig::centerNextWindow(ctx);
ImGui::SetNextWindowSize(static_cast<ImVec2>(m_size));
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
if (ImGui::BeginPopupModal(m_title.c_str(), open, modalFlags)) {
drawContents();
ImGui::EndPopup();
}
}
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <filesystem>
#include <ox/std/std.hpp>
#include <keel/module.hpp>
#include <studio/project.hpp>
namespace studio {
static void generateTypes(ox::TypeStore &ts) noexcept {
for (auto const mod : keel::modules()) {
for (auto gen : mod->types()) {
oxLogError(gen(ts));
}
}
}
Project::Project(keel::Context &ctx, ox::String path, ox::CRStringView projectDataDir) noexcept:
m_ctx(ctx),
m_path(std::move(path)),
m_projectDataDir(projectDataDir),
m_typeStore(*m_ctx.rom, ox::sfmt("/{}/type_descriptors", projectDataDir)),
m_fs(*m_ctx.rom) {
oxTracef("studio", "Project: {}", m_path);
generateTypes(m_typeStore);
buildFileIndex();
}
ox::Error Project::create() noexcept {
std::error_code ec;
std::filesystem::create_directory(m_path.toStdString(), ec);
return OxError(static_cast<ox::ErrorCode>(ec.value()), "PassThroughFS: mkdir failed");
}
ox::FileSystem *Project::romFs() noexcept {
return &m_fs;
}
ox::Error Project::mkdir(ox::CRStringView path) const noexcept {
oxReturnError(m_fs.mkdir(path, true));
fileUpdated.emit(path);
return {};
}
ox::Result<ox::FileStat> Project::stat(ox::CRStringView path) const noexcept {
return m_fs.stat(path);
}
bool Project::exists(ox::CRStringView path) const noexcept {
return m_fs.stat(path).error == 0;
}
ox::Vector<ox::String> const&Project::fileList(ox::CRStringView ext) noexcept {
return m_fileExtFileMap[ext];
}
void Project::buildFileIndex() noexcept {
auto [files, err] = listFiles();
if (err) {
oxLogError(err);
return;
}
m_fileExtFileMap.clear();
std::sort(files.begin(), files.end());
for (auto const&file : files) {
if (!beginsWith(file, ox::sfmt("/.{}/", m_projectDataDir))) {
indexFile(file);
}
}
}
void Project::indexFile(ox::CRStringView path) noexcept {
auto const [ext, err] = fileExt(path);
if (err) {
return;
}
m_fileExtFileMap[ext].emplace_back(path);
}
ox::Error Project::writeBuff(ox::CRStringView path, ox::Buffer const&buff) noexcept {
constexpr auto HdrSz = 40;
ox::Buffer outBuff;
outBuff.reserve(buff.size() + HdrSz);
ox::BufferWriter writer(&outBuff);
auto const [uuid, err] = m_ctx.pathToUuid.at(path);
if (!err) {
oxReturnError(keel::writeUuidHeader(writer, *uuid));
}
oxReturnError(writer.write(buff.data(), buff.size()));
auto const newFile = m_fs.stat(path).error != 0;
oxReturnError(m_fs.write(path, outBuff.data(), outBuff.size(), ox::FileType::NormalFile));
if (newFile) {
fileAdded.emit(path);
indexFile(path);
} else {
fileUpdated.emit(path);
}
return {};
}
ox::Result<ox::Buffer> Project::loadBuff(ox::CRStringView path) const noexcept {
return m_fs.read(path);
}
ox::Error Project::lsProcDir(ox::Vector<ox::String> *paths, ox::CRStringView path) const noexcept {
oxRequire(files, m_fs.ls(path));
for (auto const&name : files) {
auto fullPath = ox::sfmt("{}/{}", path, name);
oxRequire(stat, m_fs.stat(ox::StringView(fullPath)));
switch (stat.fileType) {
case ox::FileType::NormalFile:
paths->emplace_back(std::move(fullPath));
break;
case ox::FileType::Directory:
oxReturnError(lsProcDir(paths, fullPath));
break;
case ox::FileType::None:
break;
}
}
return {};
}
ox::Result<ox::Vector<ox::String>> Project::listFiles(ox::CRStringView path) const noexcept {
ox::Vector<ox::String> paths;
oxReturnError(lsProcDir(&paths, path));
return paths;
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <algorithm>
#include <studio/task.hpp>
namespace studio {
void TaskRunner::update(turbine::Context &ctx) noexcept {
oxIgnoreError(m_tasks.erase(std::remove_if(m_tasks.begin(), m_tasks.end(), [&](ox::UPtr<studio::Task> &t) {
auto const done = t->update(ctx) == TaskState::Done;
if (done) {
t->finished.emit();
}
return done;
})));
}
void TaskRunner::add(Task &task) noexcept {
m_tasks.emplace_back(&task);
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/undostack.hpp>
namespace studio {
bool UndoCommand::mergeWith(UndoCommand const*) noexcept {
return false;
}
void UndoStack::push(ox::UPtr<UndoCommand> &&cmd) noexcept {
for (auto const i = m_stackIdx; i < m_stack.size();) {
oxIgnoreError(m_stack.erase(i));
}
cmd->redo();
redoTriggered.emit(cmd.get());
changeTriggered.emit(cmd.get());
if (m_stack.empty() || !(*m_stack.back().value)->mergeWith(cmd.get())) {
m_stack.emplace_back(std::move(cmd));
++m_stackIdx;
}
}
void UndoStack::redo() noexcept {
if (m_stackIdx < m_stack.size()) {
auto &c = m_stack[m_stackIdx++];
c->redo();
redoTriggered.emit(c.get());
changeTriggered.emit(c.get());
}
}
void UndoStack::undo() noexcept {
if (m_stackIdx) {
auto &c = m_stack[--m_stackIdx];
c->undo();
undoTriggered.emit(c.get());
changeTriggered.emit(c.get());
}
}
}

View File

@ -0,0 +1 @@
add_subdirectory(src)

View File

@ -0,0 +1,50 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/claw/claw.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include "context.hpp"
namespace turbine {
class BaseClipboardObject {
public:
virtual ~BaseClipboardObject() noexcept = default;
[[nodiscard]]
virtual ox::String typeId() const noexcept = 0;
[[nodiscard]]
constexpr auto typeMatch(ox::StringView name, int version) const noexcept {
return typeId() == ox::buildTypeId(name, version);
}
};
template<typename T>
class ClipboardObject: public BaseClipboardObject {
[[nodiscard]]
ox::String typeId() const noexcept final {
return ox::buildTypeId(T::TypeName, T::TypeVersion);
}
};
ox::String getClipboardText(Context &ctx) noexcept;
void setClipboardText(Context &ctx, ox::CRStringView text) noexcept;
void setClipboardObject(Context &ctx, ox::UPtr<BaseClipboardObject> &&obj) noexcept;
ox::Result<BaseClipboardObject*> getClipboardData(Context &ctx, ox::StringView typeName, int typeVersion) noexcept;
template<typename T>
ox::Result<T*> getClipboardObject(Context &ctx) noexcept {
oxRequire(p, getClipboardData(ctx, T::TypeName, T::TypeVersion));
return dynamic_cast<T*>(p);
}
}

View File

@ -0,0 +1,56 @@
/*
* 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 <keel/context.hpp>
#include "input.hpp"
namespace turbine {
class Context;
struct ContextDeleter {
void operator()(Context *p) noexcept;
};
using ContextUPtr = ox::UPtr<Context, ContextDeleter>;
void shutdown(Context &ctx) noexcept;
keel::Context const&keelCtx(Context const&ctx) noexcept;
keel::Context &keelCtx(Context &ctx) noexcept;
inline ox::FileSystem const*rom(Context const&ctx) noexcept {
return keelCtx(ctx).rom.get();
}
inline ox::FileSystem *rom(Context &ctx) noexcept {
return keelCtx(ctx).rom.get();
}
void setApplicationData(Context &ctx, void *applicationData) noexcept;
[[nodiscard]]
void *applicationDataRaw(Context &ctx) noexcept;
template<typename T>
[[nodiscard]]
T *applicationData(Context &ctx) noexcept {
return static_cast<T*>(applicationDataRaw(ctx));
}
void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept;
KeyEventHandler keyEventHandler(Context &ctx) noexcept;
}

View File

@ -0,0 +1,17 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace turbine {
class Context;
using UpdateHandler = int(*)(Context&);
// Sets event handler that sleeps for the time given in the return value. The
// sleep time is a minimum of ~16 milliseconds.
void setUpdateHandler(Context &ctx, UpdateHandler) noexcept;
}

View File

@ -0,0 +1,48 @@
/*
* 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/model/def.hpp>
#include "context.hpp"
namespace turbine {
namespace gl {
class Drawer {
public:
virtual ~Drawer() = default;
virtual void draw(Context&) noexcept = 0;
};
void addDrawer(Context &ctx, Drawer *cd) noexcept;
void removeDrawer(Context &ctx, Drawer *cd) noexcept;
}
ox::Error initGfx(Context &ctx) noexcept;
void setWindowTitle(Context &ctx, ox::CRStringView title) noexcept;
void focusWindow(Context &ctx) noexcept;
[[nodiscard]]
int getScreenWidth(Context &ctx) noexcept;
[[nodiscard]]
int getScreenHeight(Context &ctx) noexcept;
[[nodiscard]]
ox::Size getScreenSize(Context &ctx) noexcept;
ox::Bounds getWindowBounds(Context &ctx) noexcept;
ox::Error setWindowBounds(Context &ctx, ox::Bounds const&bnds) noexcept;
void setConstantRefresh(Context &ctx, bool r) noexcept;
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/defines.hpp>
namespace turbine {
enum Key {
// GBA implementation currently relies on GamePad entry order
GamePad_A = 0,
GamePad_B,
GamePad_Select,
GamePad_Start,
GamePad_Right,
GamePad_Left,
GamePad_Up,
GamePad_Down,
GamePad_R,
GamePad_L,
Num_0,
Num_1,
Num_2,
Num_3,
Num_4,
Num_5,
Num_6,
Num_7,
Num_8,
Num_9,
Alpha_A,
Alpha_B,
Alpha_C,
Alpha_D,
Alpha_E,
Alpha_F,
Alpha_G,
Alpha_H,
Alpha_I,
Alpha_J,
Alpha_K,
Alpha_L,
Alpha_M,
Alpha_N,
Alpha_O,
Alpha_P,
Alpha_Q,
Alpha_R,
Alpha_S,
Alpha_T,
Alpha_U,
Alpha_V,
Alpha_W,
Alpha_X,
Alpha_Y,
Alpha_Z,
Mod_Alt,
Mod_Ctrl,
Mod_Super,
Mod_Shift,
Escape,
End
};
class Context;
[[nodiscard]]
bool buttonDown(Context const&ctx, Key) noexcept;
using KeyEventHandler = void(*)(Context&, Key, bool);
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/memory.hpp>
#include <ox/fs/fs.hpp>
#include "clipboard.hpp"
#include "event.hpp"
#include "gfx.hpp"
#include "input.hpp"
namespace turbine {
ox::Result<ContextUPtr> init(ox::UPtr<ox::FileSystem> &&fs, ox::CRStringView appName) noexcept;
ox::Error run(Context &ctx) noexcept;
// Returns the number of milliseconds that have passed since the start of the
// program.
[[nodiscard]]
uint64_t ticksMs(Context const&ctx) noexcept;
void requestShutdown(Context &ctx) noexcept;
}

View File

@ -0,0 +1,40 @@
add_library(Turbine)
set(TURBINE_BACKEND_GBA ${TURBINE_BUILD_TYPE} STREQUAL "GBA")
set(TURBINE_BACKEND_GLFW NOT ${TURBINE_BACKEND_GBA})
add_subdirectory(gba)
if(${TURBINE_BACKEND_GLFW})
add_subdirectory(glfw)
endif()
target_include_directories(
Turbine PUBLIC
../include
)
target_link_libraries(
Turbine PUBLIC
Keel
)
target_compile_definitions(
Turbine PRIVATE
TURBINE_BACKEND_GBA=$<IF:$<BOOL:${TURBINE_BACKEND_GBA}>,1,0>
TURBINE_BACKEND_GLFW=$<IF:$<BOOL:${TURBINE_BACKEND_GLFW}>,1,0>
)
install(
DIRECTORY
../include/turbine
DESTINATION
include/turbine
)
install(
TARGETS
Turbine
DESTINATION
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)

View File

@ -0,0 +1,46 @@
option(TURBINE_GBA_EVENT_LOOP_TIMER_BASED "Run event loop on time instead of vsync" OFF)
set(TURBINE_GBA_TIMER_BITS "32" CACHE STRING "Bits for system time (16, 32, 64)")
add_library(Turbine-GBA OBJECT)
target_sources(
Turbine-GBA PRIVATE
context.cpp
clipboard.cpp
event.cpp
gfx.cpp
irq.arm.cpp
turbine.arm.cpp
turbine.cpp
)
target_compile_definitions(
Turbine-GBA PRIVATE
TURBINE_GBA_EVENT_LOOP_TIMER_BASED=$<IF:$<BOOL:${TURBINE_GBA_EVENT_LOOP_TIMER_BASED}>,true,false>
TURBINE_GBA_TIMER_BITS=${TURBINE_GBA_TIMER_BITS}
)
if(TURBINE_BUILD_TYPE STREQUAL "GBA")
enable_language(ASM)
set_source_files_properties(turbine.arm.cpp irq.arm.cpp PROPERTIES COMPILE_FLAGS -marm)
target_sources(
Turbine-GBA PRIVATE
irq.s
)
target_link_libraries(Turbine PUBLIC Turbine-GBA)
else()
target_sources(
Turbine-GBA PRIVATE
irqstub.cpp
)
endif()
target_include_directories(
Turbine-GBA PUBLIC
../../include
)
target_link_libraries(
Turbine-GBA PUBLIC
TeaGBA
Keel
)

View File

@ -0,0 +1,18 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/string.hpp>
#include <turbine/context.hpp>
namespace turbine {
ox::String getClipboardText(Context&) noexcept {
return {};
}
void setClipboardText(Context&, ox::CRStringView) noexcept {
}
}

View File

@ -0,0 +1,12 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace turbine::config {
constexpr bool GbaEventLoopTimerBased = TURBINE_GBA_EVENT_LOOP_TIMER_BASED;
constexpr int GbaTimerBits = TURBINE_GBA_TIMER_BITS;
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "context.hpp"
namespace turbine {
void ContextDeleter::operator()(Context *p) noexcept {
ox::safeDelete(p);
}
keel::Context const&keelCtx(Context const&ctx) noexcept {
return ctx.keelCtx;
}
keel::Context &keelCtx(Context &ctx) noexcept {
return ctx.keelCtx;
}
void setApplicationData(Context &ctx, void *applicationData) noexcept {
ctx.applicationData = applicationData;
}
void *applicationDataRaw(Context &ctx) noexcept {
return ctx.applicationData;
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <keel/context.hpp>
#include <turbine/clipboard.hpp>
#include <turbine/context.hpp>
#include <turbine/event.hpp>
namespace turbine {
class Context {
public:
UpdateHandler updateHandler = [](Context&) -> int {return 0;};
keel::Context keelCtx;
KeyEventHandler keyEventHandler = nullptr;
void *applicationData = nullptr;
// GBA impl data /////////////////////////////////////////////////////////
bool running = true;
Context() noexcept = default;
Context(Context &other) noexcept = delete;
Context(Context const&other) noexcept = delete;
Context(Context const&&other) noexcept = delete;
virtual inline ~Context() noexcept {
shutdown(*this);
}
};
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <turbine/event.hpp>
#include "context.hpp"
namespace turbine {
void setUpdateHandler(Context &ctx, UpdateHandler h) noexcept {
ctx.updateHandler = h;
}
void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept {
ctx.keyEventHandler = h;
}
KeyEventHandler keyEventHandler(Context &ctx) noexcept {
return ctx.keyEventHandler;
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/size.hpp>
#include <ox/std/stringview.hpp>
#include <teagba/addresses.hpp>
#include <teagba/gfx.hpp>
#include <teagba/irq.hpp>
#include <turbine/context.hpp>
namespace turbine {
ox::Error initGfx(Context&) noexcept {
REG_DISPCTL = teagba::DispCtl_Mode0
| teagba::DispCtl_SpriteMap1D
| teagba::DispCtl_Obj;
// tell display to trigger vblank interrupts
REG_DISPSTAT = REG_DISPSTAT | teagba::DispStat_irq_vblank;
// enable vblank interrupt
REG_IE = REG_IE | teagba::Int_vblank;
return {};
}
void setWindowTitle(Context&, ox::CRStringView) noexcept {
}
int getScreenWidth(Context&) noexcept {
return 240;
}
int getScreenHeight(Context&) noexcept {
return 160;
}
ox::Size getScreenSize(Context&) noexcept {
return {240, 160};
}
ox::Bounds getWindowBounds(Context&) noexcept {
return {0, 0, 240, 160};
}
ox::Error setWindowBounds(Context&, ox::Bounds const&) noexcept {
return OxError(1, "setWindowBounds not supported on GBA");
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
// NOTE: this file is compiled as ARM and not THUMB, so don't but too much in
// here
#include <teagba/addresses.hpp>
#include <teagba/gfx.hpp>
#include <teagba/irq.hpp>
#include "turbine.hpp"
namespace turbine {
volatile gba_timer_t g_timerMs = 0;
}
using namespace turbine;
extern "C" {
void turbine_isr_vblank() noexcept {
teagba::applySpriteUpdates();
if constexpr(config::GbaEventLoopTimerBased) {
// disable vblank interrupt until it is needed again
REG_IE = REG_IE & ~teagba::Int_vblank;
}
}
void turbine_isr_timer0() noexcept {
g_timerMs = g_timerMs + 1;
}
}

View File

@ -0,0 +1,110 @@
//
// Copyright 2016 - 2023 gary@drinkingtea.net
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
.section .iwram, "ax", %progbits
.arm
.align
.extern turbine_isr_vblank
.extern turbine_isr_timer0
.equ REG_IFBIOS, 0x03007ff8
.equ REG_IE, 0x04000200
.equ REG_IF, 0x04000202
.equ REG_IME, 0x04000208
.equ Int_vblank, 1
.equ Int_hblank, 2
.equ Int_vcount, 4
.equ Int_timer0, 8
.equ Int_timer1, 16
.equ Int_timer2, 32
.equ Int_timer3, 64
.equ Int_serial, 128 // link cable
.equ Int_dma0, 256
.equ Int_dma1, 512
.equ Int_dma2, 1024
.equ Int_dma3, 2048
.equ Int_dma4, 4096
.equ Int_dma5, 8192
.equ Int_input, 16384 // gamepad
.equ Int_cart, 32768 // cartridge removed
.global turbine_isr
.type turbine_isr, %function
turbine_isr:
// read IE
ldr r0, =#REG_IE
ldrh r1, [r0] // r1 becomes IE value
ldrh r2, [r0, #2] // r2 becomes IF value
and r1, r1, r2 // r1 becomes IE & IF
// done with r2 as IF value
// Acknowledge IRQ in REG_IF
strh r1, [r0, #2]
ldr r0, =#REG_IFBIOS
// Acknowledge IRQ in REG_IFBIOS
ldr r2, [r0] // r2 becomes REG_IFBIOS value
orr r2, r2, r1
str r2, [r0]
// done with r2 as IFBIOS value
// done with r0 as REG_IFBIOS
////////////////////////////////////////////////////
// Interrupt Table Begin //
////////////////////////////////////////////////////
cmp r1, #Int_timer0
ldreq r0, =turbine_isr_timer0
beq isr_call_handler
cmp r1, #Int_vblank
ldreq r0, =turbine_isr_vblank
beq isr_call_handler
////////////////////////////////////////////////////
// Interrupt Table End //
////////////////////////////////////////////////////
bx lr
isr_call_handler:
// clear IME to disable interrupts
ldr r2, =#REG_IME
mov r1, #0
str r1, [r2]
// enter system mode
mrs r1, cpsr
bic r1, r1, #0xDF
orr r1, r1, #0x1F
msr cpsr, r1
push {lr}
ldr lr, =isr_restore
bx r0
isr_restore:
pop {lr}
// re-enter irq mode
mrs r0, cpsr
bic r0, r0, #0xDF
orr r0, r0, #0x92
msr cpsr, r0
// set IME to re-enable interrupts
ldr r2, =#REG_IME
mov r0, #1
str r0, [r2]
bx lr
// vim: ft=armv4

View File

@ -0,0 +1,8 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
// stub for building TeaGBA for PC targets, for purposes of not having to
// switch back and forth between builds when editing GBA files
extern "C" void turbine_isr() {}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <teagba/addresses.hpp>
#include <teagba/bios.hpp>
#include <teagba/irq.hpp>
#include "context.hpp"
#include "turbine.hpp"
namespace turbine {
extern volatile gba_timer_t g_timerMs;
static gba_timer_t g_wakeupTime{};
ox::Error run(Context &ctx) noexcept {
g_wakeupTime = 0;
while (ctx.running) {
if (g_wakeupTime <= g_timerMs && ctx.updateHandler) {
auto sleepTime = ctx.updateHandler(ctx);
if (sleepTime >= 0) {
g_wakeupTime = g_timerMs + static_cast<unsigned>(sleepTime);
} else {
g_wakeupTime = ~gba_timer_t(0);
}
}
if constexpr(config::GbaEventLoopTimerBased) {
// wait for timer interrupt
teagba_intrwait(
0,
teagba::Int_timer0 | teagba::Int_timer1 | teagba::Int_timer2 | teagba::Int_timer3);
} else {
teagba_vblankintrwait();
}
}
return {};
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <teagba/addresses.hpp>
#include <teagba/irq.hpp>
#include <keel/keel.hpp>
#include <turbine/context.hpp>
#include <turbine/gfx.hpp>
#include "context.hpp"
#include "turbine.hpp"
extern "C" void turbine_isr();
namespace turbine {
// Timer Consts
constexpr int NanoSecond = 1'000'000'000;
constexpr int MilliSecond = 1000;
constexpr int TicksMs59ns =
65535 - static_cast<uint16_t>(static_cast<double>(NanoSecond / MilliSecond) / 59.59);
extern volatile gba_timer_t g_timerMs;
static void initIrq() noexcept {
REG_ISR = turbine_isr;
REG_IME = 1; // enable interrupts
}
static void initTimer() noexcept {
// make timer0 a ~1 millisecond timer
REG_TIMER0 = TicksMs59ns;
REG_TIMER0CTL = 0b11000000;
// enable interrupt for timer0
REG_IE = REG_IE | teagba::Int_timer0;
}
[[maybe_unused]]
static ox::Result<std::size_t> findPreloadSection() noexcept {
// put the header in the wrong order to prevent mistaking this code for the
// media section
constexpr auto headerP2 = "DER_____________";
constexpr auto headerP1 = "KEEL_PRELOAD_HEA";
constexpr auto headerP1Len = ox_strlen(headerP2);
constexpr auto headerP2Len = ox_strlen(headerP1);
constexpr auto headerLen = headerP1Len + headerP2Len;
for (auto current = MEM_ROM; current < reinterpret_cast<char*>(0x0a000000); current += headerLen) {
if (memcmp(current, headerP1, headerP1Len) == 0 &&
memcmp(current + headerP1Len, headerP2, headerP2Len) == 0) {
return reinterpret_cast<std::size_t>(current + headerLen);
}
}
return OxError(1);
}
ox::Result<ContextUPtr> init(
ox::UPtr<ox::FileSystem> &&fs, ox::CRStringView appName) noexcept {
auto ctx = ox::make_unique<Context>();
oxReturnError(keel::init(ctx->keelCtx, std::move(fs), appName));
#ifdef OX_BARE_METAL
oxReturnError(findPreloadSection().moveTo(ctx->keelCtx.preloadSectionOffset));
#endif
oxReturnError(initGfx(*ctx));
initTimer();
initIrq();
return ox::UPtr<turbine::Context, ContextDeleter>(std::move(ctx));
}
void shutdown(Context&) noexcept {
}
uint64_t ticksMs(Context&) noexcept {
return g_timerMs;
}
bool buttonDown(Context const&, Key k) noexcept {
return k <= Key::GamePad_L && !(REG_GAMEPAD & (1 << static_cast<int>(k)));
}
void requestShutdown(Context &ctx) noexcept {
ctx.running = false;
}
}

View File

@ -0,0 +1,15 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/types.hpp>
#include "config.hpp"
namespace turbine {
using gba_timer_t = ox::Uint<config::GbaTimerBits>;
}

View File

@ -0,0 +1,24 @@
option(TURBINE_USE_IMGUI "Include DearImGUI in build (GLFW only)" ON)
option(TURBINE_GL_FPS_PRINT "Print FPS to stdout" OFF)
target_sources(
Turbine PRIVATE
context.cpp
clipboard.cpp
event.cpp
gfx.cpp
turbine.cpp
)
target_compile_definitions(
Turbine PRIVATE
TURBINE_USE_IMGUI=$<IF:$<BOOL:${TURBINE_USE_IMGUI}>,1,0>
TURBINE_GL_FPS_PRINT=$<IF:$<BOOL:${TURBINE_GL_FPS_PRINT}>,true,false>
)
target_link_libraries(
Turbine PUBLIC
glad
glfw
imgui
)

View File

@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <GLFW/glfw3.h>
#include <ox/std/string.hpp>
#include <turbine/turbine.hpp>
#include "context.hpp"
namespace turbine {
ox::String getClipboardText(Context &ctx) noexcept {
return ox::String(glfwGetClipboardString(ctx.window));
}
void setClipboardText(Context &ctx, ox::CRStringView text) noexcept {
auto cstr = ox_malloca(text.bytes() + 1, char);
ox_strncpy(cstr.get(), text.data(), text.bytes());
glfwSetClipboardString(ctx.window, cstr.get());
}
void setClipboardObject(Context &ctx, ox::UPtr<BaseClipboardObject> &&obj) noexcept {
ctx.clipboard = std::move(obj);
}
ox::Result<BaseClipboardObject*> getClipboardData(Context &ctx, ox::StringView typeName, int typeVersion) noexcept {
if (ctx.clipboard && ctx.clipboard->typeMatch(typeName, typeVersion)) {
return ctx.clipboard.get();
}
return OxError(1);
}
}

View File

@ -0,0 +1,11 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
namespace turbine::config {
constexpr bool GlFpsPrint = TURBINE_GL_FPS_PRINT;
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <turbine/context.hpp>
#include "context.hpp"
namespace turbine {
void ContextDeleter::operator()(Context *p) noexcept {
ox::safeDelete(p);
}
keel::Context const&keelCtx(Context const&ctx) noexcept {
return ctx.keelCtx;
}
keel::Context &keelCtx(Context &ctx) noexcept {
return ctx.keelCtx;
}
void setApplicationData(Context &ctx, void *applicationData) noexcept {
ctx.applicationData = applicationData;
}
void *applicationDataRaw(Context &ctx) noexcept {
return ctx.applicationData;
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <turbine/clipboard.hpp>
#include <turbine/context.hpp>
#include <turbine/gfx.hpp>
#include <turbine/event.hpp>
namespace turbine {
class Context {
public:
UpdateHandler updateHandler = [](Context&) -> int {return 0;};
keel::Context keelCtx;
KeyEventHandler keyEventHandler = nullptr;
void *applicationData = nullptr;
// GLFW impl data ////////////////////////////////////////////////////////
int uninterruptedRefreshes = 3;
ox::UPtr<BaseClipboardObject> clipboard;
struct GLFWwindow *window = nullptr;
// sets screen refresh to constant instead of only on event
bool constantRefresh = true;
ox::Vector<gl::Drawer*, 5> drawers;
int64_t startTime = 0;
uint64_t wakeupTime = 0;
uint64_t keysDown = 0;
uint64_t prevFpsCheckTime = 0;
uint64_t draws = 0;
Context() noexcept = default;
Context(Context const&other) noexcept = delete;
Context(Context const&&other) noexcept = delete;
};
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <turbine/event.hpp>
#include "context.hpp"
namespace turbine {
void setUpdateHandler(Context &ctx, UpdateHandler h) noexcept {
ctx.updateHandler = h;
}
void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept {
ctx.keyEventHandler = h;
}
KeyEventHandler keyEventHandler(Context &ctx) noexcept {
return ctx.keyEventHandler;
}
}

View File

@ -0,0 +1,265 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#if TURBINE_USE_IMGUI
#include <imgui_impl_glfw.h>
#include <imgui_impl_opengl3.h>
#endif
#include "context.hpp"
namespace turbine {
namespace gl {
void addDrawer(Context &ctx, Drawer *cd) noexcept {
ctx.drawers.emplace_back(cd);
}
void removeDrawer(Context &ctx, Drawer *cd) noexcept {
for (auto i = 0u; i < ctx.drawers.size(); ++i) {
if (ctx.drawers[i] == cd) {
oxIgnoreError(ctx.drawers.erase(i));
break;
}
}
}
}
static void handleGlfwError(int err, char const*desc) noexcept {
oxErrf("GLFW error ({}): {}\n", err, desc);
}
static auto setKeyDownStatus(Context &ctx, Key key, bool down) noexcept {
ctx.keysDown &= ~(1llu << static_cast<int>(key));
ctx.keysDown |= static_cast<uint64_t>(down) << static_cast<int>(key);
}
static void handleKeyPress(Context &ctx, int key, bool down) noexcept {
static constexpr auto keyMap = [] {
ox::Array<Key, GLFW_KEY_LAST> map = {};
for (auto i = 0u; i < 26; ++i) {
map[GLFW_KEY_A + i] = static_cast<Key>(static_cast<unsigned>(Key::Alpha_A) + i);
}
for (auto i = 0u; i < 10; ++i) {
map[GLFW_KEY_0 + i] = static_cast<Key>(static_cast<unsigned>(Key::Num_0) + i);
}
map[GLFW_KEY_LEFT_ALT] = Key::Mod_Alt;
map[GLFW_KEY_RIGHT_ALT] = Key::Mod_Alt;
map[GLFW_KEY_LEFT_CONTROL] = Key::Mod_Ctrl;
map[GLFW_KEY_RIGHT_CONTROL] = Key::Mod_Ctrl;
map[GLFW_KEY_LEFT_SUPER] = Key::Mod_Super;
map[GLFW_KEY_RIGHT_SUPER] = Key::Mod_Super;
map[GLFW_KEY_ESCAPE] = Key::Escape;
return map;
}();
auto const eventHandler = keyEventHandler(ctx);
auto const k = keyMap[static_cast<std::size_t>(key)];
setKeyDownStatus(ctx, k, down);
if (eventHandler) {
eventHandler(ctx, k, down);
}
}
static void handleGlfwCursorPosEvent(GLFWwindow*, double, double) noexcept {
}
static void handleGlfwMouseButtonEvent(GLFWwindow *window, int, int, int) noexcept {
auto const ctx = static_cast<Context*>(glfwGetWindowUserPointer(window));
ctx->uninterruptedRefreshes = 25;
}
static void handleGlfwKeyEvent(GLFWwindow *window, int key, int, int action, int) noexcept {
auto const ctx = static_cast<Context*>(glfwGetWindowUserPointer(window));
ctx->uninterruptedRefreshes = 25;
if (action == GLFW_PRESS) {
handleKeyPress(*ctx, key, true);
} else if (action == GLFW_RELEASE) {
handleKeyPress(*ctx, key, false);
}
}
#if TURBINE_USE_IMGUI
static void themeImgui() noexcept {
// Dark Ruda style by Raikiri from ImThemes
auto &style = ImGui::GetStyle();
style.Alpha = 1.0;
style.DisabledAlpha = 0.6000000238418579;
style.WindowPadding = ImVec2(8.0, 8.0);
style.WindowRounding = 0.0;
style.WindowBorderSize = 1.0;
style.WindowMinSize = ImVec2(32.0, 32.0);
style.WindowTitleAlign = ImVec2(0.0, 0.5);
style.WindowMenuButtonPosition = ImGuiDir_Left;
style.ChildRounding = 0.0;
style.ChildBorderSize = 1.0;
style.PopupRounding = 0.0;
style.PopupBorderSize = 1.0;
style.FramePadding = ImVec2(4.0, 3.0);
// custom value
style.FrameRounding = 3.0;
style.FrameBorderSize = 0.0;
style.ItemSpacing = ImVec2(8.0, 4.0);
style.ItemInnerSpacing = ImVec2(4.0, 4.0);
style.CellPadding = ImVec2(4.0, 2.0);
style.IndentSpacing = 21.0;
style.ColumnsMinSpacing = 6.0;
style.ScrollbarSize = 14.0;
style.ScrollbarRounding = 9.0;
style.GrabMinSize = 10.0;
style.GrabRounding = 4.0;
style.TabRounding = 4.0;
style.TabBorderSize = 0.0;
style.TabMinWidthForCloseButton = 0.0;
style.ColorButtonPosition = ImGuiDir_Right;
style.ButtonTextAlign = ImVec2(0.5, 0.5);
style.SelectableTextAlign = ImVec2(0.0, 0.0);
// colors
constexpr auto imVec4 = [](double r, double g, double b, double a) {
return ImVec4(
static_cast<float>(r),
static_cast<float>(g),
static_cast<float>(b),
static_cast<float>(a));
};
style.Colors[ImGuiCol_Text] = imVec4(0.9490196108818054, 0.95686274766922, 0.9764705896377563, 1.0);
style.Colors[ImGuiCol_TextDisabled] = imVec4(0.3568627536296844, 0.4196078479290009, 0.4666666686534882, 1.0);
style.Colors[ImGuiCol_WindowBg] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
style.Colors[ImGuiCol_ChildBg] = imVec4(0.1490196138620377, 0.1764705926179886, 0.2196078449487686, 1.0);
style.Colors[ImGuiCol_PopupBg] = imVec4(0.0784313753247261, 0.0784313753247261, 0.0784313753247261, 0.9399999976158142);
style.Colors[ImGuiCol_Border] = imVec4(0.0784313753247261, 0.09803921729326248, 0.1176470592617989, 1.0);
style.Colors[ImGuiCol_BorderShadow] = imVec4(0.0, 0.0, 0.0, 0.0);
style.Colors[ImGuiCol_FrameBg] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
style.Colors[ImGuiCol_FrameBgHovered] = imVec4(0.1176470592617989, 0.2000000029802322, 0.2784313857555389, 1.0);
style.Colors[ImGuiCol_FrameBgActive] = imVec4(0.08627451211214066, 0.1176470592617989, 0.1372549086809158, 1.0);
style.Colors[ImGuiCol_TitleBg] = imVec4(0.08627451211214066, 0.1176470592617989, 0.1372549086809158, 0.6499999761581421);
style.Colors[ImGuiCol_TitleBgActive] = imVec4(0.0784313753247261, 0.09803921729326248, 0.1176470592617989, 1.0);
style.Colors[ImGuiCol_TitleBgCollapsed] = imVec4(0.0, 0.0, 0.0, 0.5099999904632568);
style.Colors[ImGuiCol_MenuBarBg] = imVec4(0.1490196138620377, 0.1764705926179886, 0.2196078449487686, 1.0);
style.Colors[ImGuiCol_ScrollbarBg] = imVec4(0.01960784383118153, 0.01960784383118153, 0.01960784383118153, 0.3899999856948853);
style.Colors[ImGuiCol_ScrollbarGrab] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
style.Colors[ImGuiCol_ScrollbarGrabHovered] = imVec4(0.1764705926179886, 0.2196078449487686, 0.2470588237047195, 1.0);
style.Colors[ImGuiCol_ScrollbarGrabActive] = imVec4(0.08627451211214066, 0.2078431397676468, 0.3098039329051971, 1.0);
style.Colors[ImGuiCol_CheckMark] = imVec4(0.2784313857555389, 0.5568627715110779, 1.0, 1.0);
style.Colors[ImGuiCol_SliderGrab] = imVec4(0.2784313857555389, 0.5568627715110779, 1.0, 1.0);
style.Colors[ImGuiCol_SliderGrabActive] = imVec4(0.3686274588108063, 0.6078431606292725, 1.0, 1.0);
style.Colors[ImGuiCol_Button] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
style.Colors[ImGuiCol_ButtonHovered] = imVec4(0.2784313857555389, 0.5568627715110779, 1.0, 1.0);
style.Colors[ImGuiCol_ButtonActive] = imVec4(0.05882352963089943, 0.529411792755127, 0.9764705896377563, 1.0);
// custom value
style.Colors[ImGuiCol_Header] = imVec4(0.4000000029802322, 0.4470588237047195, 0.4862745225429535, 0.550000011920929);
style.Colors[ImGuiCol_HeaderHovered] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.800000011920929);
style.Colors[ImGuiCol_HeaderActive] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 1.0);
style.Colors[ImGuiCol_Separator] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
style.Colors[ImGuiCol_SeparatorHovered] = imVec4(0.09803921729326248, 0.4000000059604645, 0.7490196228027344, 0.7799999713897705);
style.Colors[ImGuiCol_SeparatorActive] = imVec4(0.09803921729326248, 0.4000000059604645, 0.7490196228027344, 1.0);
style.Colors[ImGuiCol_ResizeGrip] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.25);
style.Colors[ImGuiCol_ResizeGripHovered] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.6700000166893005);
style.Colors[ImGuiCol_ResizeGripActive] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.949999988079071);
style.Colors[ImGuiCol_Tab] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
style.Colors[ImGuiCol_TabHovered] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.800000011920929);
style.Colors[ImGuiCol_TabActive] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
style.Colors[ImGuiCol_TabUnfocused] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
style.Colors[ImGuiCol_TabUnfocusedActive] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
style.Colors[ImGuiCol_PlotLines] = imVec4(0.6078431606292725, 0.6078431606292725, 0.6078431606292725, 1.0);
style.Colors[ImGuiCol_PlotLinesHovered] = imVec4(1.0, 0.4274509847164154, 0.3490196168422699, 1.0);
style.Colors[ImGuiCol_PlotHistogram] = imVec4(0.8980392217636108, 0.6980392336845398, 0.0, 1.0);
style.Colors[ImGuiCol_PlotHistogramHovered] = imVec4(1.0, 0.6000000238418579, 0.0, 1.0);
style.Colors[ImGuiCol_TableHeaderBg] = imVec4(0.1882352977991104, 0.1882352977991104, 0.2000000029802322, 1.0);
style.Colors[ImGuiCol_TableBorderStrong] = imVec4(0.3098039329051971, 0.3098039329051971, 0.3490196168422699, 1.0);
style.Colors[ImGuiCol_TableBorderLight] = imVec4(0.2274509817361832, 0.2274509817361832, 0.2470588237047195, 1.0);
style.Colors[ImGuiCol_TableRowBg] = imVec4(0.0, 0.0, 0.0, 0.0);
style.Colors[ImGuiCol_TableRowBgAlt] = imVec4(1.0, 1.0, 1.0, 0.05999999865889549);
style.Colors[ImGuiCol_TextSelectedBg] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.3499999940395355);
style.Colors[ImGuiCol_DragDropTarget] = imVec4(1.0, 1.0, 0.0, 0.8999999761581421);
style.Colors[ImGuiCol_NavHighlight] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 1.0);
style.Colors[ImGuiCol_NavWindowingHighlight] = imVec4(1.0, 1.0, 1.0, 0.699999988079071);
style.Colors[ImGuiCol_NavWindowingDimBg] = imVec4(0.800000011920929, 0.800000011920929, 0.800000011920929, 0.2000000029802322);
style.Colors[ImGuiCol_ModalWindowDimBg] = imVec4(0.800000011920929, 0.800000011920929, 0.800000011920929, 0.3499999940395355);
}
#endif
ox::Error initGfx(Context &ctx) noexcept {
glfwSetErrorCallback(handleGlfwError);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
constexpr auto Scale = 5;
ctx.window = glfwCreateWindow(240 * Scale, 160 * Scale, ctx.keelCtx.appName.c_str(), nullptr, nullptr);
//ctx.window = glfwCreateWindow(876, 743, ctx.keelCtx.appName.c_str(), nullptr, nullptr);
if (ctx.window == nullptr) {
return OxError(1, "Could not open GLFW window");
}
glfwSetCursorPosCallback(ctx.window, handleGlfwCursorPosEvent);
glfwSetMouseButtonCallback(ctx.window, handleGlfwMouseButtonEvent);
glfwSetKeyCallback(ctx.window, handleGlfwKeyEvent);
glfwSetWindowUserPointer(ctx.window, &ctx);
glfwMakeContextCurrent(ctx.window);
if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress))) {
return OxError(2, "Could not init Glad");
}
#if TURBINE_USE_IMGUI
IMGUI_CHECKVERSION();
ImGui::CreateContext();
auto &io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
//io.MouseDrawCursor = true;
ImGui_ImplGlfw_InitForOpenGL(ctx.window, true);
ImGui_ImplOpenGL3_Init();
themeImgui();
#endif
return {};
}
void setWindowTitle(Context &ctx, ox::CRStringView title) noexcept {
auto cstr = ox_malloca(title.bytes() + 1, char);
ox_strncpy(cstr.get(), title.data(), title.bytes());
glfwSetWindowTitle(ctx.window, cstr.get());
}
void focusWindow(Context &ctx) noexcept {
glfwFocusWindow(ctx.window);
}
int getScreenWidth(Context &ctx) noexcept {
int w = 0, h = 0;
glfwGetFramebufferSize(ctx.window, &w, &h);
return w;
}
int getScreenHeight(Context &ctx) noexcept {
int w = 0, h = 0;
glfwGetFramebufferSize(ctx.window, &w, &h);
return h;
}
ox::Size getScreenSize(Context &ctx) noexcept {
int w = 0, h = 0;
glfwGetFramebufferSize(ctx.window, &w, &h);
return {w, h};
}
ox::Bounds getWindowBounds(Context &ctx) noexcept {
ox::Bounds bnds;
glfwGetWindowPos(ctx.window, &bnds.x, &bnds.y);
glfwGetWindowSize(ctx.window, &bnds.width, &bnds.height);
return bnds;
}
ox::Error setWindowBounds(Context &ctx, ox::Bounds const&bnds) noexcept {
glfwSetWindowPos(ctx.window, bnds.x, bnds.y);
glfwSetWindowSize(ctx.window, bnds.width, bnds.height);
return {};
}
void setConstantRefresh(Context &ctx, bool r) noexcept {
ctx.constantRefresh = r;
}
}

View File

@ -0,0 +1,123 @@
/*
* Copyright 2016 - 2023 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <GLFW/glfw3.h>
#if TURBINE_USE_IMGUI
#include <imgui_impl_opengl3.h>
#include <imgui_impl_glfw.h>
#endif
#include <keel/keel.hpp>
#include <turbine/turbine.hpp>
#include "config.hpp"
#include "context.hpp"
namespace turbine {
static void draw(Context &ctx) noexcept {
// draw start
#if TURBINE_USE_IMGUI
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
#endif
for (auto d : ctx.drawers) {
d->draw(ctx);
}
#if TURBINE_USE_IMGUI
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
#endif
// draw end
glfwSwapBuffers(ctx.window);
}
static void draw(GLFWwindow *window, int, int) noexcept {
auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window));
draw(ctx);
}
ox::Result<ContextUPtr> init(
ox::UPtr<ox::FileSystem> &&fs, ox::CRStringView appName) noexcept {
auto ctx = ox::make_unique<Context>();
oxReturnError(keel::init(ctx->keelCtx, std::move(fs), appName));
using namespace std::chrono;
ctx->startTime = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
glfwInit();
oxReturnError(initGfx(*ctx));
glfwSetWindowSizeCallback(ctx->window, draw);
return ox::UPtr<Context, ContextDeleter>(ctx.release());
}
static void tickFps(Context &ctx, uint64_t nowMs) noexcept {
++ctx.draws;
if (ctx.draws >= 500) {
auto const duration = static_cast<double>(nowMs - ctx.prevFpsCheckTime) / 1000.0;
auto const fps = static_cast<int>(static_cast<double>(ctx.draws) / duration);
if constexpr(config::GlFpsPrint) {
oxOutf("FPS: {}\n", fps);
}
oxTracef("turbine.fps", "FPS: {}", fps);
ctx.prevFpsCheckTime = nowMs;
ctx.draws = 0;
}
}
ox::Error run(Context &ctx) noexcept {
int sleepTime = 0;
while (!glfwWindowShouldClose(ctx.window)) {
glfwPollEvents();
auto const ticks = ticksMs(ctx);
if (ctx.wakeupTime <= ticks) {
sleepTime = ctx.updateHandler(ctx);
if (sleepTime >= 0) {
ctx.wakeupTime = ticks + static_cast<unsigned>(sleepTime);
} else {
ctx.wakeupTime = ~uint64_t(0);
}
} else {
sleepTime = 10;
}
tickFps(ctx, ticks);
draw(ctx);
if (!ctx.constantRefresh) {
if (ctx.uninterruptedRefreshes) {
--ctx.uninterruptedRefreshes;
} else {
glfwWaitEventsTimeout(sleepTime);
}
}
}
shutdown(ctx);
return {};
}
void shutdown(Context &ctx) noexcept {
if (ctx.window) {
#if TURBINE_USE_IMGUI
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
#endif
glfwDestroyWindow(ctx.window);
ctx.window = nullptr;
}
}
uint64_t ticksMs(Context const&ctx) noexcept {
using namespace std::chrono;
auto const now = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
return static_cast<uint64_t>(now - ctx.startTime);
}
bool buttonDown(Context const&ctx, Key key) noexcept {
return (ctx.keysDown >> static_cast<int>(key)) & 1;
}
void requestShutdown(Context &ctx) noexcept {
glfwSetWindowShouldClose(ctx.window, true);
}
}