Files
nostalgia/src/olympic/studio/modlib/include/studio/project.hpp
2025-07-24 01:26:26 -05:00

233 lines
6.5 KiB
C++

/*
* Copyright 2016 - 2025 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/model/descwrite.hpp>
#include <ox/std/hashmap.hpp>
#include <keel/typestore.hpp>
#include <keel/media.hpp>
namespace studio {
enum class ProjectEvent {
None,
DirAdded,
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,
DirDeleted,
FileDeleted,
FileUpdated,
FileMoved,
};
[[nodiscard]]
constexpr ox::Result<ox::StringView> fileExt(ox::StringViewCR path) noexcept {
auto const extStart = ox::find(path.crbegin(), path.crend(), '.').offset();
if (!extStart) {
return ox::Error(1, "file path does not have valid extension");
}
return substr(path, extStart + 1);
}
[[nodiscard]]
constexpr ox::StringView parentDir(ox::StringView path) noexcept {
if (path.len() && path[path.len() - 1] == '/') {
path = substr(path, 0, path.len() - 1);
}
auto const extStart = ox::find(path.crbegin(), path.crend(), '/').offset();
return substr(path, 0, extStart);
}
class Project: public ox::SignalHandler {
private:
ox::SmallMap<ox::String, ox::Optional<ox::ClawFormat>> m_typeFmt;
keel::Context &m_kctx;
ox::String m_path;
ox::String m_projectDataDir;
ox::String m_typeDescPath;
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::StringParam path, ox::StringViewCR projectDataDir);
ox::Error create() noexcept;
[[nodiscard]]
ox::String const &projectPath() const noexcept;
[[nodiscard]]
ox::FileSystem &romFs() noexcept;
ox::Error mkdir(ox::StringViewCR path) const noexcept;
/**
* Writes a MetalClaw object to the project at the given path.
*/
template<typename T>
ox::Error writeObj(
ox::StringViewCR path,
T const &obj,
ox::ClawFormat fmt) noexcept;
/**
* Writes a MetalClaw object to the project at the given path.
*/
template<typename T>
ox::Error writeObj(
ox::StringViewCR path,
T const &obj) noexcept;
template<typename T>
ox::Result<T> loadObj(ox::StringViewCR path) const noexcept;
ox::Result<ox::FileStat> stat(ox::StringViewCR path) const noexcept;
ox::Error copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept;
ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept;
ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept;
ox::Error deleteItem(ox::StringViewCR path) noexcept;
[[nodiscard]]
keel::Context &kctx() noexcept {
return m_kctx;
}
[[nodiscard]]
keel::Context const &kctx() const noexcept {
return m_kctx;
}
[[nodiscard]]
bool exists(ox::StringViewCR path) const noexcept;
template<typename Functor>
ox::Error subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept;
[[nodiscard]]
ox::Vector<ox::String> const &fileList(ox::StringViewCR ext) noexcept;
ox::Error writeTypeStore() noexcept;
private:
void buildFileIndex() noexcept;
void indexFile(ox::StringViewCR path) noexcept;
ox::Error writeBuff(ox::StringViewCR path, ox::BufferView const &buff) noexcept;
ox::Result<ox::Buffer> loadBuff(ox::StringViewCR path) const noexcept;
ox::Error lsProcDir(ox::Vector<ox::String> &paths, ox::StringViewCR path) const noexcept;
ox::Result<ox::Vector<ox::String>> listFiles(ox::StringViewCR path = "") const noexcept;
// signals
public:
ox::Signal<ox::Error(ProjectEvent, ox::StringViewCR)> fileEvent;
ox::Signal<ox::Error(ox::StringViewCR)> dirAdded;
ox::Signal<ox::Error(ox::StringViewCR)> 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::StringViewCR)> fileRecognized;
ox::Signal<ox::Error(ox::StringViewCR)> fileDeleted;
ox::Signal<ox::Error(ox::StringViewCR)> dirDeleted;
ox::Signal<ox::Error(ox::StringViewCR path, ox::UUID const &id)> fileUpdated;
ox::Signal<ox::Error(ox::StringViewCR oldPath, ox::StringViewCR newPath, ox::UUID const &id)> fileMoved;
};
template<typename T>
ox::Error Project::writeObj(ox::StringViewCR path, T const &obj, ox::ClawFormat fmt) noexcept {
OX_REQUIRE_M(buff, ox::writeClaw(obj, fmt));
if (fmt == ox::ClawFormat::Organic) {
buff.pop_back();
}
// write to FS
OX_RETURN_ERROR(mkdir(parentDir(path)));
OX_RETURN_ERROR(writeBuff(path, buff));
// write type descriptor
if (m_typeStore.get<T>().error) {
OX_RETURN_ERROR(ox::buildTypeDef(m_typeStore, obj));
}
OX_REQUIRE(desc, m_typeStore.get<T>());
auto const descPath = ox::sfmt("{}/{}", m_typeDescPath, buildTypeId(*desc));
auto const descExists = m_fs.exists(descPath);
if (!descExists) {
OX_RETURN_ERROR(writeTypeStore());
}
OX_RETURN_ERROR(keel::reloadAsset(m_kctx, path));
OX_REQUIRE(uuid, pathToUuid(m_kctx, path));
fileUpdated.emit(path, uuid);
return {};
}
template<typename T>
ox::Error Project::writeObj(ox::StringViewCR path, T const &obj) noexcept {
OX_REQUIRE(ext, fileExt(path));
auto const fmt = m_typeFmt[ext].or_value(ox::ClawFormat::Metal);
return writeObj(path, obj, fmt);
}
template<typename T>
ox::Result<T> Project::loadObj(ox::StringViewCR path) const noexcept {
OX_REQUIRE(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::DirAdded:
connect(this, &Project::dirAdded, tgt, slot);
break;
case ProjectEvent::FileAdded:
connect(this, &Project::fileAdded, tgt, slot);
break;
case ProjectEvent::FileRecognized:
{
OX_REQUIRE(files, listFiles());
for (auto const &f : files) {
slot(f);
}
connect(this, &Project::fileRecognized, tgt, slot);
break;
}
case ProjectEvent::DirDeleted:
connect(this, &Project::dirDeleted, tgt, slot);
break;
case ProjectEvent::FileDeleted:
connect(this, &Project::fileDeleted, tgt, slot);
break;
case ProjectEvent::FileUpdated:
connect(this, &Project::fileUpdated, tgt, slot);
break;
case ProjectEvent::FileMoved:
connect(this, &Project::fileMoved, tgt, slot);
break;
}
return {};
}
}