375 lines
12 KiB
C++
375 lines
12 KiB
C++
#include "MetaCoreFoundation/MetaCoreProject.h"
|
|
|
|
#include <array>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <system_error>
|
|
#include <regex>
|
|
#include <sstream>
|
|
#include <string_view>
|
|
|
|
namespace MetaCore {
|
|
|
|
namespace {
|
|
|
|
[[nodiscard]] std::string MetaCorePathToPortableString(const std::filesystem::path& path) {
|
|
return path.lexically_normal().generic_string();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<std::string> MetaCoreReadTextFile(const std::filesystem::path& path) {
|
|
std::ifstream input(path);
|
|
if (!input.is_open()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::ostringstream stream;
|
|
stream << input.rdbuf();
|
|
return stream.str();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<std::string> MetaCoreFindJsonStringValue(
|
|
const std::string& document,
|
|
const std::string& key
|
|
) {
|
|
const std::regex pattern("\\\"" + key + "\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"");
|
|
std::smatch match;
|
|
if (!std::regex_search(document, match, pattern)) {
|
|
return std::nullopt;
|
|
}
|
|
return match[1].str();
|
|
}
|
|
|
|
[[nodiscard]] std::vector<std::string> MetaCoreFindJsonStringArray(
|
|
const std::string& document,
|
|
const std::string& key
|
|
) {
|
|
const std::regex arrayPattern("\\\"" + key + "\\\"\\s*:\\s*\\[([^\\]]*)\\]");
|
|
std::smatch arrayMatch;
|
|
if (!std::regex_search(document, arrayMatch, arrayPattern)) {
|
|
return {};
|
|
}
|
|
|
|
std::vector<std::string> results;
|
|
const std::regex stringPattern("\\\"([^\\\"]*)\\\"");
|
|
const std::string arrayBody = arrayMatch[1].str();
|
|
for (std::sregex_iterator iterator(arrayBody.begin(), arrayBody.end(), stringPattern), end; iterator != end; ++iterator) {
|
|
results.push_back((*iterator)[1].str());
|
|
}
|
|
return results;
|
|
}
|
|
|
|
[[nodiscard]] std::string MetaCoreEscapeJsonString(const std::string& value) {
|
|
std::string escaped;
|
|
escaped.reserve(value.size());
|
|
for (char character : value) {
|
|
switch (character) {
|
|
case '\\':
|
|
escaped += "\\\\";
|
|
break;
|
|
case '"':
|
|
escaped += "\\\"";
|
|
break;
|
|
default:
|
|
escaped += character;
|
|
break;
|
|
}
|
|
}
|
|
return escaped;
|
|
}
|
|
|
|
[[nodiscard]] std::filesystem::path MetaCoreNormalizeProjectRootForCreation(
|
|
const std::filesystem::path& projectRoot
|
|
) {
|
|
if (projectRoot.empty()) {
|
|
return {};
|
|
}
|
|
|
|
std::error_code errorCode;
|
|
std::filesystem::path absoluteRoot = projectRoot.is_absolute()
|
|
? projectRoot
|
|
: std::filesystem::absolute(projectRoot, errorCode);
|
|
if (errorCode) {
|
|
absoluteRoot = projectRoot;
|
|
}
|
|
|
|
const std::filesystem::path parentPath = absoluteRoot.parent_path();
|
|
if (!parentPath.empty() && std::filesystem::exists(parentPath, errorCode)) {
|
|
errorCode.clear();
|
|
const std::filesystem::path canonicalParent = std::filesystem::weakly_canonical(parentPath, errorCode);
|
|
if (!errorCode && !canonicalParent.empty()) {
|
|
return (canonicalParent / absoluteRoot.filename()).lexically_normal();
|
|
}
|
|
}
|
|
|
|
return absoluteRoot.lexically_normal();
|
|
}
|
|
|
|
[[nodiscard]] bool MetaCoreWriteTextFileIfMissing(
|
|
const std::filesystem::path& path,
|
|
std::string_view contents
|
|
) {
|
|
if (std::filesystem::exists(path)) {
|
|
return true;
|
|
}
|
|
std::ofstream output(path, std::ios::binary);
|
|
if (!output.is_open()) {
|
|
return false;
|
|
}
|
|
output.write(contents.data(), static_cast<std::streamsize>(contents.size()));
|
|
return output.good();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::filesystem::path MetaCoreGetProjectFilePath(const std::filesystem::path& projectRoot) {
|
|
return projectRoot / "MetaCore.project.json";
|
|
}
|
|
|
|
std::optional<std::filesystem::path> MetaCoreResolveProjectRootCandidate(
|
|
const std::filesystem::path& candidatePath
|
|
) {
|
|
if (candidatePath.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path projectRoot = candidatePath;
|
|
if (projectRoot.filename() == "MetaCore.project.json") {
|
|
projectRoot = projectRoot.parent_path();
|
|
}
|
|
|
|
const std::filesystem::path projectFilePath = MetaCoreGetProjectFilePath(projectRoot);
|
|
if (!std::filesystem::exists(projectFilePath)) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::error_code errorCode;
|
|
const std::filesystem::path canonicalRoot = std::filesystem::weakly_canonical(projectRoot, errorCode);
|
|
if (!errorCode && !canonicalRoot.empty()) {
|
|
return canonicalRoot;
|
|
}
|
|
|
|
return projectRoot.lexically_normal();
|
|
}
|
|
|
|
bool MetaCoreCreateProjectSkeleton(
|
|
const std::filesystem::path& projectRoot,
|
|
std::string_view projectName
|
|
) {
|
|
const std::filesystem::path normalizedRoot = MetaCoreNormalizeProjectRootForCreation(projectRoot);
|
|
if (normalizedRoot.empty()) {
|
|
return false;
|
|
}
|
|
|
|
std::error_code errorCode;
|
|
const std::array<std::filesystem::path, 12> directories = {
|
|
normalizedRoot / "Assets",
|
|
normalizedRoot / "Scenes",
|
|
normalizedRoot / "Runtime",
|
|
normalizedRoot / "Ui",
|
|
normalizedRoot / "Library",
|
|
normalizedRoot / "Build",
|
|
normalizedRoot / "BuildProfiles",
|
|
normalizedRoot / "Scripts",
|
|
normalizedRoot / "Assets" / "Models",
|
|
normalizedRoot / "Assets" / "Materials",
|
|
normalizedRoot / "Assets" / "Textures",
|
|
normalizedRoot / "Assets" / "Prefabs"
|
|
};
|
|
|
|
for (const auto& directory : directories) {
|
|
errorCode.clear();
|
|
std::filesystem::create_directories(directory, errorCode);
|
|
if (errorCode) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
constexpr std::string_view defaultHudRml = R"rml(<rml>
|
|
<head>
|
|
<link type="text/rcss" href="Hud.rcss" />
|
|
</head>
|
|
<body>
|
|
<div id="hud">
|
|
<div class="title">MetaCore Runtime</div>
|
|
<div class="row">Cube: <span data-metacore-bind="cube.position" data-metacore-format="2">0, 0, 0</span></div>
|
|
<button id="runtime-action" data-metacore-action="hud.primary">Action</button>
|
|
<input id="runtime-input" type="text" value="" />
|
|
</div>
|
|
</body>
|
|
</rml>
|
|
)rml";
|
|
constexpr std::string_view defaultHudRcss = R"rcss(body {
|
|
margin: 0;
|
|
font-family: sans-serif;
|
|
color: #f2f4f8;
|
|
}
|
|
#hud {
|
|
position: absolute;
|
|
left: 24dp;
|
|
top: 24dp;
|
|
width: 300dp;
|
|
padding: 14dp;
|
|
background-color: #17202bcc;
|
|
border: 1dp solid #4f6b88;
|
|
}
|
|
.title { font-size: 20dp; margin-bottom: 10dp; }
|
|
.row { margin-bottom: 12dp; }
|
|
button { padding: 7dp 12dp; margin-right: 8dp; background-color: #386ea5; color: #ffffff; }
|
|
input { width: 150dp; padding: 6dp; background-color: #0f1720; color: #ffffff; }
|
|
)rcss";
|
|
if (!MetaCoreWriteTextFileIfMissing(normalizedRoot / "Ui" / "Hud.rml", defaultHudRml) ||
|
|
!MetaCoreWriteTextFileIfMissing(normalizedRoot / "Ui" / "Hud.rcss", defaultHudRcss)) {
|
|
return false;
|
|
}
|
|
|
|
constexpr std::string_view defaultBuildProfile = R"json({
|
|
"format_version": 1,
|
|
"name": "LinuxDevelopment",
|
|
"target_platform": "Linux-x86_64",
|
|
"configuration": "Development",
|
|
"graphics_backend": "OpenGL",
|
|
"startup_scene": "Scenes/Main.mcscene.json",
|
|
"output_directory": "Build",
|
|
"resource_policy": "ReferencedOnly"
|
|
}
|
|
)json";
|
|
if (!MetaCoreWriteTextFileIfMissing(
|
|
normalizedRoot / "BuildProfiles" / "LinuxDevelopment.mcbuildprofile.json",
|
|
defaultBuildProfile)) {
|
|
return false;
|
|
}
|
|
|
|
MetaCoreProjectFileDocument document;
|
|
if (!projectName.empty()) {
|
|
document.Name = std::string(projectName);
|
|
}
|
|
document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json";
|
|
|
|
return MetaCoreWriteProjectFile(MetaCoreGetProjectFilePath(normalizedRoot), document);
|
|
}
|
|
|
|
std::optional<std::filesystem::path> MetaCoreReadProjectRootFromEnvironment() {
|
|
#if defined(_MSC_VER)
|
|
char* projectPathRaw = nullptr;
|
|
std::size_t projectPathLength = 0;
|
|
if (_dupenv_s(&projectPathRaw, &projectPathLength, "METACORE_PROJECT_PATH") != 0 || projectPathRaw == nullptr) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path projectPath(projectPathRaw);
|
|
std::free(projectPathRaw);
|
|
#else
|
|
const char* projectPathRaw = std::getenv("METACORE_PROJECT_PATH");
|
|
if (projectPathRaw == nullptr || projectPathRaw[0] == '\0') {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::filesystem::path projectPath(projectPathRaw);
|
|
#endif
|
|
|
|
if (projectPath.filename() == "MetaCore.project.json") {
|
|
projectPath = projectPath.parent_path();
|
|
}
|
|
|
|
if (std::filesystem::exists(MetaCoreGetProjectFilePath(projectPath))) {
|
|
std::error_code errorCode;
|
|
return std::filesystem::weakly_canonical(projectPath, errorCode);
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<std::filesystem::path> MetaCoreDiscoverProjectRoot(const std::filesystem::path& startDirectory) {
|
|
if (const auto environmentProjectRoot = MetaCoreReadProjectRootFromEnvironment(); environmentProjectRoot.has_value()) {
|
|
return environmentProjectRoot;
|
|
}
|
|
|
|
std::error_code errorCode;
|
|
std::filesystem::path current = std::filesystem::weakly_canonical(startDirectory, errorCode);
|
|
if (current.empty()) {
|
|
current = startDirectory;
|
|
}
|
|
|
|
while (!current.empty()) {
|
|
const std::array<std::filesystem::path, 3> candidates = {
|
|
current,
|
|
current / "SandboxProject",
|
|
current / "TestProject"
|
|
};
|
|
|
|
for (const auto& candidate : candidates) {
|
|
if (std::filesystem::exists(MetaCoreGetProjectFilePath(candidate))) {
|
|
return std::filesystem::weakly_canonical(candidate, errorCode);
|
|
}
|
|
}
|
|
|
|
const std::filesystem::path parent = current.parent_path();
|
|
if (parent == current) {
|
|
break;
|
|
}
|
|
current = parent;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<MetaCoreProjectFileDocument> MetaCoreReadProjectFile(const std::filesystem::path& projectFilePath) {
|
|
const auto projectDocument = MetaCoreReadTextFile(projectFilePath);
|
|
if (!projectDocument.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
MetaCoreProjectFileDocument document;
|
|
if (const auto name = MetaCoreFindJsonStringValue(*projectDocument, "name"); name.has_value()) {
|
|
document.Name = *name;
|
|
}
|
|
if (const auto version = MetaCoreFindJsonStringValue(*projectDocument, "version"); version.has_value()) {
|
|
document.Version = *version;
|
|
}
|
|
if (const auto startupScene = MetaCoreFindJsonStringValue(*projectDocument, "startup_scene"); startupScene.has_value()) {
|
|
document.StartupScenePath = std::filesystem::path(*startupScene).lexically_normal();
|
|
}
|
|
if (const auto runtimeDir = MetaCoreFindJsonStringValue(*projectDocument, "runtime_directory"); runtimeDir.has_value()) {
|
|
document.RuntimeDirectory = std::filesystem::path(*runtimeDir).lexically_normal();
|
|
}
|
|
if (const auto uiDir = MetaCoreFindJsonStringValue(*projectDocument, "ui_directory"); uiDir.has_value()) {
|
|
document.UiDirectory = std::filesystem::path(*uiDir).lexically_normal();
|
|
}
|
|
if (const auto buildDir = MetaCoreFindJsonStringValue(*projectDocument, "build_directory"); buildDir.has_value()) {
|
|
document.BuildDirectory = std::filesystem::path(*buildDir).lexically_normal();
|
|
}
|
|
for (const std::string& scenePath : MetaCoreFindJsonStringArray(*projectDocument, "scenes")) {
|
|
document.ScenePaths.push_back(std::filesystem::path(scenePath).lexically_normal());
|
|
}
|
|
|
|
return document;
|
|
}
|
|
|
|
bool MetaCoreWriteProjectFile(
|
|
const std::filesystem::path& projectFilePath,
|
|
const MetaCoreProjectFileDocument& document
|
|
) {
|
|
std::ofstream output(projectFilePath, std::ios::trunc);
|
|
if (!output.is_open()) {
|
|
return false;
|
|
}
|
|
|
|
output << "{\n";
|
|
output << " \"name\": \"" << MetaCoreEscapeJsonString(document.Name) << "\",\n";
|
|
output << " \"version\": \"" << MetaCoreEscapeJsonString(document.Version) << "\",\n";
|
|
output << " \"runtime_directory\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(document.RuntimeDirectory)) << "\",\n";
|
|
output << " \"ui_directory\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(document.UiDirectory)) << "\",\n";
|
|
output << " \"build_directory\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(document.BuildDirectory)) << "\",\n";
|
|
output << " \"startup_scene\": \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(document.StartupScenePath)) << "\",\n";
|
|
output << " \"scenes\": [\n";
|
|
for (std::size_t index = 0; index < document.ScenePaths.size(); ++index) {
|
|
output << " \"" << MetaCoreEscapeJsonString(MetaCorePathToPortableString(document.ScenePaths[index])) << "\"";
|
|
output << (index + 1 < document.ScenePaths.size() ? ",\n" : "\n");
|
|
}
|
|
output << " ]\n";
|
|
output << "}\n";
|
|
return true;
|
|
}
|
|
|
|
} // namespace MetaCore
|