Create-Engine/src/src/core/functions/ProjectManager.cpp
2025-05-12 12:33:53 -05:00

1041 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ProjectManager.h"
#include "../../core/utils/EngineConfig.h"
#include "../../core/utils/Logging.h"
#include "../../Engine.h"
#include "../../core/utils/FileDialog.h"
#include "../../core/utils/utils.h"
#include "../../core/utils/AssetManager.h"
#include "SceneSerializer.h"
#include <yaml-cpp/yaml.h>
#include <filesystem>
#include <fstream>
#include <chrono>
#include <imgui.h>
#include <string>
#include <cstring>
using namespace std::chrono;
#define ASSET_MANEFEST_NAME "assets.capk"
namespace {
struct CachedEntry {
fs::path path;
bool isDirectory;
AssetType type;
ImTextureID thumbID;
std::string label;
};
static std::vector<CachedEntry> cacheEntries;
struct TreeNode {
fs::path path;
std::vector<TreeNode> children;
};
static bool rebuild = true;
static TreeNode cachedTreeRoot;
static fs::path lastTreeRoot;
static bool treeDirty = true;
// Recursively build the TreeNode children
void BuildTreeNode(const fs::path& p, TreeNode& node) {
for (auto& e : fs::directory_iterator(p)) {
if (e.is_directory()) {
node.children.push_back({ e.path(), {} });
BuildTreeNode(e.path(), node.children.back());
}
}
}
// Ensure cachedTreeRoot matches s_root
void EnsureTreeCache(const fs::path& root) {
if (treeDirty || root != lastTreeRoot) {
cachedTreeRoot = { root, {} };
BuildTreeNode(root, cachedTreeRoot);
lastTreeRoot = root;
treeDirty = false;
}
}
// Draw from the cached tree
void DrawCachedTree(const TreeNode& node) {
// use folder name (or root)
auto name = node.path == lastTreeRoot
? "res://"
: node.path.filename().string().c_str();
bool open = ImGui::TreeNodeEx(
node.path.string().c_str(),
ImGuiTreeNodeFlags_OpenOnArrow,
"%s",
name
);
if (open) {
for (auto& child : node.children)
DrawCachedTree(child);
ImGui::TreePop();
}
}
}
bool FileExplorer::s_initialized = false;
std::string FileExplorer::SelectedPath;
fs::path FileExplorer::s_root;
fs::path FileExplorer::s_currentDir;
std::unordered_set<std::string> FileExplorer::s_knownFiles;
const char* FileExplorer::ICONS_PATH = "./src/assets/icons/";
unsigned int FileExplorer::s_folderIcon = 0;
std::string FileExplorer::fileSearchQuery = "";
bool FileExplorer::sortAscending = true;
int FileExplorer::sortMode = 0;
bool FileExplorer::showScenes = true;
bool FileExplorer::showImages = true;
bool FileExplorer::showAudio = true;
bool FileExplorer::showScripts = true;
bool FileExplorer::showOther = true;
std::vector<fs::path> FileExplorer::s_loadQueue;
size_t FileExplorer::s_loadIndex = 0;
bool FileExplorer::s_loadingInit = false;
std::mutex FileExplorer::s_fileMutex;
std::atomic<bool> FileExplorer::s_scanInProgress{false};
steady_clock::time_point FileExplorer::s_lastScan = steady_clock::now();
const milliseconds FileExplorer::s_scanInterval = milliseconds(1000);
bool FileExplorer::Init()
{
if (s_initialized)
return false;
if (!ProjectManager::HasProject())
return false;
// Resolve and verify root…
s_root = fs::path(ProjectManager::ResolveResPath("res://"));
s_currentDir = s_root;
if (!fs::exists(s_root) || !fs::is_directory(s_root)) {
Logger::LogError("FileExplorer: invalid root path '%s'", s_root.string().c_str());
return false;
}
s_folderIcon = EngineLoadTextureIfNeeded(std::string(ICONS_PATH) + "folder-outline.png");
std::error_code ec;
fs::recursive_directory_iterator it(s_root, fs::directory_options::skip_permission_denied, ec);
fs::recursive_directory_iterator end;
for (; it != end; it.increment(ec)) {
if (ec) {
Logger::LogWarning("FileExplorer: skipping '%s': %s",
it->path().string().c_str(),
ec.message().c_str());
ec.clear();
continue;
}
if (it->is_regular_file())
s_loadQueue.push_back(it->path());
}
s_initialized = true;
return true;
}
void FileExplorer::Show(bool* p_open)
{
if (!s_initialized) return;
// rebuild tree if the root changed
if (s_root != lastTreeRoot) treeDirty = true;
EnsureTreeCache(s_root);
if (!ImGui::Begin("File Explorer", p_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_HorizontalScrollbar))
{
ImGui::End();
return;
}
// -------------------------------------------------------------------------
// LEFT PANE: interactive foldertree with icons
// -------------------------------------------------------------------------
ImGui::BeginChild("##FolderPane", ImVec2(250,0), true);
{
// recursive lambda to draw each node
std::function<void(const TreeNode&)> drawNode = [&](const TreeNode& node) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow
| ImGuiTreeNodeFlags_SpanAvailWidth;
if (node.children.empty())
flags |= ImGuiTreeNodeFlags_Leaf;
if (node.path == s_currentDir)
flags |= ImGuiTreeNodeFlags_Selected;
ImGui::Image((ImTextureID)s_folderIcon, ImVec2(16,16));
ImGui::SameLine();
const char* label = (node.path == lastTreeRoot)
? "res://"
: node.path.filename().string().c_str();
bool open = ImGui::TreeNodeEx(
node.path.string().c_str(),
flags,
"%s",
label
);
// 3) click to change directory
if (ImGui::IsItemClicked()) {
s_currentDir = node.path;
treeDirty = true; // also force tree repaint?
rebuild = true; // filegrid rebuild
}
// 4) children
if (open) {
for (auto& child : node.children)
drawNode(child);
ImGui::TreePop();
}
};
drawNode(cachedTreeRoot);
}
ImGui::EndChild();
ImGui::SameLine();
// -------------------------------------------------------------------------
// RIGHT PANE: toolbar + filegrid
// -------------------------------------------------------------------------
ImGui::BeginChild("##ContentPane", ImVec2(0,0), false);
// --- Filters / Sort / Search state (static to persist frame-to-frame) ---
static char searchBuf[256] = "";
static fs::path lastDir;
static int lastSortMode = -1;
static bool lastSortAsc = false;
static bool lastShowScenes = true;
static bool lastShowImages = true;
static bool lastShowAudio = true;
static bool lastShowScripts = true;
static bool lastShowOther = true;
static std::string lastSearch;
static bool rebuild = true;
// Filters / Sort popup
if (ImGui::Button("Filters / Sort")) ImGui::OpenPopup("FileFilterPopup");
ImGui::SameLine();
ImGui::SetNextItemWidth(200);
if (ImGui::InputTextWithHint("##Search","Search...",searchBuf,sizeof(searchBuf))) {
fileSearchQuery = searchBuf;
rebuild = true;
}
if (ImGui::BeginPopup("FileFilterPopup")) {
if (ImGui::Selectable("Name", sortMode==0)) { sortMode=0; rebuild=true; }
if (ImGui::Selectable("Type", sortMode==1)) { sortMode=1; rebuild=true; }
if (ImGui::Checkbox("Ascending",&sortAscending)) rebuild=true;
ImGui::Separator();
if (ImGui::Checkbox("Scenes", &showScenes)) rebuild=true;
if (ImGui::Checkbox("Images", &showImages)) rebuild=true;
if (ImGui::Checkbox("Audio", &showAudio)) rebuild=true;
if (ImGui::Checkbox("Scripts", &showScripts)) rebuild=true;
if (ImGui::Checkbox("Other", &showOther)) rebuild=true;
ImGui::EndPopup();
}
// “Up” button + current path display
if (s_currentDir != s_root) {
if (ImGui::Button("Up")) {
s_currentDir = s_currentDir.parent_path();
rebuild = true;
}
ImGui::SameLine();
}
{
fs::path rel = fs::relative(s_currentDir, s_root);
std::string displayPath = "res://"
+ (rel.empty() ? "" : rel.generic_string()+"/");
ImGui::TextUnformatted(displayPath.c_str());
}
ImGui::Separator();
// Detect any state changes → rebuild the filegrid cache
if ( lastDir != s_currentDir
|| lastSortMode != sortMode
|| lastSortAsc != sortAscending
|| lastSearch != fileSearchQuery
|| lastShowScenes != showScenes
|| lastShowImages != showImages
|| lastShowAudio != showAudio
|| lastShowScripts != showScripts
|| lastShowOther != showOther)
{
rebuild = true;
lastDir = s_currentDir;
lastSortMode = sortMode;
lastSortAsc = sortAscending;
lastSearch = fileSearchQuery;
lastShowScenes = showScenes;
lastShowImages = showImages;
lastShowAudio = showAudio;
lastShowScripts = showScripts;
lastShowOther = showOther;
}
if (rebuild) {
cacheEntries.clear();
cacheEntries.reserve(128);
for (auto& e : fs::directory_iterator(s_currentDir)) {
bool isDir = e.is_directory();
AssetType t = AssetManager::AssetTypeFromPath(e.path().string());
// type filters
if (!isDir) {
if ((t==AssetType::Scene && !showScenes )||
(t==AssetType::Image && !showImages )||
(t==AssetType::Audio && !showAudio )||
(t==AssetType::Script && !showScripts)||
(t==AssetType::Unknown && !showOther ))
continue;
}
// search filter
if (!fileSearchQuery.empty()
&& e.path().filename().string().find(fileSearchQuery)==std::string::npos)
continue;
CachedEntry ce;
ce.path = e.path();
ce.isDirectory = isDir;
ce.type = t;
// thumbnail
if (isDir) {
ce.thumbID = (ImTextureID)(intptr_t)s_folderIcon;
}
else if (t==AssetType::Image) {
if (auto* asset = AssetManager::GetAssetByPath(ce.path.string()))
if (auto* img = dynamic_cast<const ImageAssetInfo*>(asset))
ce.thumbID = (ImTextureID)(intptr_t)img->textureID;
}
if (!ce.thumbID) {
ce.thumbID = (ImTextureID)(intptr_t)
EngineLoadTextureIfNeeded(ICONS_PATH + IconFileForPath(ce.path));
}
// label for nonimage files
if (!isDir && t!=AssetType::Image) {
static const std::unordered_map<AssetType,const char*> L = {
{AssetType::Prefab,"Prefab"},
{AssetType::Scene, "Scene"},
{AssetType::Audio, "Audio"},
{AssetType::Script,"Script"},
{AssetType::Video, "Video"},
{AssetType::Font, "Font"},
{AssetType::Shader,"Shader"}
};
auto it = L.find(t);
if (it!=L.end()) ce.label = it->second;
else {
auto ext = ce.path.extension().string();
ce.label = ext.size()>1 ? ext.substr(1) : ext;
}
}
cacheEntries.push_back(std::move(ce));
}
// sort
std::sort(cacheEntries.begin(), cacheEntries.end(),
[&](auto& a, auto& b){
if (sortMode==1) {
if (a.isDirectory!=b.isDirectory)
return sortAscending
? a.isDirectory>b.isDirectory
: a.isDirectory<b.isDirectory;
return sortAscending
? a.path.extension()<b.path.extension()
: a.path.extension()>b.path.extension();
}
return sortAscending
? a.path.filename()<b.path.filename()
: a.path.filename()>b.path.filename();
});
rebuild = false;
}
// Icongrid draw pass
constexpr float iconSize=64.0f, padding=8.0f;
float avail = ImGui::GetContentRegionAvail().x;
int cols = std::max(1,int(avail/(iconSize+padding)));
ImGui::Columns(cols,nullptr,false);
for (auto& ce : cacheEntries) {
PROFILE_ENGINE_SCOPE("entry");
const auto& p = ce.path;
const auto pathStr = p.string();
ImGui::PushID(pathStr.c_str());
bool clicked = false;
if (ce.isDirectory||ce.type==AssetType::Image) {
PROFILE_ENGINE_SCOPE("Image");
clicked = ImGui::ImageButton("##entry",
ce.thumbID,
ImVec2(iconSize,iconSize));
} else {
clicked = ImGui::Button(
(ce.label+"##entry").c_str(),
ImVec2(iconSize,iconSize)
);
}
if (clicked) {
if (ce.isDirectory) s_currentDir=p;
else SelectedPath=pathStr;
rebuild=true;
}
if (ImGui::BeginPopupContextItem("##entry")) {
if (ce.isDirectory && ImGui::MenuItem("Open"))
s_currentDir=p, rebuild=true;
if (!ce.isDirectory && ImGui::MenuItem("Open File"))
SelectedPath=pathStr;
if (ImGui::MenuItem("Copy Path"))
ImGui::SetClipboardText(pathStr.c_str());
ImGui::EndPopup();
}
if (!ce.isDirectory &&
ImGui::BeginDragDropSource(ImGuiDragDropFlags_SourceAllowNullID))
{
if (auto* asset=AssetManager::GetAssetByPath(pathStr)) {
const void* data=&asset->uaid;
const char* typeStr=
ce.type==AssetType::Image ? ASSET_TEXTURE :
ce.type==AssetType::Prefab ? ASSET_PREFAB : nullptr;
if(typeStr)
ImGui::SetDragDropPayload(typeStr,
data,
sizeof(asset->uaid));
}
ImGui::TextUnformatted(p.filename().string().c_str());
ImGui::EndDragDropSource();
}
ImGui::TextWrapped("%s",p.filename().string().c_str());
ImGui::NextColumn();
ImGui::PopID();
}
ImGui::Columns(1);
ImGui::EndChild();
ImGui::End();
}
void FileExplorer::ScanDirectory()
{
std::error_code ec;
std::unordered_set<std::string> currentFiles;
// walk the tree once
for (auto it = fs::recursive_directory_iterator(s_root,
fs::directory_options::skip_permission_denied, ec),
end = fs::recursive_directory_iterator{};
it != end; it.increment(ec))
{
if (ec)
{
ec.clear();
continue;
}
if (!it->is_regular_file()) continue;
auto pathStr = it->path().string();
currentFiles.insert(pathStr);
{
std::lock_guard<std::mutex> lock(s_fileMutex);
if (!s_knownFiles.count(pathStr))
{
s_knownFiles.insert(pathStr);
s_loadQueue.push_back(it->path());
}
}
}
{
std::lock_guard<std::mutex> lock(s_fileMutex);
for (auto it = s_knownFiles.begin(); it != s_knownFiles.end(); )
{
if (!currentFiles.count(*it))
{
AssetManager::GetAssetByPath(*it);
it = s_knownFiles.erase(it);
}
else ++it;
}
}
}
void FileExplorer::Update()
{
if (!s_initialized)
{
Init();
return;
}
auto now = steady_clock::now();
if (!s_scanInProgress.load() && now - s_lastScan >= s_scanInterval)
{
s_lastScan = now;
s_scanInProgress = true;
// fireandforget background scan
std::thread([](){
ScanDirectory();
s_scanInProgress = false;
}).detach();
}
}
bool FileExplorer::LoadingDone()
{
return s_initialized && s_loadIndex >= s_loadQueue.size();
}
void FileExplorer::DrawFolderTree(const fs::path &path)
{
PROFILE_ENGINE_SCOPE("DrawTree");
for (auto &e : fs::directory_iterator(path)) {
if (!e.is_directory()) continue;
auto &d = e.path();
ImGui::PushID(d.string().c_str());
ImGui::Image((ImTextureID)(intptr_t)s_folderIcon, ImVec2(16,16)); ImGui::SameLine();
bool open = ImGui::TreeNodeEx(d.string().c_str(), ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth, "%s", d.filename().string().c_str());
if (ImGui::IsItemClicked()) s_currentDir = d;
if (open) { DrawFolderTree(d); ImGui::TreePop(); }
ImGui::PopID();
}
}
void FileExplorer::ProcessLoadQueue()
{
while (!s_loadQueue.empty()) {
fs::path p = s_loadQueue.back();
s_loadQueue.pop_back();
// Load the asset and get the type,... Long way...
AssetManager::LoadAssetAsync(p.string(), AssetManager::AssetTypeFromExtension(AssetManager::GetFileExtension(p.string())));
Logger::LogVerbose("FileExplorer: imported '%s'", p.string().c_str());
}
}
std::string FileExplorer::IconFileForPath(const fs::path &path)
{
auto ext = path.extension().string();
if (ext == ".cene") return "movie-open-outline.png";
return "error.png";
}
namespace fs = std::filesystem;
std::string ProjectManager::s_currentProjectPath;
std::string ProjectManager::s_currentProjectName;
std::string ProjectManager::s_defaultScene;
void ProjectManager::ShowCreateProjectPopup()
{
static char nameBuf[256] = "";
static char pathBuf[512] = "";
ImGui::SetNextWindowSizeConstraints({ 0, 0 }, { 600, FLT_MAX });
if (!ImGui::BeginPopupModal(
"Create New Project",
nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
return;
ImGui::Text("Create New Project");
ImGui::Separator();
ImGui::Spacing();
// Project Name (full width)
ImGui::TextUnformatted("Project Name");
ImGui::PushItemWidth(-1);
ImGui::InputText("##proj_name", nameBuf, IM_ARRAYSIZE(nameBuf));
ImGui::PopItemWidth();
ImGui::Spacing();
ImGui::TextUnformatted("Project Folder");
ImGui::PushItemWidth(-85);
ImGui::InputText("##proj_path", pathBuf, IM_ARRAYSIZE(pathBuf));
ImGui::PopItemWidth();
ImGui::SameLine();
if (ImGui::Button("Browse...", { 80, 0 }))
{
if (auto chosen = OpenFolderDialog(); !chosen.empty())
std::strncpy(pathBuf, chosen.c_str(), sizeof(pathBuf));
}
ImGui::Spacing();
// Preview path wrapped so it never overflows
auto preview = std::filesystem::path(pathBuf) / (std::string(nameBuf) + ".cproj");
ImGui::Text("Will be created at:");
ImGui::TextWrapped("%s", preview.string().c_str());
ImGui::Spacing();
// Action buttons centered
ImGui::Separator();
float btnW = 80.0f;
float totalW = btnW * 2 + ImGui::GetStyle().ItemSpacing.x;
float offsetX = (ImGui::GetWindowWidth() - totalW) * 0.5f;
ImGui::SetCursorPosX(offsetX);
if (ImGui::Button("Cancel", { btnW, 0 }))
ImGui::CloseCurrentPopup();
ImGui::SameLine();
ImGui::SetItemDefaultFocus();
if (ImGui::Button("Create", { btnW, 0 }))
{
CreateProject(pathBuf, nameBuf);
nameBuf[0] = pathBuf[0] = '\0';
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
bool CreateDirectories(const fs::path &baseDir)
{
static const std::vector<std::string> subdirs = {
"Prefabs",
"Scenes",
"Images",
"Sounds",
"Fonts",
"Shaders",
"Scripts",
"Videos",
};
std::error_code ec;
bool allOk = true;
fs::path assetsRoot = baseDir / "Assets";
for (const auto &name : subdirs)
{
Logger::LogVerbose("Creating Folder: '%s'", name.c_str());
fs::create_directories(assetsRoot / name, ec);
if (ec)
{
Logger::LogError("Could not ensure %s dir: %s",
name.c_str(),
ec.message().c_str());
allOk = false;
}
}
return allOk;
}
bool ProjectManager::HasProject() {
if (s_currentProjectPath.empty() || s_currentProjectName.empty())
{
return false;
}
return true;
}
std::string ProjectManager::ResolveResPath(const std::string &resPath)
{
constexpr const char *prefix = "res://";
// if its not a “res://” path, just return it
if (resPath.rfind(prefix, 0) != 0)
return resPath;
if (s_currentProjectPath.empty())
{
Logger::LogError("Cannot resolve res path: project not loaded!");
return resPath;
}
// start from whatever s_currentProjectPath holds
fs::path baseDir{s_currentProjectPath};
// only append the project name if it isn't already the last path component
if (!s_currentProjectName.empty() &&
baseDir.filename().string() != s_currentProjectName)
{
baseDir /= s_currentProjectName;
}
// drop the "res://" prefix
std::string relativePart = resPath.substr(strlen(prefix));
// build & normalize
fs::path fullPath = (baseDir / relativePart).lexically_normal();
return fullPath.string();
}
bool ProjectManager::Init()
{
s_currentProjectPath.clear();
s_currentProjectName.clear();
s_defaultScene.clear();
Logger::LogOk("Project Core");
return true;
}
bool ProjectManager::LoadProject(const std::string &projectFilePath)
{
fs::path projFile{ projectFilePath };
if (projFile.extension() != ".cproj" || !fs::exists(projFile)) {
Logger::LogError("Project file not found or invalid: '%s'",
projectFilePath.c_str());
return false;
}
fs::path baseDir = projFile.parent_path();
std::string projectName = projFile.stem().string();
YAML::Node config;
try {
config = YAML::LoadFile(projFile.string());
}
catch (const YAML::Exception &e) {
Logger::LogError("Failed to parse project file '%s': %s",
projFile.string().c_str(), e.what());
return false;
}
std::string savedName = config["Name"].as<std::string>(projectName);
std::string defaultScene = config["s_defaultScene"].as<std::string>("");
Logger::LogDebug("Loading '%s'",
savedName.c_str());
// update project pointers
s_currentProjectPath = baseDir.string();
s_currentProjectName = savedName;
// ensure folders
CreateDirectories(baseDir);
// --- Load the asset manifest from Assets/asset_manifest.yaml ---
{
fs::path manifest = baseDir / "Assets" / ASSET_MANEFEST_NAME;
if (fs::exists(manifest)) {
YAML::Node doc = YAML::LoadFile(manifest.string());
if (doc["Assets"]) {
AssetManager::Load(doc["Assets"]);
Logger::LogDebug("Loaded asset manifest: %s",
manifest.string().c_str());
}
} else {
Logger::LogDebug("No asset manifest found at '%s', skipping.",
manifest.string().c_str());
}
}
// finally load the default scene (if any)
if (!defaultScene.empty()) {
s_defaultScene = ResolveResPath(defaultScene);
Logger::LogDebug("Loading Default Scene: %s", s_defaultScene.c_str());
if (!fs::exists(s_defaultScene)) {
Logger::LogError("Default scene does not exist: %s", s_defaultScene.c_str());
return false;
}
SceneLoader::LoadScene(s_defaultScene);
}
Logger::LogOk("loaded project '%s'", savedName.c_str());
return true;
}
bool ProjectManager::CreateProject(
const std::string &projectPath,
const std::string &projectName)
{
fs::path baseDir = fs::path(projectPath) / projectName;
fs::path projFile = baseDir / (projectName + ".cproj");
std::error_code ec;
// 1) create project root
if (!fs::create_directories(baseDir, ec) && ec) {
Logger::LogError("Failed to create project directory '%s': %s",
projectPath.c_str(), ec.message().c_str());
return false;
}
s_currentProjectPath = baseDir.string();
s_currentProjectName = projectName;
CreateDirectories(baseDir);
fs::path sceneDir = baseDir / "Assets" / "Scenes";
fs::path sceneFile = sceneDir / (projectName + ".cene");
s_defaultScene = sceneFile.string();
{
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "Name" << YAML::Value << projectName;
out << YAML::Key << "EngineVersion" << YAML::Value << g_engineConfig.version;
fs::path rel = fs::relative(sceneFile, baseDir);
out << YAML::Key << "s_defaultScene"
<< YAML::Value << ("res://" + rel.generic_string());
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
out << YAML::Key << "CreatedDate" << YAML::Value << buf;
out << YAML::EndMap;
std::ofstream fout(projFile);
if (!fout.is_open()) {
Logger::LogError("Could not open project file for writing: %s",
projFile.string().c_str());
return false;
}
fout << out.c_str();
}
Logger::LogOk("Created project '%s' at '%s'",
projectName.c_str(), projectPath.c_str());
SceneLoader::SaveScene(sceneFile.string());
SceneLoader::LoadScene(sceneFile.string());
return true;
}
bool ProjectManager::SaveCurrentProject()
{
if (s_currentProjectPath.empty() || s_currentProjectName.empty())
{
Logger::LogError("Cannot save project: no project loaded");
return false;
}
fs::path baseDir = fs::path(s_currentProjectPath);
fs::path projFile = baseDir / (s_currentProjectName + ".cproj");
{
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "Name" << YAML::Value << s_currentProjectName;
out << YAML::Key << "EngineVersion" << YAML::Value << g_engineConfig.version;
fs::path rel = fs::relative(s_defaultScene, baseDir);
out << YAML::Key << "s_defaultScene"
<< YAML::Value << ("res://" + rel.generic_string());
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
out << YAML::Key << "CreatedDate" << YAML::Value << buf;
out << YAML::EndMap;
std::ofstream fout(projFile);
if (!fout.is_open())
{
Logger::LogError("Could not open project file for writing: %s",
projFile.string().c_str());
return false;
}
fout << out.c_str();
}
Logger::LogOk("Saved project");
CreateDirectories(baseDir);
{
fs::path manifest = baseDir / "Assets" / ASSET_MANEFEST_NAME;
YAML::Emitter manOut;
manOut << YAML::BeginMap;
AssetManager::Save(manOut);
manOut << YAML::EndMap;
std::ofstream mf(manifest);
if (!mf.is_open())
{
Logger::LogError("Failed to write asset manifest: %s",
manifest.string().c_str());
}
else
{
mf << manOut.c_str();
Logger::LogDebug("Wrote asset manifest: %s",
manifest.string().c_str());
}
}
return true;
}
bool ProjectManager::SaveProject(
const std::string &projectPath,
const std::string &projectName)
{
fs::path baseDir = fs::path(projectPath) / projectName;
fs::path projFile = baseDir / (projectName + ".cproj");
std::error_code ec;
if (!fs::create_directories(baseDir, ec) && ec) {
Logger::LogError("Failed to create project directory '%s': %s",
projectPath.c_str(), ec.message().c_str());
return false;
}
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "Name" << YAML::Value << projectName;
out << YAML::Key << "EngineVersion" << YAML::Value << g_engineConfig.version;
fs::path sceneRel = fs::relative(s_defaultScene,
fs::path(s_currentProjectPath) / s_currentProjectName);
out << YAML::Key << "s_defaultScene"
<< YAML::Value << ("res://" + sceneRel.generic_string());
// timestamp
auto now = std::chrono::system_clock::now();
auto t = std::chrono::system_clock::to_time_t(now);
std::tm tm{};
#if defined(_WIN32)
gmtime_s(&tm, &t);
#else
gmtime_r(&t, &tm);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &tm);
out << YAML::Key << "CreatedDate" << YAML::Value << buf;
out << YAML::EndMap;
std::ofstream fout(projFile);
if (!fout.is_open()) {
Logger::LogError("Could not open project file for writing: %s",
projFile.string().c_str());
return false;
}
fout << out.c_str();
fout.close();
Logger::LogOk("Saved project '%s'",
projectName.c_str());
CreateDirectories(baseDir);
{
fs::path manifest = baseDir / "Assets" / ASSET_MANEFEST_NAME;
YAML::Emitter assetOut;
assetOut << YAML::BeginMap;
AssetManager::Save(assetOut);
assetOut << YAML::EndMap;
std::ofstream mf(manifest);
if (!mf.is_open()) {
Logger::LogError("Failed to write asset manifest: %s",
manifest.string().c_str());
} else {
mf << assetOut.c_str();
mf.close();
Logger::LogDebug("Wrote asset manifest: %s",
manifest.string().c_str());
}
}
// update our in-memory project pointers
s_currentProjectPath = projectPath;
s_currentProjectName = projectName;
return true;
}
const std::string &ProjectManager::GetCurrentProjectPath()
{
return s_currentProjectPath;
}
const std::string &ProjectManager::GetCurrentProjectName()
{
return s_currentProjectName;
}
const std::string &ProjectManager::GetCurrentAssetsPath()
{
static std::string assetsPath;
fs::path baseDir{s_currentProjectPath};
if (!s_currentProjectName.empty() &&
baseDir.filename().string() != s_currentProjectName)
{
baseDir /= s_currentProjectName;
}
fs::path assetDir = (baseDir / "Assets").lexically_normal();
assetsPath = assetDir.string();
return assetsPath;
}