diff --git a/Apps/MetaCoreLauncher/main.cpp b/Apps/MetaCoreLauncher/main.cpp new file mode 100644 index 0000000..445022c --- /dev/null +++ b/Apps/MetaCoreLauncher/main.cpp @@ -0,0 +1,1075 @@ +#include "MetaCoreFoundation/MetaCoreProject.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct LauncherProject { + QString Name{}; + std::filesystem::path RootPath{}; + QString UpdatedDate{}; +}; + +constexpr const char* OrganizationName = "MetaCore"; +constexpr const char* ApplicationName = "MetaCore Hub"; +constexpr const char* NewProjectSettingsGroup = "NewProjectDialog"; +constexpr const char* LastProjectPathKey = "lastProjectPath"; + +std::filesystem::path ToPath(const QString& text) { + return std::filesystem::path(text.toStdWString()); +} + +QString ToQString(const std::filesystem::path& path) { + return QString::fromStdWString(path.wstring()); +} + +std::filesystem::path GetExecutableDirectory() { + return ToPath(QCoreApplication::applicationDirPath()); +} + +std::filesystem::path GetDatabasePath() { + QString dataLocation = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation); + if (dataLocation.isEmpty()) { + dataLocation = QDir::homePath() + "/AppData/Local/MetaCore/Hub"; + } + return ToPath(dataLocation) / "projects.tsv"; +} + +std::filesystem::path GetDefaultProjectLocation() { + QString documents = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + if (documents.isEmpty()) { + documents = QDir::homePath(); + } + return ToPath(documents) / "MetaCore Projects"; +} + +bool IsProjectRoot(const std::filesystem::path& path) { + return std::filesystem::is_regular_file(MetaCore::MetaCoreGetProjectFilePath(path)); +} + +bool WriteTextFile(const std::filesystem::path& path, const std::string& text, QString& errorMessage) { + std::error_code error; + if (!path.parent_path().empty()) { + std::filesystem::create_directories(path.parent_path(), error); + if (error) { + errorMessage = QString("Unable to create folder for %1: %2") + .arg(ToQString(path)) + .arg(QString::fromStdString(error.message())); + return false; + } + } + + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output.is_open()) { + errorMessage = QString("Unable to write %1.").arg(ToQString(path)); + return false; + } + + output << text; + return true; +} + +QPushButton* CreateButton(const QString& text, const QString& objectName = {}) { + auto* button = new QPushButton(text); + button->setCursor(Qt::PointingHandCursor); + if (!objectName.isEmpty()) { + button->setObjectName(objectName); + } + return button; +} + +QLabel* CreateMutedLabel(const QString& text) { + auto* label = new QLabel(text); + label->setObjectName("mutedText"); + return label; +} + +void ClearLayout(QLayout* layout) { + if (layout == nullptr) { + return; + } + + while (QLayoutItem* item = layout->takeAt(0)) { + if (QWidget* widget = item->widget()) { + widget->deleteLater(); + } + if (QLayout* childLayout = item->layout()) { + ClearLayout(childLayout); + } + delete item; + } +} + +bool CreateProjectOnDisk(const std::filesystem::path& rootPath, const QString& name, QString& errorMessage) { + if (rootPath.empty()) { + errorMessage = "Project location is required."; + return false; + } + + std::error_code error; + if (std::filesystem::exists(rootPath, error) && !std::filesystem::is_empty(rootPath, error)) { + errorMessage = "Project folder already exists and is not empty."; + return false; + } + + const std::vector directories = { + rootPath / "ProjectSettings", + rootPath / "Logs", + rootPath / "Library", + rootPath / "Assets", + rootPath / "Assets" / "Models", + rootPath / "Assets" / "Materials", + rootPath / "Assets" / "Textures", + rootPath / "Assets" / "Prefabs", + rootPath / "Scenes", + rootPath / "Runtime", + rootPath / "Ui", + rootPath / "Build", + rootPath / ".vscode" + }; + + for (const std::filesystem::path& directory : directories) { + std::filesystem::create_directories(directory, error); + if (error) { + errorMessage = QString("Unable to create project folder %1: %2") + .arg(ToQString(directory)) + .arg(QString::fromStdString(error.message())); + return false; + } + } + + MetaCore::MetaCoreProjectFileDocument document; + document.Name = name.toStdString(); + document.Version = "0.1.0"; + document.RuntimeDirectory = "Runtime"; + document.UiDirectory = "Ui"; + document.BuildDirectory = "Build"; + document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + + if (!MetaCore::MetaCoreWriteProjectFile(MetaCore::MetaCoreGetProjectFilePath(rootPath), document)) { + errorMessage = "Unable to write MetaCore.project.json."; + return false; + } + + if (!WriteTextFile( + rootPath / "Assets" / "README.md", + "# Project Assets\n\nThis folder contains all assets for the MetaCore project.\n", + errorMessage + )) { + return false; + } + + const std::string createdAt = QDateTime::currentDateTime().toString(Qt::ISODate).toStdString(); + std::ostringstream ini; + ini << "[Project]\n"; + ini << "name = " << name.toStdString() << "\n"; + ini << "path = " << ToQString(rootPath).toStdString() << "\n"; + ini << "created_at = " << createdAt << "\n"; + ini << "changed_at = " << createdAt << "\n"; + if (!WriteTextFile(rootPath / (name.toStdString() + ".ini"), ini.str(), errorMessage)) { + return false; + } + + if (!WriteTextFile( + rootPath / ".vscode" / "settings.json", + "{\n" + " \"editor.formatOnSave\": true,\n" + " \"files.exclude\": {\n" + " \"Library\": true,\n" + " \"Logs\": true,\n" + " \"Build\": true\n" + " }\n" + "}\n", + errorMessage + )) { + return false; + } + + return true; +} + +class ProjectCard final : public QFrame { +public: + explicit ProjectCard(LauncherProject project, QWidget* parent = nullptr) + : QFrame(parent), + Project_(std::move(project)) { + setObjectName("projectCard"); + setCursor(Qt::PointingHandCursor); + setMinimumHeight(72); + setMaximumHeight(72); + + auto* rootLayout = new QHBoxLayout(this); + rootLayout->setContentsMargins(16, 10, 12, 10); + rootLayout->setSpacing(14); + + Avatar_ = new QLabel(Project_.Name.left(1).toUpper()); + Avatar_->setObjectName("projectAvatar"); + Avatar_->setAlignment(Qt::AlignCenter); + Avatar_->setFixedSize(44, 44); + rootLayout->addWidget(Avatar_); + + auto* textLayout = new QVBoxLayout(); + textLayout->setContentsMargins(0, 0, 0, 0); + textLayout->setSpacing(2); + auto* nameLabel = new QLabel(Project_.Name); + nameLabel->setObjectName("projectName"); + auto* pathLabel = CreateMutedLabel(ToQString(Project_.RootPath)); + pathLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + pathLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + textLayout->addWidget(nameLabel); + textLayout->addWidget(pathLabel); + rootLayout->addLayout(textLayout, 1); + + auto* dateLabel = CreateMutedLabel(Project_.UpdatedDate); + dateLabel->setMinimumWidth(88); + rootLayout->addWidget(dateLabel); + + auto* openButton = CreateButton("Open"); + openButton->setObjectName("smallButton"); + openButton->setFixedWidth(64); + connect(openButton, &QPushButton::clicked, this, [this]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(Project_.RootPath))); + }); + rootLayout->addWidget(openButton); + } + + void SetSelected(bool selected) { + setProperty("selected", selected); + style()->unpolish(this); + style()->polish(this); + } + + const LauncherProject& Project() const { + return Project_; + } + +protected: + void mousePressEvent(QMouseEvent* event) override { + emitSelected(); + QFrame::mousePressEvent(event); + } + +private: + void emitSelected() { + if (OnSelected) { + OnSelected(); + } + } + +public: + std::function OnSelected{}; + +private: + LauncherProject Project_{}; + QLabel* Avatar_ = nullptr; +}; + +class NewProjectDialog final : public QDialog { +public: + explicit NewProjectDialog(QWidget* parent = nullptr) + : QDialog(parent) { + setWindowTitle("Create New Project"); + setMinimumWidth(520); + setModal(true); + + auto* rootLayout = new QVBoxLayout(this); + rootLayout->setContentsMargins(22, 20, 22, 18); + rootLayout->setSpacing(10); + + rootLayout->addWidget(new QLabel("Project Name:")); + NameEdit_ = new QLineEdit(); + NameEdit_->setPlaceholderText("Enter a name for your project"); + rootLayout->addWidget(NameEdit_); + + rootLayout->addWidget(new QLabel("Project Location:")); + auto* locationLayout = new QHBoxLayout(); + LocationEdit_ = new QLineEdit(); + LocationEdit_->setReadOnly(true); + LocationEdit_->setPlaceholderText("No path selected"); + auto* browseButton = CreateButton("Browse..."); + browseButton->setFixedWidth(90); + locationLayout->addWidget(LocationEdit_, 1); + locationLayout->addWidget(browseButton); + rootLayout->addLayout(locationLayout); + + QSettings settings(OrganizationName, ApplicationName); + const QString lastPath = settings.value( + QString("%1/%2").arg(NewProjectSettingsGroup, LastProjectPathKey), + QString() + ).toString(); + if (!lastPath.isEmpty()) { + LocationEdit_->setText(lastPath); + } + + connect(browseButton, &QPushButton::clicked, this, [this]() { + const QString currentPath = !LocationEdit_->text().isEmpty() ? LocationEdit_->text() : ToQString(GetDefaultProjectLocation()); + const QString selectedPath = QFileDialog::getExistingDirectory( + this, + "Project Location", + currentPath + ); + if (!selectedPath.isEmpty()) { + LocationEdit_->setText(selectedPath); + QSettings settings(OrganizationName, ApplicationName); + settings.setValue(QString("%1/%2").arg(NewProjectSettingsGroup, LastProjectPathKey), selectedPath); + UpdateCreateButtonState(); + } + }); + + rootLayout->addWidget(new QLabel("Engine Version:")); + VersionCombo_ = new QComboBox(); + VersionCombo_->setFixedHeight(32); + VersionCombo_->addItem("dev (current environment)", "dev-current"); + rootLayout->addWidget(VersionCombo_); + + ErrorLabel_ = new QLabel(); + ErrorLabel_->setObjectName("errorText"); + ErrorLabel_->setWordWrap(true); + ErrorLabel_->hide(); + rootLayout->addWidget(ErrorLabel_); + + auto* buttonsLayout = new QHBoxLayout(); + buttonsLayout->addStretch(1); + auto* cancelButton = CreateButton("Cancel"); + CreateButton_ = CreateButton("Create", "primaryButton"); + cancelButton->setFixedWidth(90); + CreateButton_->setFixedWidth(90); + buttonsLayout->addWidget(cancelButton); + buttonsLayout->addWidget(CreateButton_); + rootLayout->addLayout(buttonsLayout); + + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); + connect(CreateButton_, &QPushButton::clicked, this, [this]() { + accept(); + }); + connect(NameEdit_, &QLineEdit::textChanged, this, [this]() { + UpdateCreateButtonState(); + }); + connect(VersionCombo_, &QComboBox::currentTextChanged, this, [this]() { + UpdateCreateButtonState(); + }); + UpdateCreateButtonState(); + } + + QString ProjectName() const { + return NameEdit_->text().trimmed(); + } + + std::filesystem::path ProjectRootPath() const { + return ToPath(LocationEdit_->text()) / ToPath(ProjectName()); + } + + void accept() override { + UpdateCreateButtonState(); + if (!CreateButton_->isEnabled()) { + ShowError("Project name, location, and engine version are required."); + return; + } + if (ProjectName().contains('/') || ProjectName().contains('\\')) { + ShowError("Project name cannot contain path separators."); + return; + } + QDialog::accept(); + } + +private: + void ShowError(const QString& text) { + ErrorLabel_->setText(text); + ErrorLabel_->show(); + } + + void UpdateCreateButtonState() { + if (CreateButton_ == nullptr) { + return; + } + + const bool valid = !ProjectName().isEmpty() && + !LocationEdit_->text().trimmed().isEmpty() && + VersionCombo_->currentIndex() >= 0; + CreateButton_->setEnabled(valid); + if (valid) { + ErrorLabel_->hide(); + } + } + + QLineEdit* NameEdit_ = nullptr; + QLineEdit* LocationEdit_ = nullptr; + QComboBox* VersionCombo_ = nullptr; + QLabel* ErrorLabel_ = nullptr; + QPushButton* CreateButton_ = nullptr; +}; + +class MetaCoreLauncherWindow final : public QMainWindow { +public: + explicit MetaCoreLauncherWindow(QWidget* parent = nullptr) + : QMainWindow(parent), + ExecutableDirectory_(GetExecutableDirectory()), + DatabasePath_(GetDatabasePath()) { + setWindowTitle("MetaCore Hub"); + resize(1080, 720); + setMinimumSize(960, 640); + setObjectName("launcherWindow"); + + LoadProjects(); + BuildUi(); + RefreshProjectList(); + } + +private: + void BuildUi() { + ApplyStyleSheet(true); + + auto* central = new QWidget(this); + central->setObjectName("central"); + setCentralWidget(central); + + auto* rootLayout = new QHBoxLayout(central); + rootLayout->setContentsMargins(0, 0, 0, 0); + rootLayout->setSpacing(0); + + rootLayout->addWidget(BuildSidebar()); + + Pages_ = new QStackedWidget(); + Pages_->addWidget(BuildProjectsPage()); + Pages_->addWidget(BuildInstallsPage()); + rootLayout->addWidget(Pages_, 1); + } + + QWidget* BuildSidebar() { + auto* sidebar = new QWidget(); + sidebar->setObjectName("sidebar"); + sidebar->setFixedWidth(220); + + auto* layout = new QVBoxLayout(sidebar); + layout->setContentsMargins(18, 24, 18, 18); + layout->setSpacing(8); + + auto* titleLabel = new QLabel("MetaCore"); + titleLabel->setObjectName("hubTitle"); + auto* subtitleLabel = CreateMutedLabel("Hub"); + layout->addWidget(titleLabel); + layout->addWidget(subtitleLabel); + layout->addSpacing(28); + + ProjectsNavButton_ = CreateButton("Projects", "navButton"); + InstallsNavButton_ = CreateButton("Installs", "navButton"); + ProjectsNavButton_->setProperty("selected", true); + layout->addWidget(ProjectsNavButton_); + layout->addWidget(InstallsNavButton_); + layout->addStretch(1); + + auto* darkMode = new QCheckBox("Dark Mode"); + darkMode->setChecked(true); + layout->addWidget(darkMode); + + connect(ProjectsNavButton_, &QPushButton::clicked, this, [this]() { + SetPage(0); + }); + connect(InstallsNavButton_, &QPushButton::clicked, this, [this]() { + SetPage(1); + }); + connect(darkMode, &QCheckBox::toggled, this, [this](bool checked) { + ApplyStyleSheet(checked); + }); + + return sidebar; + } + + QWidget* BuildProjectsPage() { + auto* page = new QWidget(); + auto* layout = new QVBoxLayout(page); + layout->setContentsMargins(28, 24, 28, 24); + layout->setSpacing(14); + + auto* headerLayout = new QHBoxLayout(); + auto* titleLabel = new QLabel("Projects"); + titleLabel->setObjectName("pageTitle"); + headerLayout->addWidget(titleLabel); + headerLayout->addStretch(1); + + auto* deleteButton = CreateButton("Delete", "dangerButton"); + auto* newButton = CreateButton("+ New Project"); + auto* launchButton = CreateButton("Launch", "primaryButton"); + headerLayout->addWidget(deleteButton); + headerLayout->addWidget(newButton); + headerLayout->addWidget(launchButton); + layout->addLayout(headerLayout); + + SearchEdit_ = new QLineEdit(); + SearchEdit_->setPlaceholderText("Search projects..."); + SearchEdit_->setFixedHeight(36); + layout->addWidget(SearchEdit_); + + StatusLabel_ = CreateMutedLabel(""); + StatusLabel_->hide(); + layout->addWidget(StatusLabel_); + + auto* scrollArea = new QScrollArea(); + scrollArea->setObjectName("projectScroll"); + scrollArea->setWidgetResizable(true); + scrollArea->setFrameShape(QFrame::NoFrame); + ProjectListContainer_ = new QWidget(); + ProjectListLayout_ = new QVBoxLayout(ProjectListContainer_); + ProjectListLayout_->setContentsMargins(0, 0, 0, 0); + ProjectListLayout_->setSpacing(10); + ProjectListLayout_->addStretch(1); + scrollArea->setWidget(ProjectListContainer_); + layout->addWidget(scrollArea, 1); + + connect(SearchEdit_, &QLineEdit::textChanged, this, [this]() { + RefreshProjectList(); + }); + connect(newButton, &QPushButton::clicked, this, [this]() { + CreateNewProject(); + }); + connect(deleteButton, &QPushButton::clicked, this, [this]() { + RemoveSelectedProject(); + }); + connect(launchButton, &QPushButton::clicked, this, [this]() { + LaunchSelectedProject(); + }); + + return page; + } + + QWidget* BuildInstallsPage() { + auto* page = new QWidget(); + auto* layout = new QVBoxLayout(page); + layout->setContentsMargins(28, 24, 28, 24); + layout->setSpacing(14); + + auto* headerLayout = new QHBoxLayout(); + auto* titleLabel = new QLabel("Installs"); + titleLabel->setObjectName("pageTitle"); + headerLayout->addWidget(titleLabel); + headerLayout->addStretch(1); + auto* refreshButton = CreateButton("Refresh"); + headerLayout->addWidget(refreshButton); + layout->addLayout(headerLayout); + + layout->addWidget(BuildInstallCard("MetaCore Editor", "", "Locate", [this]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(ExecutableDirectory_))); + }, &EditorInstallStatusLabel_)); + layout->addWidget(BuildInstallCard("MetaCore Player", "", "Locate", [this]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(ExecutableDirectory_))); + }, &PlayerInstallStatusLabel_)); + layout->addWidget(BuildInstallCard("Cook / Package Tool", "", "Locate", [this]() { + QDesktopServices::openUrl(QUrl::fromLocalFile(ToQString(ExecutableDirectory_))); + }, &CookToolInstallStatusLabel_)); + layout->addWidget(BuildInstallCard("Runtime Stack", "C++20 / Filament / ImGui / RmlUi / Cook", "Refresh", [this]() { + RefreshInstallsPage(); + })); + layout->addStretch(1); + + connect(refreshButton, &QPushButton::clicked, this, [this]() { + RefreshInstallsPage(); + }); + RefreshInstallsPage(); + return page; + } + + QWidget* BuildInstallCard( + const QString& title, + const QString& subtitle, + const QString& action, + std::function callback, + QLabel** subtitleLabelOut = nullptr + ) { + auto* card = new QFrame(); + card->setObjectName("installCard"); + card->setMinimumHeight(78); + card->setMaximumHeight(78); + + auto* layout = new QHBoxLayout(card); + layout->setContentsMargins(16, 12, 16, 12); + layout->setSpacing(12); + + auto* textLayout = new QVBoxLayout(); + textLayout->setSpacing(3); + auto* titleLabel = new QLabel(title); + titleLabel->setObjectName("projectName"); + textLayout->addWidget(titleLabel); + QLabel* subtitleLabel = CreateMutedLabel(subtitle); + subtitleLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + textLayout->addWidget(subtitleLabel); + if (subtitleLabelOut != nullptr) { + *subtitleLabelOut = subtitleLabel; + } + layout->addLayout(textLayout, 1); + + auto* button = CreateButton(action); + button->setFixedWidth(90); + connect(button, &QPushButton::clicked, this, std::move(callback)); + layout->addWidget(button); + return card; + } + + void RefreshInstallsPage() { + auto describeExecutable = [this](const QString& label, const std::filesystem::path& executablePath) { + if (std::filesystem::is_regular_file(executablePath)) { + return QString("Installed: %1").arg(ToQString(executablePath)); + } + return QString("Missing: %1 was not found next to the launcher. Build the %2 target first.") + .arg(ToQString(executablePath)) + .arg(label); + }; + + if (EditorInstallStatusLabel_ != nullptr) { + const QString status = describeExecutable("MetaCoreEditorApp", ExecutableDirectory_ / "MetaCoreEditorApp.exe"); + EditorInstallStatusLabel_->setText(status); + EditorInstallStatusLabel_->setToolTip(status); + } + if (PlayerInstallStatusLabel_ != nullptr) { + const QString status = describeExecutable("MetaCorePlayer", ExecutableDirectory_ / "MetaCorePlayer.exe"); + PlayerInstallStatusLabel_->setText(status); + PlayerInstallStatusLabel_->setToolTip(status); + } + if (CookToolInstallStatusLabel_ != nullptr) { + const QString status = describeExecutable("MetaCoreBuildPackageTool", ExecutableDirectory_ / "MetaCoreBuildPackageTool.exe"); + CookToolInstallStatusLabel_->setText(status); + CookToolInstallStatusLabel_->setToolTip(status); + } + } + + void SetPage(int index) { + Pages_->setCurrentIndex(index); + ProjectsNavButton_->setProperty("selected", index == 0); + InstallsNavButton_->setProperty("selected", index == 1); + ProjectsNavButton_->style()->unpolish(ProjectsNavButton_); + ProjectsNavButton_->style()->polish(ProjectsNavButton_); + InstallsNavButton_->style()->unpolish(InstallsNavButton_); + InstallsNavButton_->style()->polish(InstallsNavButton_); + if (index == 1) { + RefreshInstallsPage(); + } + } + + void LoadProjects() { + Projects_.clear(); + std::ifstream stream(DatabasePath_, std::ios::binary); + if (!stream.is_open()) { + return; + } + + std::string line; + while (std::getline(stream, line)) { + std::stringstream lineStream(line); + std::string name; + std::string path; + std::string date; + if (!std::getline(lineStream, name, '\t') || + !std::getline(lineStream, path, '\t') || + !std::getline(lineStream, date, '\t')) { + continue; + } + + LauncherProject project; + project.Name = QString::fromStdString(name); + project.RootPath = std::filesystem::path(path); + project.UpdatedDate = QString::fromStdString(date); + if (IsProjectRoot(project.RootPath)) { + Projects_.push_back(std::move(project)); + } + } + if (!Projects_.empty()) { + SelectedProjectIndex_ = 0; + } + } + + void SaveProjects() const { + std::error_code error; + std::filesystem::create_directories(DatabasePath_.parent_path(), error); + std::ofstream stream(DatabasePath_, std::ios::binary | std::ios::trunc); + if (!stream.is_open()) { + return; + } + + for (const LauncherProject& project : Projects_) { + stream << project.Name.toStdString() << '\t' + << project.RootPath.string() << '\t' + << project.UpdatedDate.toStdString() << '\n'; + } + } + + void RefreshProjectList() { + ClearLayout(ProjectListLayout_); + + const QString searchText = SearchEdit_ != nullptr ? SearchEdit_->text().trimmed().toLower() : QString(); + for (int index = 0; index < static_cast(Projects_.size()); ++index) { + const LauncherProject& project = Projects_[static_cast(index)]; + if (!searchText.isEmpty() && + !project.Name.toLower().contains(searchText) && + !ToQString(project.RootPath).toLower().contains(searchText)) { + continue; + } + + auto* card = new ProjectCard(project); + card->SetSelected(index == SelectedProjectIndex_); + card->OnSelected = [this, index]() { + SelectedProjectIndex_ = index; + RefreshProjectList(); + }; + ProjectListLayout_->addWidget(card); + } + ProjectListLayout_->addStretch(1); + } + + void CreateNewProject() { + NewProjectDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) { + return; + } + + if (FindProjectIndexByName(dialog.ProjectName()) >= 0) { + QMessageBox::critical(this, "Duplicate Name", QString("Project '%1' already exists.").arg(dialog.ProjectName())); + return; + } + + QString errorMessage; + const std::filesystem::path projectRoot = dialog.ProjectRootPath(); + if (!CreateProjectOnDisk(projectRoot, dialog.ProjectName(), errorMessage)) { + QMessageBox::warning(this, "Create New Project", errorMessage); + return; + } + + LauncherProject project; + project.Name = dialog.ProjectName(); + project.RootPath = projectRoot; + project.UpdatedDate = QDate::currentDate().toString(Qt::ISODate); + Projects_.insert(Projects_.begin(), std::move(project)); + SelectedProjectIndex_ = 0; + SaveProjects(); + RefreshProjectList(); + ShowStatus("Created " + dialog.ProjectName()); + } + + void RemoveSelectedProject() { + if (!HasSelectedProject()) { + QMessageBox::warning(this, "No Selection", "Please select a project to delete."); + return; + } + + const LauncherProject& project = Projects_[static_cast(SelectedProjectIndex_)]; + QString deleteSafetyError; + const bool projectFolderExists = std::filesystem::exists(project.RootPath); + if (projectFolderExists && !IsSafeProjectRootForDeletion(project.RootPath, deleteSafetyError)) { + QMessageBox::warning(this, "Delete Project", deleteSafetyError); + return; + } + + const QMessageBox::StandardButton result = QMessageBox::question( + this, + "Confirm Deletion", + QString("Delete project '%1' and remove its folder from disk?\n\n%2") + .arg(project.Name, ToQString(project.RootPath)) + ); + if (result != QMessageBox::Yes) { + return; + } + + if (projectFolderExists) { + std::error_code error; + std::filesystem::remove_all(project.RootPath, error); + if (error) { + QMessageBox::critical( + this, + "Project Deletion Failed", + QString("Failed to remove the project folder:\n%1\n\n%2") + .arg(ToQString(project.RootPath)) + .arg(QString::fromStdString(error.message())) + ); + return; + } + } + + const QString deletedName = project.Name; + Projects_.erase(Projects_.begin() + SelectedProjectIndex_); + SelectedProjectIndex_ = std::min(SelectedProjectIndex_, static_cast(Projects_.size()) - 1); + SaveProjects(); + RefreshProjectList(); + ShowStatus(projectFolderExists ? "Deleted " + deletedName : "Removed missing project from Hub."); + } + + void LaunchSelectedProject() { + if (!HasSelectedProject()) { + ShowStatus("Select a project first."); + return; + } + + LauncherProject& project = Projects_[static_cast(SelectedProjectIndex_)]; + const std::filesystem::path editorPath = ExecutableDirectory_ / "MetaCoreEditorApp.exe"; + if (!std::filesystem::is_regular_file(editorPath)) { + QMessageBox::warning(this, "Launch", "MetaCoreEditorApp.exe was not found next to the launcher."); + return; + } + + QProcess process; + process.setProgram(ToQString(editorPath)); + process.setWorkingDirectory(ToQString(ExecutableDirectory_)); + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + environment.insert("METACORE_PROJECT_PATH", ToQString(project.RootPath)); + process.setProcessEnvironment(environment); + + if (!process.startDetached()) { + QMessageBox::warning(this, "Launch", "Unable to launch MetaCoreEditorApp.exe."); + return; + } + + project.UpdatedDate = QDate::currentDate().toString(Qt::ISODate); + SaveProjects(); + RefreshProjectList(); + ShowStatus("Launched " + project.Name); + } + + bool HasSelectedProject() const { + return SelectedProjectIndex_ >= 0 && SelectedProjectIndex_ < static_cast(Projects_.size()); + } + + int FindProjectIndexByName(const QString& name) const { + for (int index = 0; index < static_cast(Projects_.size()); ++index) { + if (Projects_[static_cast(index)].Name.compare(name, Qt::CaseInsensitive) == 0) { + return index; + } + } + return -1; + } + + bool IsSafeProjectRootForDeletion(const std::filesystem::path& rootPath, QString& errorMessage) const { + std::error_code error; + const std::filesystem::path canonical = std::filesystem::weakly_canonical(rootPath, error); + const std::filesystem::path checkedPath = error ? rootPath.lexically_normal() : canonical; + + if (!std::filesystem::is_directory(rootPath)) { + errorMessage = "Selected project path is not a folder."; + return false; + } + if (!IsProjectRoot(rootPath)) { + errorMessage = "Selected folder does not contain MetaCore.project.json, so it will not be deleted."; + return false; + } + if (checkedPath == checkedPath.root_path()) { + errorMessage = "Refusing to delete a filesystem root."; + return false; + } + if (checkedPath.parent_path() == checkedPath) { + errorMessage = "Refusing to delete an invalid project path."; + return false; + } + + return true; + } + + void ShowStatus(const QString& text) { + if (StatusLabel_ == nullptr) { + return; + } + StatusLabel_->setText(text); + StatusLabel_->show(); + } + + void ApplyStyleSheet(bool darkMode) { + if (!darkMode) { + qApp->setStyleSheet({}); + return; + } + + qApp->setStyleSheet(R"( + QMainWindow#launcherWindow, QWidget#central { + background: #191919; + color: #cfcfcf; + font-size: 13px; + } + QWidget#sidebar { + background: #141414; + border-right: 1px solid #222222; + } + QLabel#hubTitle { + color: #f0f0f0; + font-size: 22px; + font-weight: 700; + } + QLabel#pageTitle { + color: #f0f0f0; + font-size: 24px; + font-weight: 700; + } + QLabel#mutedText { + color: #707070; + } + QLabel#errorText { + color: #eb5757; + } + QLabel#projectName { + color: #e5e5e5; + font-weight: 600; + } + QLabel#projectAvatar { + background: #333333; + color: #f1f1f1; + border-radius: 6px; + font-size: 16px; + font-weight: 700; + } + QPushButton { + background: #202020; + color: #cfcfcf; + border: 1px solid #2f2f2f; + border-radius: 4px; + padding: 7px 12px; + } + QPushButton:hover { + background: #2a2a2a; + } + QPushButton:pressed, QPushButton[selected="true"] { + background: #333333; + } + QPushButton#primaryButton { + background: #f1f1f1; + color: #151515; + border-color: #f1f1f1; + } + QPushButton#primaryButton:hover { + background: #ffffff; + } + QPushButton#primaryButton:disabled { + background: #555555; + color: #9a9a9a; + border-color: #555555; + } + QPushButton#dangerButton { + background: #2a2020; + color: #f2b7b7; + border-color: #4a2d2d; + } + QPushButton#dangerButton:hover { + background: #3a2424; + } + QPushButton#navButton { + text-align: left; + min-height: 36px; + border: none; + padding-left: 12px; + background: #141414; + } + QPushButton#navButton:hover { + background: #1e1e1e; + } + QPushButton#navButton[selected="true"] { + background: #252525; + } + QPushButton#smallButton { + padding: 5px 8px; + } + QLineEdit, QComboBox { + background: #202020; + color: #d8d8d8; + border: 1px solid #2f2f2f; + border-radius: 4px; + padding: 8px 10px; + } + QLineEdit:focus, QComboBox:focus { + border-color: #555555; + } + QLineEdit:read-only { + color: #b8b8b8; + } + QFrame#projectCard, QFrame#installCard { + background: #202020; + border: 1px solid #2f2f2f; + border-radius: 6px; + } + QFrame#projectCard:hover { + background: #2a2a2a; + } + QFrame#projectCard[selected="true"] { + background: #333333; + border-color: #4a4a4a; + } + QScrollArea#projectScroll { + background: transparent; + } + QScrollArea#projectScroll > QWidget > QWidget { + background: transparent; + } + QDialog { + background: #191919; + color: #cfcfcf; + } + QMessageBox { + background: #191919; + color: #cfcfcf; + } + QCheckBox { + color: #cfcfcf; + } + )"); + } + + std::vector Projects_{}; + int SelectedProjectIndex_ = -1; + std::filesystem::path ExecutableDirectory_{}; + std::filesystem::path DatabasePath_{}; + + QStackedWidget* Pages_ = nullptr; + QPushButton* ProjectsNavButton_ = nullptr; + QPushButton* InstallsNavButton_ = nullptr; + QLineEdit* SearchEdit_ = nullptr; + QLabel* StatusLabel_ = nullptr; + QLabel* EditorInstallStatusLabel_ = nullptr; + QLabel* PlayerInstallStatusLabel_ = nullptr; + QLabel* CookToolInstallStatusLabel_ = nullptr; + QWidget* ProjectListContainer_ = nullptr; + QVBoxLayout* ProjectListLayout_ = nullptr; +}; + +} // namespace + +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + QApplication::setApplicationName(ApplicationName); + QApplication::setOrganizationName(OrganizationName); + QApplication::setApplicationVersion("1.0.0"); + + MetaCoreLauncherWindow window; + window.show(); + return app.exec(); +} diff --git a/Apps/MetaCorePlayer/main.cpp b/Apps/MetaCorePlayer/main.cpp index c496651..28c1926 100644 --- a/Apps/MetaCorePlayer/main.cpp +++ b/Apps/MetaCorePlayer/main.cpp @@ -1,18 +1,31 @@ #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" +#include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataDispatcher.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataSource.h" #include "MetaCoreScene/MetaCoreScenePackage.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreScene/MetaCoreScene.h" +#include "MetaCoreScene/MetaCoreUiRmlCompiler.h" +#include +#include #include +#include #include #include +#include +#include +#include +#include +#include +#include namespace { @@ -25,15 +38,143 @@ MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() { return sceneView; } -[[nodiscard]] MetaCore::MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument() { - MetaCore::MetaCoreRuntimeProjectDocument document; - document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; - document.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; - document.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; - document.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; +struct MetaCoreRuntimeDataSourceInstance { + MetaCore::MetaCoreDataSourceDefinition Definition{}; + std::unique_ptr Adapter{}; + MetaCore::MetaCoreRuntimeDataSourceState LastReportedState = + MetaCore::MetaCoreRuntimeDataSourceState::Disconnected; +}; + +[[nodiscard]] std::optional MetaCoreResolveRuntimeDataReplayFilePaths( + MetaCore::MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + const std::filesystem::path& projectRoot +) { + for (MetaCore::MetaCoreDataSourceDefinition& sourceDefinition : sourcesDocument.Sources) { + if (sourceDefinition.AdapterType != "file_replay") { + continue; + } + + for (MetaCore::MetaCoreDataSourceSetting& setting : sourceDefinition.ConnectionSettings) { + if (setting.Key != "file_path" || setting.Value.empty()) { + continue; + } + + const std::filesystem::path replayPath(setting.Value); + if (replayPath.is_absolute()) { + continue; + } + if (MetaCore::MetaCoreIsUnsafeRuntimeProjectPath(replayPath)) { + return "RuntimeData source " + sourceDefinition.Id + + " file_path must be project-relative without parent traversal: " + + replayPath.generic_string(); + } + + setting.Value = (projectRoot / replayPath.lexically_normal()).string(); + } + } + return std::nullopt; +} + +[[nodiscard]] std::optional MetaCoreReadCookManifestDocument( + const std::filesystem::path& manifestPath, + const MetaCore::MetaCoreTypeRegistry& registry +) { + std::ifstream input(manifestPath, std::ios::binary); + if (!input.is_open()) { + return std::nullopt; + } + + input.seekg(0, std::ios::end); + const auto size = static_cast(input.tellg()); + input.seekg(0, std::ios::beg); + std::vector buffer(size); + if (size > 0 && !input.read(reinterpret_cast(buffer.data()), static_cast(size))) { + return std::nullopt; + } + + MetaCore::MetaCoreCookManifestDocument document; + if (!MetaCore::MetaCoreDeserializeFromBytes( + std::span(buffer.data(), buffer.size()), + document, + registry)) { + return std::nullopt; + } return document; } +[[nodiscard]] std::optional MetaCoreReadSceneDocumentFromPath( + const std::filesystem::path& path, + const MetaCore::MetaCoreTypeRegistry& registry +) { + if (path.extension() == ".json") { + return MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(path, registry); + } + return MetaCore::MetaCoreReadScenePackage(path); +} + +[[nodiscard]] std::string MetaCoreBuildPortablePathKey(const std::filesystem::path& path) { + return path.lexically_normal().generic_string(); +} + +[[nodiscard]] const MetaCore::MetaCoreCookManifestEntry* MetaCoreFindCookedManifestEntry( + const MetaCore::MetaCoreCookManifestDocument& manifest, + const std::filesystem::path& relativeSourcePath +) { + const std::string requestedPath = MetaCoreBuildPortablePathKey(relativeSourcePath); + const auto iterator = std::find_if( + manifest.Entries.begin(), + manifest.Entries.end(), + [&](const MetaCore::MetaCoreCookManifestEntry& entry) { + return MetaCoreBuildPortablePathKey(entry.SourcePackagePath) == requestedPath; + } + ); + return iterator == manifest.Entries.end() ? nullptr : &*iterator; +} + +template +[[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( + const MetaCore::MetaCorePackageDocument& package, + const MetaCore::MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +) { + const MetaCore::MetaCoreTypeId expectedTypeId = MetaCore::MetaCoreMakeTypeId(expectedTypeName); + for (const MetaCore::MetaCoreExportEntry& exportEntry : package.Exports) { + if (exportEntry.TypeId != expectedTypeId || exportEntry.PayloadIndex >= package.PayloadSections.size()) { + continue; + } + + T payload{}; + if (MetaCore::MetaCoreDeserializeFromBytes( + package.PayloadSections[exportEntry.PayloadIndex], + payload, + registry)) { + return payload; + } + } + return std::nullopt; +} + +template +[[nodiscard]] std::optional MetaCoreReadCookedDocument( + const std::filesystem::path& projectRoot, + const MetaCore::MetaCoreCookManifestDocument& manifest, + const std::filesystem::path& relativeSourcePath, + const MetaCore::MetaCoreTypeRegistry& registry, + std::string_view expectedTypeName +) { + const MetaCore::MetaCoreCookManifestEntry* entry = + MetaCoreFindCookedManifestEntry(manifest, relativeSourcePath); + if (entry == nullptr || entry->CookedPath.empty()) { + return std::nullopt; + } + + const auto cookedPackage = MetaCore::MetaCoreReadPackageFile(projectRoot / entry->CookedPath, registry); + if (!cookedPackage.has_value()) { + return std::nullopt; + } + return MetaCoreReadTypedPackagePayload(*cookedPackage, registry, expectedTypeName); +} + [[nodiscard]] MetaCore::MetaCoreRuntimeDataSourcesDocument MetaCoreBuildDefaultRuntimeDataSourcesDocument() { MetaCore::MetaCoreRuntimeDataSourcesDocument document; document.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ @@ -130,6 +271,313 @@ MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() { return document; } +[[nodiscard]] std::string MetaCoreBuildRuntimeDataStatusText( + const MetaCore::MetaCoreRuntimeDiagnosticsSnapshot& diagnostics +) { + const auto connectedSources = std::count_if( + diagnostics.SourceStatuses.begin(), + diagnostics.SourceStatuses.end(), + [](const MetaCore::MetaCoreRuntimeDataSourceStatus& status) { + return status.State == MetaCore::MetaCoreRuntimeDataSourceState::Connected; + } + ); + const auto sourceIssues = std::count_if( + diagnostics.SourceStatuses.begin(), + diagnostics.SourceStatuses.end(), + [](const MetaCore::MetaCoreRuntimeDataSourceStatus& status) { + return status.State == MetaCore::MetaCoreRuntimeDataSourceState::Degraded || + status.State == MetaCore::MetaCoreRuntimeDataSourceState::Faulted; + } + ); + const auto bindingIssues = std::count_if( + diagnostics.BindingStatuses.begin(), + diagnostics.BindingStatuses.end(), + [](const MetaCore::MetaCoreRuntimeBindingStatus& status) { + return !status.Healthy || status.Stale; + } + ); + + return "RuntimeData sources=" + std::to_string(diagnostics.SourceStatuses.size()) + + " connected=" + std::to_string(static_cast(connectedSources)) + + " source_issues=" + std::to_string(static_cast(sourceIssues)) + + " bindings=" + std::to_string(diagnostics.BindingStatuses.size()) + + " binding_issues=" + std::to_string(static_cast(bindingIssues)); +} + +[[nodiscard]] std::string MetaCoreFormatRuntimeDataValue( + const MetaCore::MetaCoreRuntimeDataValue& value +) { + std::ostringstream stream; + switch (value.Type) { + case MetaCore::MetaCoreRuntimeValueType::Bool: + return value.BoolValue ? "true" : "false"; + case MetaCore::MetaCoreRuntimeValueType::Int64: + return std::to_string(value.Int64Value); + case MetaCore::MetaCoreRuntimeValueType::Double: + stream << value.DoubleValue; + return stream.str(); + case MetaCore::MetaCoreRuntimeValueType::String: + return value.StringValue; + case MetaCore::MetaCoreRuntimeValueType::Vec3: + stream << value.Vec3Value.x << ", " << value.Vec3Value.y << ", " << value.Vec3Value.z; + return stream.str(); + } + return {}; +} + +[[nodiscard]] const MetaCore::MetaCoreDataPointDefinition* MetaCoreFindRuntimeDataPoint( + const std::vector& dataPoints, + const std::string& dataPointId +) { + const auto iterator = std::find_if( + dataPoints.begin(), + dataPoints.end(), + [&](const MetaCore::MetaCoreDataPointDefinition& dataPoint) { + return dataPoint.Id == dataPointId; + } + ); + return iterator == dataPoints.end() ? nullptr : &*iterator; +} + +void MetaCoreMarkRuntimeBindingFault( + std::vector& statuses, + const std::string& bindingId, + const std::string& error +) { + const auto iterator = std::find_if( + statuses.begin(), + statuses.end(), + [&](const MetaCore::MetaCoreRuntimeBindingStatus& status) { + return status.BindingId == bindingId; + } + ); + if (iterator == statuses.end()) { + return; + } + + iterator->Healthy = false; + iterator->Stale = false; + iterator->LastError = error; +} + +void MetaCoreMarkRuntimeBindingHealthy( + std::vector& statuses, + const std::string& bindingId, + std::uint64_t appliedAt +) { + const auto iterator = std::find_if( + statuses.begin(), + statuses.end(), + [&](const MetaCore::MetaCoreRuntimeBindingStatus& status) { + return status.BindingId == bindingId; + } + ); + if (iterator == statuses.end()) { + return; + } + + iterator->Healthy = true; + iterator->Stale = false; + iterator->LastAppliedAt = appliedAt; + iterator->LastError.clear(); +} + +[[nodiscard]] std::vector MetaCoreBuildRuntimeUiBindingStatuses( + const std::vector& uiBindings +) { + std::vector statuses; + statuses.reserve(uiBindings.size()); + for (const MetaCore::MetaCoreUiBindingDefinition& binding : uiBindings) { + statuses.push_back(MetaCore::MetaCoreRuntimeBindingStatus{ + binding.BindingId, + true, + false, + 0, + 1000, + {} + }); + } + return statuses; +} + +[[nodiscard]] bool MetaCoreHasRuntimeUiTextBindingForNode( + const std::vector& uiBindings, + std::string_view nodeId +) { + return std::any_of( + uiBindings.begin(), + uiBindings.end(), + [nodeId](const MetaCore::MetaCoreUiBindingDefinition& binding) { + return binding.Target == MetaCore::MetaCoreRuntimeUiBindingTarget::Text && + binding.TargetNodeId == nodeId; + } + ); +} + +void MetaCoreValidateRuntimeUiBindingTargets( + const std::vector& uiBindings, + const MetaCore::MetaCoreRuntimeUiRenderer& runtimeUiRenderer, + std::vector& uiBindingStatuses +) { + for (const MetaCore::MetaCoreUiBindingDefinition& binding : uiBindings) { + if (binding.Target != MetaCore::MetaCoreRuntimeUiBindingTarget::Text) { + continue; + } + + if (!runtimeUiRenderer.GetStats().Loaded) { + MetaCoreMarkRuntimeBindingFault( + uiBindingStatuses, + binding.BindingId, + "Runtime UI document is not loaded" + ); + continue; + } + + if (!runtimeUiRenderer.HasNode(binding.TargetNodeId)) { + MetaCoreMarkRuntimeBindingFault( + uiBindingStatuses, + binding.BindingId, + "Runtime UI node was not found: " + binding.TargetNodeId + ); + } + } +} + +void MetaCoreApplyRuntimeUiBindings( + const std::vector& updates, + const std::vector& dataPoints, + const std::vector& uiBindings, + MetaCore::MetaCoreRuntimeUiRenderer& runtimeUiRenderer, + std::vector& uiBindingStatuses +) { + for (const MetaCore::MetaCoreRuntimeDataUpdate& update : updates) { + const MetaCore::MetaCoreDataPointDefinition* dataPoint = + MetaCoreFindRuntimeDataPoint(dataPoints, update.DataPointId); + if (dataPoint == nullptr) { + continue; + } + + for (const MetaCore::MetaCoreUiBindingDefinition& binding : uiBindings) { + if (binding.DataPointId != update.DataPointId) { + continue; + } + if (update.Value.Type != dataPoint->ValueType) { + MetaCoreMarkRuntimeBindingFault( + uiBindingStatuses, + binding.BindingId, + "Update type does not match data point definition" + ); + continue; + } + if (update.Value.Quality == MetaCore::MetaCoreRuntimeDataQuality::Bad) { + MetaCoreMarkRuntimeBindingFault(uiBindingStatuses, binding.BindingId, "Update quality is bad"); + continue; + } + if (binding.Target == MetaCore::MetaCoreRuntimeUiBindingTarget::Text) { + if (!runtimeUiRenderer.SetNodeText(binding.TargetNodeId, MetaCoreFormatRuntimeDataValue(update.Value))) { + MetaCoreMarkRuntimeBindingFault( + uiBindingStatuses, + binding.BindingId, + runtimeUiRenderer.GetStats().LastError + ); + continue; + } + } + MetaCoreMarkRuntimeBindingHealthy(uiBindingStatuses, binding.BindingId, update.Value.SourceTimestamp); + } + } +} + +void MetaCoreTickRuntimeBindingStaleness( + std::vector& statuses, + std::uint64_t currentTimestamp +) { + for (MetaCore::MetaCoreRuntimeBindingStatus& status : statuses) { + if (status.LastAppliedAt == 0) { + status.Stale = true; + continue; + } + status.Stale = + currentTimestamp > status.LastAppliedAt && + (currentTimestamp - status.LastAppliedAt) > status.StaleAfterMs; + } +} + +void MetaCoreAppendRuntimeUiBindingDiagnostics( + MetaCore::MetaCoreRuntimeDiagnosticsSnapshot& diagnostics, + const std::vector& uiBindingStatuses +) { + diagnostics.BindingStatuses.insert( + diagnostics.BindingStatuses.end(), + uiBindingStatuses.begin(), + uiBindingStatuses.end() + ); + diagnostics.HasFaults = diagnostics.HasFaults || std::any_of( + uiBindingStatuses.begin(), + uiBindingStatuses.end(), + [](const MetaCore::MetaCoreRuntimeBindingStatus& status) { + return !status.Healthy || status.Stale; + } + ); +} + +[[nodiscard]] std::vector MetaCoreCollectRuntimeSourceStatuses( + const std::vector& runtimeSources +) { + std::vector sourceStatuses; + sourceStatuses.reserve(runtimeSources.size()); + for (const MetaCoreRuntimeDataSourceInstance& sourceInstance : runtimeSources) { + sourceStatuses.push_back(sourceInstance.Adapter->GetStatus()); + } + return sourceStatuses; +} + +void MetaCoreWriteRuntimeDiagnosticsSnapshot( + const std::filesystem::path& diagnosticsPath, + const MetaCore::MetaCoreTypeRegistry& typeRegistry, + const MetaCore::MetaCoreRuntimeDataDispatcher& runtimeDataDispatcher, + const std::vector& sourceStatuses, + const std::vector& uiBindingStatuses +) { + MetaCore::MetaCoreRuntimeDiagnosticsSnapshot diagnostics = + runtimeDataDispatcher.BuildDiagnosticsSnapshot(sourceStatuses); + MetaCoreAppendRuntimeUiBindingDiagnostics(diagnostics, uiBindingStatuses); + (void)MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot(diagnosticsPath, diagnostics, typeRegistry); +} + +[[nodiscard]] std::string MetaCoreRuntimeBindingIssueSignature( + const MetaCore::MetaCoreRuntimeBindingStatus& bindingStatus +) { + std::ostringstream stream; + stream << (bindingStatus.Healthy ? "healthy" : "fault") + << "|stale=" << (bindingStatus.Stale ? "true" : "false") + << "|error=" << bindingStatus.LastError; + return stream.str(); +} + +void MetaCoreLogRuntimeBindingIssues( + const MetaCore::MetaCoreRuntimeDiagnosticsSnapshot& diagnostics, + std::unordered_map& reportedBindingIssues +) { + for (const MetaCore::MetaCoreRuntimeBindingStatus& bindingStatus : diagnostics.BindingStatuses) { + if (bindingStatus.Healthy && !bindingStatus.Stale) { + reportedBindingIssues.erase(bindingStatus.BindingId); + continue; + } + + const std::string issueSignature = MetaCoreRuntimeBindingIssueSignature(bindingStatus); + const auto reportedIterator = reportedBindingIssues.find(bindingStatus.BindingId); + if (reportedIterator != reportedBindingIssues.end() && reportedIterator->second == issueSignature) { + continue; + } + + std::cout << "MetaCorePlayer: binding issue id=" << bindingStatus.BindingId + << " stale=" << (bindingStatus.Stale ? "true" : "false") + << " error=" << bindingStatus.LastError << '\n'; + reportedBindingIssues[bindingStatus.BindingId] = issueSignature; + } +} + } // namespace int main(int argc, char* argv[]) { @@ -167,6 +615,12 @@ int main(int argc, char* argv[]) { return 1; } + MetaCore::MetaCoreRuntimeUiRenderer runtimeUiRenderer; + if (!runtimeUiRenderer.Initialize()) { + std::cerr << "MetaCorePlayer: runtime UI renderer initialize failed\n"; + return 1; + } + std::filesystem::path projectRoot; if (!customProjectRoot.empty()) { projectRoot = std::filesystem::absolute(customProjectRoot); @@ -181,20 +635,83 @@ int main(int argc, char* argv[]) { const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(projectRoot); viewportRenderer.SetProjectRootPath(projectRoot); MetaCore::MetaCoreTypeRegistry typeRegistry; + MetaCore::MetaCoreRegisterFoundationGeneratedTypes(typeRegistry); + MetaCore::MetaCoreRegisterSceneGeneratedTypes(typeRegistry); MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(typeRegistry); + const auto projectDocument = MetaCore::MetaCoreReadProjectFile(projectPath); + std::filesystem::path runtimeDirectoryRelative = projectDocument.has_value() + ? projectDocument->RuntimeDirectory + : std::filesystem::path("Runtime"); + if (runtimeDirectoryRelative.empty()) { + runtimeDirectoryRelative = "Runtime"; + } + if (MetaCore::MetaCoreIsUnsafeRuntimeProjectPath(runtimeDirectoryRelative)) { + std::cerr << "MetaCorePlayer: project runtime_directory must be project-relative without parent traversal: " + << runtimeDirectoryRelative.generic_string() << '\n'; + return 1; + } + const std::filesystem::path runtimeDirectory = + projectRoot / runtimeDirectoryRelative.lexically_normal(); const auto loadedRuntimeProjectDocument = MetaCore::MetaCoreReadRuntimeProjectDocument( - projectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + runtimeDirectory / "ProjectRuntime.mcruntimecfg", typeRegistry ); - const auto runtimeProjectDocument = loadedRuntimeProjectDocument.value_or(MetaCoreBuildDefaultRuntimeProjectDocument()); + auto runtimeProjectDocument = + loadedRuntimeProjectDocument.value_or(MetaCore::MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative)); + MetaCore::MetaCoreApplyRuntimeProjectDefaults(runtimeProjectDocument, runtimeDirectoryRelative); + const std::vector runtimeProjectPathIssues = + MetaCore::MetaCoreValidateRuntimeProjectPaths(runtimeProjectDocument); + if (!runtimeProjectPathIssues.empty()) { + std::cerr << "MetaCorePlayer: Runtime project path validation failed: " + << runtimeProjectPathIssues.front().Message << '\n'; + return 1; + } + std::cout << "MetaCorePlayer: build profile=" << runtimeProjectDocument.BuildProfileName + << " platform=" << runtimeProjectDocument.TargetPlatform + << " output=" << runtimeProjectDocument.OutputDirectory.generic_string() + << " cooked_assets=" << (runtimeProjectDocument.UseCookedAssets ? "true" : "false") + << '\n'; + + std::optional cookedManifest; + if (runtimeProjectDocument.UseCookedAssets) { + const std::filesystem::path manifestPath = + projectRoot / runtimeProjectDocument.CookedAssetsDirectory / "CookManifest.bin"; + cookedManifest = MetaCoreReadCookManifestDocument(manifestPath, typeRegistry); + if (cookedManifest.has_value()) { + std::cout << "MetaCorePlayer: loaded cook manifest " + << manifestPath.string() + << " entries=" << cookedManifest->Entries.size() + << '\n'; + } else { + std::cerr << "MetaCorePlayer: cooked assets enabled but cook manifest is missing or unreadable: " + << manifestPath.string() << '\n'; + } + } std::optional startupSceneDocument; if (!customScenePath.empty()) { const auto absoluteScene = customScenePath.is_absolute() ? customScenePath : (projectRoot / customScenePath); - startupSceneDocument = MetaCore::MetaCoreReadScenePackage(absoluteScene); + startupSceneDocument = MetaCoreReadSceneDocumentFromPath(absoluteScene, typeRegistry); + } + if (!startupSceneDocument.has_value() && + cookedManifest.has_value() && + !runtimeProjectDocument.StartupScenePath.empty()) { + startupSceneDocument = MetaCoreReadCookedDocument( + projectRoot, + *cookedManifest, + runtimeProjectDocument.StartupScenePath, + typeRegistry, + "MetaCoreSceneDocument" + ); + if (startupSceneDocument.has_value()) { + std::cout << "MetaCorePlayer: loaded cooked startup scene " + << runtimeProjectDocument.StartupScenePath.generic_string() + << '\n'; + } } if (!startupSceneDocument.has_value() && !runtimeProjectDocument.StartupScenePath.empty()) { - startupSceneDocument = MetaCore::MetaCoreReadScenePackage(projectRoot / runtimeProjectDocument.StartupScenePath); + startupSceneDocument = + MetaCoreReadSceneDocumentFromPath(projectRoot / runtimeProjectDocument.StartupScenePath, typeRegistry); } if (!startupSceneDocument.has_value()) { startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath); @@ -210,6 +727,58 @@ int main(int argc, char* argv[]) { scene = MetaCore::MetaCoreCreateDefaultScene(); std::cout << "MetaCorePlayer: startup scene unavailable, using built-in default scene\n"; } + + std::optional startupUiDocument; + if (!runtimeProjectDocument.StartupUiPath.empty()) { + bool startupUiLoadedFromCooked = false; + if (cookedManifest.has_value()) { + startupUiDocument = MetaCoreReadCookedDocument( + projectRoot, + *cookedManifest, + runtimeProjectDocument.StartupUiPath, + typeRegistry, + "MetaCoreUiDocument" + ); + startupUiLoadedFromCooked = startupUiDocument.has_value(); + } + if (!startupUiDocument.has_value()) { + const std::filesystem::path absoluteUiPath = + runtimeProjectDocument.StartupUiPath.is_absolute() + ? runtimeProjectDocument.StartupUiPath + : (projectRoot / runtimeProjectDocument.StartupUiPath); + if (std::filesystem::exists(absoluteUiPath)) { + startupUiDocument = MetaCore::MetaCoreSceneSerializer::LoadUiFromJson(absoluteUiPath, typeRegistry); + if (!startupUiDocument.has_value()) { + std::cerr << "MetaCorePlayer: startup UI exists but is unreadable: " + << runtimeProjectDocument.StartupUiPath.generic_string() + << '\n'; + } + } else { + std::cout << "MetaCorePlayer: startup UI not found: " + << runtimeProjectDocument.StartupUiPath.generic_string() + << '\n'; + } + } + if (startupUiDocument.has_value()) { + const auto compiledUi = MetaCore::MetaCoreCompileUiDocumentToRml( + *startupUiDocument, + runtimeProjectDocument.StartupUiPath.filename().string() + ".rcss" + ); + if (!runtimeUiRenderer.LoadCompiledDocument(compiledUi)) { + std::cerr << "MetaCorePlayer: startup UI failed to load into runtime renderer error=" + << runtimeUiRenderer.GetStats().LastError << '\n'; + } + std::cout << "MetaCorePlayer: loaded " + << (startupUiLoadedFromCooked ? "cooked " : "") + << "startup UI " + << runtimeProjectDocument.StartupUiPath.generic_string() + << " nodes=" << startupUiDocument->Nodes.size() + << " roots=" << startupUiDocument->RootNodeIds.size() + << " rml_bytes=" << compiledUi.Rml.size() + << " rcss_bytes=" << compiledUi.Rcss.size() + << '\n'; + } + } MetaCore::MetaCoreRuntimeDataDispatcher runtimeDataDispatcher(scene); const auto sourcesPath = (projectRoot / runtimeProjectDocument.DataSourcesPath).lexically_normal(); @@ -217,7 +786,7 @@ int main(int argc, char* argv[]) { const auto diagnosticsPath = (projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal(); const auto loadedSourcesDocument = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, typeRegistry); const auto loadedBindingsDocument = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, typeRegistry); - const auto sourcesDocument = loadedSourcesDocument.value_or(MetaCoreBuildDefaultRuntimeDataSourcesDocument()); + auto sourcesDocument = loadedSourcesDocument.value_or(MetaCoreBuildDefaultRuntimeDataSourcesDocument()); const auto bindingsDocument = loadedBindingsDocument.value_or(MetaCoreBuildDefaultRuntimeBindingsDocument()); if (loadedSourcesDocument.has_value() && loadedBindingsDocument.has_value()) { @@ -232,36 +801,86 @@ int main(int argc, char* argv[]) { std::cout << "MetaCorePlayer: runtime config missing, using built-in fallback config\n"; } } + if (const auto replayPathError = MetaCoreResolveRuntimeDataReplayFilePaths(sourcesDocument, projectRoot); + replayPathError.has_value()) { + std::cerr << "MetaCorePlayer: " << *replayPathError << '\n'; + return 1; + } runtimeDataDispatcher.SetDataPointDefinitions(sourcesDocument.DataPoints); runtimeDataDispatcher.SetBindingDefinitions(bindingsDocument.Bindings); + std::vector runtimeUiBindingStatuses = + MetaCoreBuildRuntimeUiBindingStatuses(bindingsDocument.UiBindings); + MetaCoreValidateRuntimeUiBindingTargets(bindingsDocument.UiBindings, runtimeUiRenderer, runtimeUiBindingStatuses); - std::unique_ptr runtimeAdapter; - if (!sourcesDocument.Sources.empty()) { - runtimeAdapter = MetaCore::MetaCoreCreateRuntimeDataSourceAdapter(sourcesDocument.Sources.front().AdapterType); + std::vector runtimeSources; + runtimeSources.reserve(sourcesDocument.Sources.size()); + for (const MetaCore::MetaCoreDataSourceDefinition& sourceDefinition : sourcesDocument.Sources) { + auto runtimeAdapter = MetaCore::MetaCoreCreateRuntimeDataSourceAdapter(sourceDefinition.AdapterType); if (runtimeAdapter == nullptr) { std::cerr << "MetaCorePlayer: unsupported adapter type " - << sourcesDocument.Sources.front().AdapterType << '\n'; + << sourceDefinition.AdapterType + << " source=" << sourceDefinition.Id << '\n'; + auto sourceStatuses = MetaCoreCollectRuntimeSourceStatuses(runtimeSources); + MetaCore::MetaCoreRuntimeDataSourceStatus unsupportedStatus; + unsupportedStatus.SourceId = sourceDefinition.Id; + unsupportedStatus.State = MetaCore::MetaCoreRuntimeDataSourceState::Faulted; + unsupportedStatus.LastError = "Unsupported adapter type: " + sourceDefinition.AdapterType; + sourceStatuses.push_back(std::move(unsupportedStatus)); + MetaCoreWriteRuntimeDiagnosticsSnapshot( + diagnosticsPath, + typeRegistry, + runtimeDataDispatcher, + sourceStatuses, + runtimeUiBindingStatuses + ); return 1; } - if (!runtimeAdapter->Configure(sourcesDocument.Sources.front())) { + if (!runtimeAdapter->Configure(sourceDefinition)) { std::cerr << "MetaCorePlayer: failed to configure runtime adapter error=" << runtimeAdapter->GetStatus().LastError << '\n'; + auto sourceStatuses = MetaCoreCollectRuntimeSourceStatuses(runtimeSources); + sourceStatuses.push_back(runtimeAdapter->GetStatus()); + MetaCoreWriteRuntimeDiagnosticsSnapshot( + diagnosticsPath, + typeRegistry, + runtimeDataDispatcher, + sourceStatuses, + runtimeUiBindingStatuses + ); return 1; } - if (!runtimeAdapter->Connect()) { + MetaCoreRuntimeDataSourceInstance sourceInstance; + sourceInstance.Definition = sourceDefinition; + sourceInstance.LastReportedState = runtimeAdapter->GetStatus().State; + sourceInstance.Adapter = std::move(runtimeAdapter); + if (sourceDefinition.AutoConnect && !sourceInstance.Adapter->Connect()) { std::cerr << "MetaCorePlayer: failed to connect runtime adapter error=" - << runtimeAdapter->GetStatus().LastError << '\n'; + << sourceInstance.Adapter->GetStatus().LastError + << " source=" << sourceDefinition.Id << '\n'; + auto sourceStatuses = MetaCoreCollectRuntimeSourceStatuses(runtimeSources); + sourceStatuses.push_back(sourceInstance.Adapter->GetStatus()); + MetaCoreWriteRuntimeDiagnosticsSnapshot( + diagnosticsPath, + typeRegistry, + runtimeDataDispatcher, + sourceStatuses, + runtimeUiBindingStatuses + ); return 1; } + sourceInstance.LastReportedState = sourceInstance.Adapter->GetStatus().State; + runtimeSources.push_back(std::move(sourceInstance)); } - MetaCore::MetaCoreRuntimeDataSourceState lastReportedSourceState = - runtimeAdapter != nullptr - ? runtimeAdapter->GetStatus().State - : MetaCore::MetaCoreRuntimeDataSourceState::Disconnected; - std::uint64_t diagnosticsWriteFrame = 0; + std::unordered_map reportedBindingIssues; + bool runtimeStatusUiAvailable = + !MetaCoreHasRuntimeUiTextBindingForNode(bindingsDocument.UiBindings, "runtime.status"); + if (!runtimeStatusUiAvailable) { + std::cout << "MetaCorePlayer: runtime.status is controlled by a RuntimeData UI binding\n"; + } + std::string lastRuntimeStatusText{}; while (!window.ShouldClose()) { window.BeginFrame(); const auto [windowWidth, windowHeight] = window.GetWindowSize(); @@ -271,42 +890,66 @@ int main(int argc, char* argv[]) { static_cast(windowWidth), static_cast(windowHeight) }); - if (runtimeAdapter != nullptr) { - runtimeAdapter->Tick(1.0 / 60.0); - runtimeDataDispatcher.ApplyUpdates(runtimeAdapter->PollUpdates()); - runtimeDataDispatcher.TickStaleness(runtimeAdapter->GetStatus().LastUpdateAt); + runtimeUiRenderer.Resize(windowWidth, windowHeight); + runtimeUiRenderer.BeginFrame(1.0F / 60.0F); + if (!runtimeSources.empty()) { + std::uint64_t latestUpdateAt = 0; + for (MetaCoreRuntimeDataSourceInstance& sourceInstance : runtimeSources) { + sourceInstance.Adapter->Tick(1.0 / 60.0); + const std::vector updates = + sourceInstance.Adapter->PollUpdates(); + runtimeDataDispatcher.ApplyUpdates(updates); + MetaCoreApplyRuntimeUiBindings( + updates, + sourcesDocument.DataPoints, + bindingsDocument.UiBindings, + runtimeUiRenderer, + runtimeUiBindingStatuses + ); + latestUpdateAt = std::max(latestUpdateAt, sourceInstance.Adapter->GetStatus().LastUpdateAt); + } + runtimeDataDispatcher.TickStaleness(latestUpdateAt); + MetaCoreTickRuntimeBindingStaleness(runtimeUiBindingStatuses, latestUpdateAt); + } + std::vector sourceStatuses = + MetaCoreCollectRuntimeSourceStatuses(runtimeSources); + auto diagnostics = runtimeDataDispatcher.BuildDiagnosticsSnapshot(sourceStatuses); + MetaCoreAppendRuntimeUiBindingDiagnostics(diagnostics, runtimeUiBindingStatuses); + if (runtimeStatusUiAvailable && runtimeUiRenderer.GetStats().Loaded) { + const std::string runtimeStatusText = MetaCoreBuildRuntimeDataStatusText(diagnostics); + if (runtimeStatusText != lastRuntimeStatusText) { + if (runtimeUiRenderer.SetNodeText("runtime.status", runtimeStatusText)) { + lastRuntimeStatusText = runtimeStatusText; + } else { + runtimeStatusUiAvailable = false; + } + } } - const auto diagnostics = runtimeDataDispatcher.BuildDiagnosticsSnapshot( - runtimeAdapter != nullptr - ? std::vector{runtimeAdapter->GetStatus()} - : std::vector{} - ); if ((diagnosticsWriteFrame % 15ULL) == 0ULL) { (void)MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot(diagnosticsPath, diagnostics, typeRegistry); } ++diagnosticsWriteFrame; - if (runtimeAdapter != nullptr && runtimeAdapter->GetStatus().State != lastReportedSourceState) { - std::cout << "MetaCorePlayer: data source state changed to " - << static_cast(runtimeAdapter->GetStatus().State) - << " error=" << runtimeAdapter->GetStatus().LastError << '\n'; - lastReportedSourceState = runtimeAdapter->GetStatus().State; - } - if (diagnostics.HasFaults) { - for (const MetaCore::MetaCoreRuntimeBindingStatus& bindingStatus : diagnostics.BindingStatuses) { - if (!bindingStatus.Healthy || bindingStatus.Stale) { - std::cout << "MetaCorePlayer: binding issue id=" << bindingStatus.BindingId - << " stale=" << (bindingStatus.Stale ? "true" : "false") - << " error=" << bindingStatus.LastError << '\n'; - } + for (MetaCoreRuntimeDataSourceInstance& sourceInstance : runtimeSources) { + if (sourceInstance.Adapter->GetStatus().State != sourceInstance.LastReportedState) { + std::cout << "MetaCorePlayer: data source state changed source=" + << sourceInstance.Definition.Id + << " adapter=" << sourceInstance.Definition.AdapterType + << " state=" << static_cast(sourceInstance.Adapter->GetStatus().State) + << " error=" << sourceInstance.Adapter->GetStatus().LastError << '\n'; + sourceInstance.LastReportedState = sourceInstance.Adapter->GetStatus().State; } } + MetaCoreLogRuntimeBindingIssues(diagnostics, reportedBindingIssues); viewportRenderer.RenderSceneToViewport(scene, MetaCoreBuildPlayerSceneView(), true); + runtimeUiRenderer.Render(); + viewportRenderer.SetRuntimeUiOverlayFrame(runtimeUiRenderer.GetLastFrame(), windowWidth, windowHeight); viewportRenderer.RenderAll(); renderDevice.RenderFrame(); renderDevice.PresentFrame(); window.EndFrame(); } + runtimeUiRenderer.Shutdown(); viewportRenderer.Shutdown(); renderDevice.Shutdown(); window.Shutdown(); diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ec622b..c3eae85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,22 @@ list(PREPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") if(EXISTS "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows/share/imgui/imgui-config.cmake") list(PREPEND CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/vcpkg_installed/x64-windows") endif() +if(EXISTS "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-windows/share/rmlui/RmlUiConfig.cmake") + list(PREPEND CMAKE_PREFIX_PATH "${CMAKE_BINARY_DIR}/vcpkg_installed/x64-windows") +endif() +set(METACORE_QTBASE_PACKAGE_DIR "") +if(CMAKE_TOOLCHAIN_FILE MATCHES "[/\\\\]vcpkg\\.cmake$") + get_filename_component(METACORE_VCPKG_BUILDSYSTEM_DIR "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY) + get_filename_component(METACORE_VCPKG_SCRIPTS_DIR "${METACORE_VCPKG_BUILDSYSTEM_DIR}" DIRECTORY) + get_filename_component(METACORE_VCPKG_ROOT_DIR "${METACORE_VCPKG_SCRIPTS_DIR}" DIRECTORY) + if(NOT VCPKG_TARGET_TRIPLET) + set(VCPKG_TARGET_TRIPLET "x64-windows") + endif() + set(METACORE_QTBASE_PACKAGE_DIR "${METACORE_VCPKG_ROOT_DIR}/packages/qtbase_${VCPKG_TARGET_TRIPLET}") + if(EXISTS "${METACORE_QTBASE_PACKAGE_DIR}/share/Qt6/Qt6Config.cmake") + list(PREPEND CMAKE_PREFIX_PATH "${METACORE_QTBASE_PACKAGE_DIR}") + endif() +endif() @@ -31,6 +47,8 @@ include(MetaCoreFilament) find_package(glm CONFIG REQUIRED) find_package(imgui CONFIG REQUIRED) +find_package(RmlUi CONFIG REQUIRED) +find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets) set(METACORE_COMMON_WARNINGS) if(MSVC) @@ -82,6 +100,12 @@ add_executable(MetaCoreRuntimeConfigTool target_compile_options(MetaCoreRuntimeConfigTool PRIVATE ${METACORE_COMMON_WARNINGS}) +add_executable(MetaCoreBuildPackageTool + Tools/MetaCoreBuildPackageTool/main.cpp +) + +target_compile_options(MetaCoreBuildPackageTool PRIVATE ${METACORE_COMMON_WARNINGS}) + add_executable(MetaCoreTcpSenderTool Tools/MetaCoreTcpSenderTool/main.cpp ) @@ -203,12 +227,14 @@ set(METACORE_SCENE_HEADERS Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScenePackage.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreScene.h Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreSceneSerializer.h + Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreUiRmlCompiler.h ) set(METACORE_SCENE_SOURCES Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp Source/MetaCoreScene/Private/MetaCoreScene.cpp Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp + Source/MetaCoreScene/Private/MetaCoreUiRmlCompiler.cpp ) metacore_generate_reflection( @@ -246,6 +272,7 @@ set(METACORE_RENDER_HEADERS Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderDevice.h + Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRuntimeUiRenderer.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreSceneRenderSync.h Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRenderTypes.h ) @@ -255,6 +282,7 @@ set(METACORE_RENDER_SOURCES Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp Source/MetaCoreRender/Private/MetaCoreRenderDevice.cpp + Source/MetaCoreRender/Private/MetaCoreRuntimeUiRenderer.cpp Source/MetaCoreRender/Private/MetaCoreSceneRenderSync.cpp ) @@ -276,6 +304,8 @@ target_link_libraries(MetaCoreRender MetaCoreScene glm::glm imgui::imgui + PRIVATE + RmlUi::RmlUi ) metacore_use_filament(MetaCoreRender) @@ -408,6 +438,11 @@ target_link_libraries(MetaCoreEditor target_compile_options(MetaCoreEditor PRIVATE ${METACORE_COMMON_WARNINGS}) target_compile_definitions(MetaCoreEditor PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) +target_link_libraries(MetaCoreBuildPackageTool + PRIVATE + MetaCoreEditor +) + add_executable(MetaCoreEditorApp Apps/MetaCoreEditor/main.cpp ) @@ -418,6 +453,35 @@ target_link_libraries(MetaCoreEditorApp ) metacore_stage_ui_blit_material(MetaCoreEditorApp) +add_executable(MetaCoreLauncher + Apps/MetaCoreLauncher/main.cpp +) + +target_link_libraries(MetaCoreLauncher + PRIVATE + MetaCoreFoundation + Qt6::Widgets +) +target_compile_options(MetaCoreLauncher PRIVATE ${METACORE_COMMON_WARNINGS}) +target_compile_definitions(MetaCoreLauncher PRIVATE NOMINMAX) +if(WIN32) + if(EXISTS "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Core.dll") + add_custom_command(TARGET MetaCoreLauncher POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Core.dll" + "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Gui.dll" + "${METACORE_QTBASE_PACKAGE_DIR}/bin/Qt6Widgets.dll" + "$" + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/platforms" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${METACORE_QTBASE_PACKAGE_DIR}/Qt6/plugins/platforms/qwindows.dll" + "$/platforms" + VERBATIM + ) + endif() +endif() + add_executable(MetaCorePlayer @@ -432,8 +496,11 @@ target_link_libraries(MetaCorePlayer MetaCoreRuntimeData MetaCoreScene ) +target_compile_options(MetaCorePlayer PRIVATE ${METACORE_COMMON_WARNINGS}) metacore_stage_ui_blit_material(MetaCorePlayer) +add_dependencies(MetaCoreBuildPackageTool MetaCorePlayer) + if(METACORE_BUILD_TESTS) @@ -449,9 +516,17 @@ if(METACORE_BUILD_TESTS) MetaCoreRuntimeData ) target_compile_options(MetaCoreSmokeTests PRIVATE ${METACORE_COMMON_WARNINGS}) + add_dependencies(MetaCoreSmokeTests MetaCoreRuntimeConfigTool) add_test(NAME MetaCoreSmokeTests COMMAND MetaCoreSmokeTests) + add_test( + NAME MetaCoreRuntimeConfigToolSmoke + COMMAND ${CMAKE_COMMAND} + -DMETACORE_RUNTIME_CONFIG_TOOL=$ + -DMETACORE_RUNTIME_CONFIG_OUTPUT_ROOT=${CMAKE_BINARY_DIR}/RuntimeConfigToolSmoke + -P ${CMAKE_SOURCE_DIR}/tests/MetaCoreRuntimeConfigToolSmoke.cmake + ) # Filament + ImGui 离屏渲染 Demo add_executable(FilamentImGuiDemo diff --git a/SandboxProject/Assets/UI/Hud.mcui.json b/SandboxProject/Assets/UI/Hud.mcui.json new file mode 100644 index 0000000..30b6a45 --- /dev/null +++ b/SandboxProject/Assets/UI/Hud.mcui.json @@ -0,0 +1,126 @@ +{ + "Name": "RuntimeHud", + "ReferenceWidth": 1280, + "ReferenceHeight": 720, + "RootNodeIds": [ + "hud.root" + ], + "Nodes": [ + { + "Id": "hud.root", + "Name": "HUD Root", + "Type": 0, + "ParentId": "", + "Children": [ + "hud.title", + "hud.status", + "hud.button" + ], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [24.0, 24.0, 0.0], + "Size": [360.0, 164.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.05, 0.08, 0.12], + "TextColor": [1.0, 1.0, 1.0], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 16.0, + "Padding": [12.0, 12.0, 0.0], + "HorizontalAlignment": 0, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "", + "Interactable": false + }, + { + "Id": "hud.title", + "Name": "Title", + "Type": 1, + "ParentId": "hud.root", + "Children": [], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [16.0, 14.0, 0.0], + "Size": [320.0, 36.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.08, 0.11, 0.16], + "TextColor": [0.95, 0.98, 1.0], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 24.0, + "Padding": [8.0, 4.0, 0.0], + "HorizontalAlignment": 0, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "MetaCore Runtime", + "Interactable": false + }, + { + "Id": "hud.status", + "Name": "Status Strip", + "Type": 0, + "ParentId": "hud.root", + "Children": [], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [16.0, 62.0, 0.0], + "Size": [220.0, 34.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.12, 0.42, 0.82], + "TextColor": [1.0, 1.0, 1.0], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 16.0, + "Padding": [8.0, 4.0, 0.0], + "HorizontalAlignment": 0, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "", + "Interactable": false + }, + { + "Id": "hud.button", + "Name": "Action Button", + "Type": 3, + "ParentId": "hud.root", + "Children": [], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [16.0, 110.0, 0.0], + "Size": [144.0, 38.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.18, 0.72, 0.36], + "TextColor": [0.02, 0.04, 0.03], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 17.0, + "Padding": [10.0, 5.0, 0.0], + "HorizontalAlignment": 1, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "Run", + "Interactable": true + } + ] +} diff --git a/SandboxProject/Assets/UI/Hud.mcui.json.mcmeta b/SandboxProject/Assets/UI/Hud.mcui.json.mcmeta new file mode 100644 index 0000000..4f0aa21 --- /dev/null +++ b/SandboxProject/Assets/UI/Hud.mcui.json.mcmeta @@ -0,0 +1,8 @@ +{ + "asset_type": "ui_document", + "guid": "821c1714-975f-4284-8e4d-650db0be9f3e", + "importer_id": "UiDocumentImporter", + "package_path": "Assets/UI/Hud.mcui.json", + "source_hash": 18133359267932109919, + "source_path": "Assets/UI/Hud.mcui.json" +} \ No newline at end of file diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 66a2960..aed1c74 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -11,6 +11,7 @@ #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCorePlatform/MetaCoreInput.h" #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" +#include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include @@ -26,10 +27,21 @@ #include "stb_image.h" #include "stb_dxt.h" +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + #include #include #include #include +#include #include #include #include @@ -770,27 +782,34 @@ template return std::nullopt; } -void MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( - MetaCoreMeshRendererComponent& meshRenderer, - const MetaCoreModelAssetDocument& document, - std::size_t materialIndex -) { - if (materialIndex >= document.GeneratedMaterialAssets.size()) { - return; - } +[[nodiscard]] MetaCoreMeshAlphaMode MetaCoreConvertMaterialAlphaMode(MetaCoreMaterialAlphaMode alphaMode) { + return alphaMode == MetaCoreMaterialAlphaMode::Mask + ? MetaCoreMeshAlphaMode::Mask + : (alphaMode == MetaCoreMaterialAlphaMode::Blend + ? MetaCoreMeshAlphaMode::Blend + : MetaCoreMeshAlphaMode::Opaque); +} - const MetaCoreMaterialAssetDocument& materialAsset = document.GeneratedMaterialAssets[materialIndex]; +[[nodiscard]] MetaCoreMaterialAlphaMode MetaCoreConvertMeshAlphaMode(MetaCoreMeshAlphaMode alphaMode) { + return alphaMode == MetaCoreMeshAlphaMode::Mask + ? MetaCoreMaterialAlphaMode::Mask + : (alphaMode == MetaCoreMeshAlphaMode::Blend + ? MetaCoreMaterialAlphaMode::Blend + : MetaCoreMaterialAlphaMode::Opaque); +} + +template +void MetaCoreSyncMaterialAssetPreviewToMeshRenderer( + MetaCoreMeshRendererComponent& meshRenderer, + const MetaCoreMaterialAssetDocument& materialAsset, + TTextureLoader&& loadTexture +) { meshRenderer.BaseColor = materialAsset.BaseColor; meshRenderer.DoubleSided = materialAsset.DoubleSided; meshRenderer.Metallic = materialAsset.Metallic; meshRenderer.Roughness = materialAsset.Roughness; meshRenderer.AlphaCutoff = materialAsset.AlphaCutoff; - meshRenderer.AlphaMode = - materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Mask - ? MetaCoreMeshAlphaMode::Mask - : (materialAsset.AlphaMode == MetaCoreMaterialAlphaMode::Blend - ? MetaCoreMeshAlphaMode::Blend - : MetaCoreMeshAlphaMode::Opaque); + meshRenderer.AlphaMode = MetaCoreConvertMaterialAlphaMode(materialAsset.AlphaMode); meshRenderer.EmissiveColor = materialAsset.EmissiveColor; const auto assignTextureReference = [&](const MetaCoreAssetGuid& textureGuid, MetaCoreAssetGuid& outGuid, std::string& outPath) { @@ -800,15 +819,8 @@ void MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( return; } - const auto textureIterator = std::find_if( - document.GeneratedTextureAssets.begin(), - document.GeneratedTextureAssets.end(), - [&](const MetaCoreTextureAssetDocument& textureAsset) { - return textureAsset.AssetGuid == textureGuid; - } - ); - if (textureIterator != document.GeneratedTextureAssets.end()) { - outPath = textureIterator->SourcePath.generic_string(); + if (const auto textureAsset = loadTexture(textureGuid); textureAsset.has_value()) { + outPath = textureAsset->SourcePath.generic_string(); } }; @@ -819,23 +831,59 @@ void MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( assignTextureReference(materialAsset.AoTexture, meshRenderer.AoTextureGuid, meshRenderer.AoTexturePath); } +void MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( + MetaCoreMeshRendererComponent& meshRenderer, + const MetaCoreModelAssetDocument& document, + std::size_t materialIndex +) { + if (materialIndex >= document.GeneratedMaterialAssets.size()) { + return; + } + + const auto loadGeneratedTexture = [&](const MetaCoreAssetGuid& textureGuid) -> std::optional { + const auto textureIterator = std::find_if( + document.GeneratedTextureAssets.begin(), + document.GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == textureGuid; + } + ); + return textureIterator != document.GeneratedTextureAssets.end() + ? std::optional(*textureIterator) + : std::nullopt; + }; + + MetaCoreSyncMaterialAssetPreviewToMeshRenderer( + meshRenderer, + document.GeneratedMaterialAssets[materialIndex], + loadGeneratedTexture + ); +} + void MetaCoreApplyGeneratedMaterialPreviewToScene( MetaCoreEditorContext& editorContext, const MetaCoreModelAssetDocument& document ) { - std::unordered_map texturePaths; - for (const MetaCoreTextureAssetDocument& textureAsset : document.GeneratedTextureAssets) { - if (textureAsset.AssetGuid.IsValid()) { - texturePaths[textureAsset.AssetGuid] = textureAsset.SourcePath.generic_string(); - } - } + const auto loadGeneratedTexture = [&](const MetaCoreAssetGuid& textureGuid) -> std::optional { + const auto textureIterator = std::find_if( + document.GeneratedTextureAssets.begin(), + document.GeneratedTextureAssets.end(), + [&](const MetaCoreTextureAssetDocument& textureAsset) { + return textureAsset.AssetGuid == textureGuid; + } + ); + return textureIterator != document.GeneratedTextureAssets.end() + ? std::optional(*textureIterator) + : std::nullopt; + }; for (MetaCoreGameObject& sceneObject : editorContext.GetScene().GetGameObjects()) { if (!sceneObject.HasComponent()) { continue; } - for (const MetaCoreAssetGuid& materialGuid : sceneObject.GetComponent().MaterialAssetGuids) { + MetaCoreMeshRendererComponent& meshRenderer = sceneObject.GetComponent(); + for (const MetaCoreAssetGuid& materialGuid : meshRenderer.MaterialAssetGuids) { const auto materialIterator = std::find_if( document.GeneratedMaterialAssets.begin(), document.GeneratedMaterialAssets.end(), @@ -847,43 +895,7 @@ void MetaCoreApplyGeneratedMaterialPreviewToScene( continue; } - sceneObject.GetComponent().BaseColor = materialIterator->BaseColor; - sceneObject.GetComponent().Metallic = materialIterator->Metallic; - sceneObject.GetComponent().Roughness = materialIterator->Roughness; - sceneObject.GetComponent().DoubleSided = materialIterator->DoubleSided; - sceneObject.GetComponent().EmissiveColor = materialIterator->EmissiveColor; - sceneObject.GetComponent().AlphaCutoff = materialIterator->AlphaCutoff; - sceneObject.GetComponent().AlphaMode = - materialIterator->AlphaMode == MetaCoreMaterialAlphaMode::Mask - ? MetaCoreMeshAlphaMode::Mask - : (materialIterator->AlphaMode == MetaCoreMaterialAlphaMode::Blend - ? MetaCoreMeshAlphaMode::Blend - : MetaCoreMeshAlphaMode::Opaque); - sceneObject.GetComponent().BaseColorTextureGuid = materialIterator->BaseColorTexture; - sceneObject.GetComponent().BaseColorTexturePath = - materialIterator->BaseColorTexture.IsValid() && texturePaths.contains(materialIterator->BaseColorTexture) - ? texturePaths[materialIterator->BaseColorTexture] - : std::string{}; - sceneObject.GetComponent().MetallicRoughnessTextureGuid = materialIterator->MetallicRoughnessTexture; - sceneObject.GetComponent().MetallicRoughnessTexturePath = - materialIterator->MetallicRoughnessTexture.IsValid() && texturePaths.contains(materialIterator->MetallicRoughnessTexture) - ? texturePaths[materialIterator->MetallicRoughnessTexture] - : std::string{}; - sceneObject.GetComponent().NormalTextureGuid = materialIterator->NormalTexture; - sceneObject.GetComponent().NormalTexturePath = - materialIterator->NormalTexture.IsValid() && texturePaths.contains(materialIterator->NormalTexture) - ? texturePaths[materialIterator->NormalTexture] - : std::string{}; - sceneObject.GetComponent().EmissiveTextureGuid = materialIterator->EmissiveTexture; - sceneObject.GetComponent().EmissiveTexturePath = - materialIterator->EmissiveTexture.IsValid() && texturePaths.contains(materialIterator->EmissiveTexture) - ? texturePaths[materialIterator->EmissiveTexture] - : std::string{}; - sceneObject.GetComponent().AoTextureGuid = materialIterator->AoTexture; - sceneObject.GetComponent().AoTexturePath = - materialIterator->AoTexture.IsValid() && texturePaths.contains(materialIterator->AoTexture) - ? texturePaths[materialIterator->AoTexture] - : std::string{}; + MetaCoreSyncMaterialAssetPreviewToMeshRenderer(meshRenderer, *materialIterator, loadGeneratedTexture); break; } } @@ -931,15 +943,15 @@ void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAc return false; } - const auto resolvedMaterial = assetEditingService->ResolveGeneratedAsset(materialGuid); - if (!resolvedMaterial.has_value() || resolvedMaterial->GeneratedKind != "material") { + const auto materialAsset = assetEditingService->LoadMaterialAsset(materialGuid); + if (!materialAsset.has_value()) { return false; } - const auto modelDocument = assetEditingService->LoadModelAsset(resolvedMaterial->SourceAsset.Guid); - if (!modelDocument.has_value() || resolvedMaterial->GeneratedIndex >= modelDocument->GeneratedMaterialAssets.size()) { - return false; - } + bool applied = false; + const auto loadTexture = [&](const MetaCoreAssetGuid& textureGuid) -> std::optional { + return assetEditingService->LoadTextureAsset(textureGuid); + }; MetaCoreTrackComponentInspectorEdit(editorContext, "拖拽材质到材质槽", false); MetaCoreForEachSelectedGameObject(editorContext, [&](MetaCoreGameObject& selectedObject) { @@ -950,13 +962,14 @@ void MetaCoreForEachSelectedGameObject(MetaCoreEditorContext& editorContext, TAc selectedObject.GetComponent().MaterialAssetGuids.resize(materialSlotIndex + 1); } selectedObject.GetComponent().MaterialAssetGuids[materialSlotIndex] = materialGuid; - MetaCoreSyncGeneratedMaterialPreviewToMeshRenderer( + MetaCoreSyncMaterialAssetPreviewToMeshRenderer( selectedObject.GetComponent(), - *modelDocument, - resolvedMaterial->GeneratedIndex + *materialAsset, + loadTexture ); + applied = true; }); - return true; + return applied; } [[nodiscard]] bool MetaCoreReapplyMaterialResourceToSelectedObjects( @@ -1203,11 +1216,6 @@ void MetaCoreDrawMeshRendererMaterialSlotSummary( return false; } - const auto resolvedMaterial = assetEditingService->ResolveGeneratedAsset(materialGuid); - if (!resolvedMaterial.has_value() || resolvedMaterial->GeneratedKind != "material") { - return false; - } - auto materialAsset = assetEditingService->LoadMaterialAsset(materialGuid); if (!materialAsset.has_value()) { return false; @@ -1220,21 +1228,18 @@ void MetaCoreDrawMeshRendererMaterialSlotSummary( materialAsset->Roughness = meshRenderer.Roughness; materialAsset->AlphaCutoff = meshRenderer.AlphaCutoff; materialAsset->EmissiveColor = meshRenderer.EmissiveColor; - materialAsset->AlphaMode = - meshRenderer.AlphaMode == MetaCoreMeshAlphaMode::Mask - ? MetaCoreMaterialAlphaMode::Mask - : (meshRenderer.AlphaMode == MetaCoreMeshAlphaMode::Blend - ? MetaCoreMaterialAlphaMode::Blend - : MetaCoreMaterialAlphaMode::Opaque); + materialAsset->AlphaMode = MetaCoreConvertMeshAlphaMode(meshRenderer.AlphaMode); + materialAsset->BaseColorTexture = meshRenderer.BaseColorTextureGuid; + materialAsset->MetallicRoughnessTexture = meshRenderer.MetallicRoughnessTextureGuid; + materialAsset->NormalTexture = meshRenderer.NormalTextureGuid; + materialAsset->EmissiveTexture = meshRenderer.EmissiveTextureGuid; + materialAsset->AoTexture = meshRenderer.AoTextureGuid; if (!assetEditingService->SaveMaterialAsset(materialGuid, *materialAsset)) { return false; } - if (const auto modelDocument = assetEditingService->LoadModelAsset(resolvedMaterial->SourceAsset.Guid); - modelDocument.has_value()) { - MetaCoreApplyGeneratedMaterialPreviewToScene(editorContext, *modelDocument); - } + (void)assetEditingService->ApplyMaterialAssetPreviewToScene(editorContext, materialGuid); return true; } @@ -2214,10 +2219,7 @@ void MetaCoreDrawMeshRendererComponentInspector(MetaCoreEditorContext& editorCon if (modified) { if (assetEditingService->SaveMaterialAsset(materialGuid, materialAsset)) { - if (const auto modelDocument = assetEditingService->LoadModelAsset(resolvedMaterial->SourceAsset.Guid); - modelDocument.has_value()) { - MetaCoreApplyGeneratedMaterialPreviewToScene(editorContext, *modelDocument); - } + (void)assetEditingService->ApplyMaterialAssetPreviewToScene(editorContext, materialGuid); editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Material", "已保存材质资源修改"); } else { editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Material", "材质修改保存失败"); @@ -3440,6 +3442,7 @@ public: return iterator == AssetRecords_.end() ? std::nullopt : std::optional(*iterator); } + [[nodiscard]] bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) override; bool Refresh() override; bool CreateFolder(const std::filesystem::path& relativeDirectory) override; bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) override; @@ -3968,6 +3971,32 @@ void MetaCoreBuiltinAssetDatabaseService::RefreshAssetRecordsFromDisk() { }); } +bool MetaCoreBuiltinAssetDatabaseService::ReimportAsset(const MetaCoreAssetGuid& assetGuid) { + if (!HasProject() || !assetGuid.IsValid() || ModuleRegistry_ == nullptr) { + return false; + } + + const auto importPipeline = ModuleRegistry_->ResolveService(); + if (importPipeline == nullptr || !importPipeline->ReimportAsset(assetGuid)) { + return false; + } + + RefreshScenePathsFromDisk(); + RefreshAssetRecordsFromDisk(); + + MetaCoreAssetRegistry::Get().Clear(); + MetaCoreAssetRegistry::Get().ScanDirectory(Project_.AssetsPath); + + ModuleRegistry_->AccessEventBus().Publish(MetaCoreEditorEvent{ + MetaCoreEditorEventType::AssetDatabaseChanged, + "Asset reimported", + Project_.RootPath, + 0, + true + }); + return true; +} + bool MetaCoreBuiltinAssetDatabaseService::Refresh() { if (!HasProject()) { return false; @@ -4064,6 +4093,542 @@ bool MetaCoreBuiltinAssetDatabaseService::SetStartupScenePath(const std::filesys return project.LibraryPath / "Cooked" / "Windows" / "CookManifest.bin"; } +[[nodiscard]] std::filesystem::path MetaCoreBuildProjectRuntimeDirectoryRelativePath( + const MetaCoreProjectDescriptor& project +) { + return MetaCoreBuildRuntimeDirectoryRelativePath(project.RootPath, project.RuntimePath); +} + +[[nodiscard]] MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument( + const MetaCoreProjectDescriptor& project +) { + const std::filesystem::path runtimeDirectoryRelative = + MetaCoreBuildProjectRuntimeDirectoryRelativePath(project); + MetaCoreRuntimeProjectDocument document = + ::MetaCore::MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative); + document.StartupScenePath = !project.StartupScenePath.empty() + ? project.StartupScenePath + : (project.ScenePaths.empty() ? std::filesystem::path("Scenes") / "Main.mcscene.json" : project.ScenePaths.front()); + return document; +} + +void MetaCoreApplyRuntimeProjectDefaults( + MetaCoreRuntimeProjectDocument& document, + const MetaCoreProjectDescriptor& project +) { + const MetaCoreRuntimeProjectDocument defaults = MetaCoreBuildDefaultRuntimeProjectDocument(project); + if (document.StartupScenePath.empty()) { + document.StartupScenePath = defaults.StartupScenePath; + } + ::MetaCore::MetaCoreApplyRuntimeProjectDefaults( + document, + MetaCoreBuildProjectRuntimeDirectoryRelativePath(project) + ); +} + +[[nodiscard]] std::filesystem::path MetaCoreResolveDefaultPlayerExecutablePath() { +#if defined(_WIN32) + std::array executablePath{}; + const DWORD length = GetModuleFileNameW( + nullptr, + executablePath.data(), + static_cast(executablePath.size()) + ); + if (length > 0 && length < executablePath.size()) { + const std::filesystem::path executableDirectory = std::filesystem::path(executablePath.data()).parent_path(); + return executableDirectory / "MetaCorePlayer.exe"; + } +#endif + return std::filesystem::current_path() / "MetaCorePlayer.exe"; +} + +[[nodiscard]] std::filesystem::path MetaCoreBuildPackageOutputRoot( + const MetaCoreProjectDescriptor& project, + const MetaCoreRuntimeProjectDocument& runtimeProject, + const MetaCoreBuildPlayerPackageRequest& request +) { + std::filesystem::path outputBase = !request.OutputDirectory.empty() + ? request.OutputDirectory + : runtimeProject.OutputDirectory; + if (outputBase.empty()) { + outputBase = std::filesystem::path("Build") / "Windows"; + } + const std::filesystem::path absoluteOutputBase = outputBase.is_absolute() + ? outputBase + : (project.RootPath / outputBase); + const std::string projectName = project.Name.empty() ? "MetaCoreProject" : project.Name; + return absoluteOutputBase / projectName; +} + +[[nodiscard]] bool MetaCoreCopyFileForPackage( + const std::filesystem::path& source, + const std::filesystem::path& target, + MetaCoreBuildPlayerPackageResult& result +) { + if (!std::filesystem::exists(source) || std::filesystem::is_directory(source)) { + return false; + } + + std::error_code errorCode; + std::filesystem::create_directories(target.parent_path(), errorCode); + if (errorCode) { + return false; + } + std::filesystem::copy_file(source, target, std::filesystem::copy_options::overwrite_existing, errorCode); + if (errorCode) { + return false; + } + result.CopiedFiles.push_back(target.lexically_relative(result.OutputRoot)); + return true; +} + +void MetaCoreCopyDirectoryForPackage( + const std::filesystem::path& sourceDirectory, + const std::filesystem::path& targetDirectory, + MetaCoreBuildPlayerPackageResult& result +) { + if (!std::filesystem::exists(sourceDirectory) || !std::filesystem::is_directory(sourceDirectory)) { + return; + } + + for (const auto& entry : std::filesystem::recursive_directory_iterator(sourceDirectory)) { + if (!entry.is_regular_file()) { + continue; + } + const std::filesystem::path relativePath = entry.path().lexically_relative(sourceDirectory); + (void)MetaCoreCopyFileForPackage(entry.path(), targetDirectory / relativePath, result); + } +} + +[[nodiscard]] bool MetaCoreCopyOptionalRelativeFileForPackage( + const MetaCoreProjectDescriptor& project, + const std::filesystem::path& relativePath, + MetaCoreBuildPlayerPackageResult& result, + std::string& error +) { + if (relativePath.empty()) { + return true; + } + if (MetaCoreIsUnsafeRelativePath(relativePath)) { + error = "Unsafe package relative path: " + relativePath.generic_string(); + return false; + } + + const std::filesystem::path sourcePath = project.RootPath / relativePath.lexically_normal(); + if (!std::filesystem::exists(sourcePath)) { + return true; + } + + const std::filesystem::path targetPath = result.OutputRoot / relativePath.lexically_normal(); + if (std::filesystem::exists(targetPath)) { + return true; + } + + if (!MetaCoreCopyFileForPackage(sourcePath, targetPath, result)) { + error = "Failed to copy runtime config file: " + relativePath.generic_string(); + return false; + } + return true; +} + +[[nodiscard]] const std::string* MetaCoreFindRuntimeDataSourceSetting( + const MetaCoreDataSourceDefinition& sourceDefinition, + std::string_view key +) { + const auto iterator = std::find_if( + sourceDefinition.ConnectionSettings.begin(), + sourceDefinition.ConnectionSettings.end(), + [key](const MetaCoreDataSourceSetting& setting) { + return setting.Key == key; + } + ); + return iterator == sourceDefinition.ConnectionSettings.end() ? nullptr : &iterator->Value; +} + +[[nodiscard]] bool MetaCoreCopyRuntimeDataReplayFilesForPackage( + const MetaCoreProjectDescriptor& project, + const MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + MetaCoreBuildPlayerPackageResult& result, + std::string& error +) { + for (const MetaCoreDataSourceDefinition& sourceDefinition : sourcesDocument.Sources) { + if (sourceDefinition.AdapterType != "file_replay") { + continue; + } + + const std::string* replayFilePath = MetaCoreFindRuntimeDataSourceSetting(sourceDefinition, "file_path"); + if (replayFilePath == nullptr || replayFilePath->empty()) { + error = "RuntimeData file_replay source missing file_path: " + sourceDefinition.Id; + return false; + } + + const std::filesystem::path relativeReplayPath(*replayFilePath); + if (MetaCoreIsUnsafeRelativePath(relativeReplayPath)) { + error = "Unsafe RuntimeData file_replay file_path: " + relativeReplayPath.generic_string(); + return false; + } + + const std::filesystem::path normalizedReplayPath = relativeReplayPath.lexically_normal(); + const std::filesystem::path sourcePath = project.RootPath / normalizedReplayPath; + if (!std::filesystem::exists(sourcePath) || std::filesystem::is_directory(sourcePath)) { + error = "RuntimeData replay file not found: " + normalizedReplayPath.generic_string(); + return false; + } + + const std::filesystem::path targetPath = result.OutputRoot / normalizedReplayPath; + if (std::filesystem::exists(targetPath)) { + continue; + } + + if (!MetaCoreCopyFileForPackage(sourcePath, targetPath, result)) { + error = "Failed to copy RuntimeData replay file: " + normalizedReplayPath.generic_string(); + return false; + } + } + return true; +} + +struct MetaCoreCookDependencyQueueContext { + std::deque Queue{}; + std::unordered_set Queued{}; + std::unordered_map ReportIndexes{}; + MetaCoreBuildPlayerPackageResult& Result; +}; + +void MetaCoreSetCookDependencyStatus( + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& assetGuid, + std::string_view status, + std::string_view message = {} +) { + const auto indexIterator = context.ReportIndexes.find(assetGuid); + if (indexIterator == context.ReportIndexes.end() || indexIterator->second >= context.Result.DependencyReport.size()) { + return; + } + + MetaCoreBuildDependencyReportEntry& entry = context.Result.DependencyReport[indexIterator->second]; + entry.Status = std::string(status); + if (!message.empty()) { + entry.Message = std::string(message); + } +} + +void MetaCoreSetCookDependencyResolvedAsset( + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& assetGuid, + const MetaCoreAssetRecord& record +) { + const auto indexIterator = context.ReportIndexes.find(assetGuid); + if (indexIterator == context.ReportIndexes.end() || indexIterator->second >= context.Result.DependencyReport.size()) { + return; + } + + MetaCoreBuildDependencyReportEntry& entry = context.Result.DependencyReport[indexIterator->second]; + entry.AssetType = record.Type; + entry.AssetPath = record.RelativePath; +} + +void MetaCoreSetCookDependencyCookedPath( + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& assetGuid, + const std::filesystem::path& cookedPath +) { + const auto indexIterator = context.ReportIndexes.find(assetGuid); + if (indexIterator == context.ReportIndexes.end() || indexIterator->second >= context.Result.DependencyReport.size()) { + return; + } + + context.Result.DependencyReport[indexIterator->second].CookedPath = cookedPath; +} + +void MetaCoreEnqueueCookDependency( + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& assetGuid, + const MetaCoreAssetGuid& referencedBy, + std::string_view reason +) { + if (!assetGuid.IsValid()) { + return; + } + + if (context.ReportIndexes.find(assetGuid) == context.ReportIndexes.end()) { + MetaCoreBuildDependencyReportEntry entry; + entry.AssetGuid = assetGuid; + entry.ReferencedBy = referencedBy; + entry.Reason = std::string(reason); + entry.Status = "Queued"; + context.ReportIndexes.emplace(assetGuid, context.Result.DependencyReport.size()); + context.Result.DependencyReport.push_back(std::move(entry)); + } + + if (context.Queued.insert(assetGuid).second) { + context.Queue.push_back(assetGuid); + } +} + +void MetaCoreCollectMaterialDependencies( + const MetaCoreMaterialAssetDocument& material, + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& sourceAssetGuid +) { + MetaCoreEnqueueCookDependency(context, material.BaseColorTexture, sourceAssetGuid, "Material.BaseColorTexture"); + MetaCoreEnqueueCookDependency(context, material.NormalTexture, sourceAssetGuid, "Material.NormalTexture"); + MetaCoreEnqueueCookDependency(context, material.MetallicRoughnessTexture, sourceAssetGuid, "Material.MetallicRoughnessTexture"); + MetaCoreEnqueueCookDependency(context, material.AoTexture, sourceAssetGuid, "Material.AoTexture"); + MetaCoreEnqueueCookDependency(context, material.EmissiveTexture, sourceAssetGuid, "Material.EmissiveTexture"); +} + +void MetaCoreCollectMeshRendererDependencies( + const MetaCoreMeshRendererComponent& meshRenderer, + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& sourceAssetGuid +) { + MetaCoreEnqueueCookDependency(context, meshRenderer.MeshAssetGuid, sourceAssetGuid, "MeshRenderer.MeshAssetGuid"); + MetaCoreEnqueueCookDependency(context, meshRenderer.SourceModelAssetGuid, sourceAssetGuid, "MeshRenderer.SourceModelAssetGuid"); + for (const MetaCoreAssetGuid& materialGuid : meshRenderer.MaterialAssetGuids) { + MetaCoreEnqueueCookDependency(context, materialGuid, sourceAssetGuid, "MeshRenderer.MaterialAssetGuids"); + } + MetaCoreEnqueueCookDependency(context, meshRenderer.BaseColorTextureGuid, sourceAssetGuid, "MeshRenderer.BaseColorTextureGuid"); + MetaCoreEnqueueCookDependency(context, meshRenderer.MetallicRoughnessTextureGuid, sourceAssetGuid, "MeshRenderer.MetallicRoughnessTextureGuid"); + MetaCoreEnqueueCookDependency(context, meshRenderer.NormalTextureGuid, sourceAssetGuid, "MeshRenderer.NormalTextureGuid"); + MetaCoreEnqueueCookDependency(context, meshRenderer.EmissiveTextureGuid, sourceAssetGuid, "MeshRenderer.EmissiveTextureGuid"); + MetaCoreEnqueueCookDependency(context, meshRenderer.AoTextureGuid, sourceAssetGuid, "MeshRenderer.AoTextureGuid"); +} + +void MetaCoreCollectGameObjectDependencies( + const std::vector& gameObjects, + MetaCoreCookDependencyQueueContext& context, + const MetaCoreAssetGuid& sourceAssetGuid +) { + for (const MetaCoreGameObjectData& gameObject : gameObjects) { + if (gameObject.MeshRenderer.has_value()) { + MetaCoreCollectMeshRendererDependencies(*gameObject.MeshRenderer, context, sourceAssetGuid); + } + if (gameObject.PrefabInstance.has_value()) { + MetaCoreEnqueueCookDependency(context, gameObject.PrefabInstance->PrefabAssetGuid, sourceAssetGuid, "PrefabInstance.PrefabAssetGuid"); + } + } +} + +[[nodiscard]] std::optional MetaCoreLoadSceneDocumentForCookDependencies( + const MetaCoreProjectDescriptor& project, + const MetaCoreAssetRecord& record, + const MetaCoreTypeRegistry& registry +) { + const std::filesystem::path absolutePath = project.RootPath / record.RelativePath; + if (record.RelativePath.extension() == ".mcscene") { + if (auto document = MetaCoreReadScenePackage(absolutePath); document.has_value()) { + return document; + } + } + return MetaCoreSceneSerializer::LoadSceneFromJson(absolutePath, registry); +} + +[[nodiscard]] std::optional MetaCoreLoadPrefabDocumentForCookDependencies( + const MetaCoreProjectDescriptor& project, + const MetaCoreAssetRecord& record, + const MetaCoreIPackageService& packageService, + const MetaCoreTypeRegistry& registry +) { + const std::filesystem::path absolutePath = project.RootPath / record.RelativePath; + if (auto document = MetaCoreSceneSerializer::LoadPrefabFromJson(absolutePath, registry); document.has_value()) { + return document; + } + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const auto package = packageService.ReadPackage(project.RootPath / relativePackagePath); + return package.has_value() + ? MetaCoreReadTypedPayload(*package, registry, "MetaCorePrefabDocument") + : std::nullopt; +} + +[[nodiscard]] std::optional MetaCoreLoadUiDocumentForCookDependencies( + const MetaCoreProjectDescriptor& project, + const MetaCoreAssetRecord& record, + const MetaCoreIPackageService& packageService, + const MetaCoreTypeRegistry& registry +) { + const std::filesystem::path absolutePath = project.RootPath / record.RelativePath; + if (auto document = MetaCoreSceneSerializer::LoadUiFromJson(absolutePath, registry); document.has_value()) { + return document; + } + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const auto package = packageService.ReadPackage(project.RootPath / relativePackagePath); + return package.has_value() + ? MetaCoreReadTypedPayload(*package, registry, "MetaCoreUiDocument") + : std::nullopt; +} + +[[nodiscard]] std::optional MetaCoreLoadMaterialDocumentForCookDependencies( + const MetaCoreProjectDescriptor& project, + const MetaCoreAssetRecord& record, + const MetaCoreIPackageService& packageService, + const MetaCoreTypeRegistry& registry +) { + const std::filesystem::path absolutePath = project.RootPath / record.RelativePath; + if (MetaCoreIsMaterialPath(absolutePath)) { + if (auto document = MetaCoreSceneSerializer::LoadMaterialFromJson(absolutePath, registry); document.has_value()) { + return document; + } + } + + const std::filesystem::path relativePackagePath = + !record.PackagePath.empty() ? record.PackagePath : record.RelativePath; + const auto package = packageService.ReadPackage(project.RootPath / relativePackagePath); + return package.has_value() + ? MetaCoreReadTypedPayload(*package, registry, "MetaCoreMaterialAssetDocument") + : std::nullopt; +} + +void MetaCoreCollectCookDependenciesForAsset( + const MetaCoreIAssetDatabaseService& assetDatabaseService, + const MetaCoreIPackageService& packageService, + const MetaCoreIReflectionRegistry& reflectionRegistry, + const MetaCoreAssetGuid& assetGuid, + MetaCoreCookDependencyQueueContext& context +) { + const MetaCoreProjectDescriptor& project = assetDatabaseService.GetProjectDescriptor(); + const MetaCoreTypeRegistry& registry = reflectionRegistry.GetTypeRegistry(); + const auto assetRecord = assetDatabaseService.FindAssetByGuid(assetGuid); + if (!assetRecord.has_value()) { + const auto resolvedGeneratedAsset = MetaCoreResolveGeneratedAssetDocument( + assetDatabaseService, + packageService, + reflectionRegistry, + assetGuid + ); + if (!resolvedGeneratedAsset.has_value()) { + return; + } + + MetaCoreEnqueueCookDependency( + context, + resolvedGeneratedAsset->AssetRecord.Guid, + assetGuid, + "GeneratedSubAsset.ParentModel" + ); + if (resolvedGeneratedAsset->GeneratedKind == "material" && + resolvedGeneratedAsset->GeneratedIndex < resolvedGeneratedAsset->Document.GeneratedMaterialAssets.size()) { + MetaCoreCollectMaterialDependencies( + resolvedGeneratedAsset->Document.GeneratedMaterialAssets[resolvedGeneratedAsset->GeneratedIndex], + context, + assetGuid + ); + } + return; + } + + if (assetRecord->Type == "scene") { + if (const auto document = MetaCoreLoadSceneDocumentForCookDependencies(project, *assetRecord, registry); document.has_value()) { + MetaCoreCollectGameObjectDependencies(document->GameObjects, context, assetGuid); + } + } else if (assetRecord->Type == "prefab") { + if (const auto document = MetaCoreLoadPrefabDocumentForCookDependencies(project, *assetRecord, packageService, registry); document.has_value()) { + MetaCoreCollectGameObjectDependencies(document->GameObjects, context, assetGuid); + } + } else if (assetRecord->Type == "material") { + if (const auto document = MetaCoreLoadMaterialDocumentForCookDependencies(project, *assetRecord, packageService, registry); document.has_value()) { + MetaCoreCollectMaterialDependencies(*document, context, assetGuid); + } + } else if (assetRecord->Type == "ui_document") { + if (const auto document = MetaCoreLoadUiDocumentForCookDependencies(project, *assetRecord, packageService, registry); document.has_value()) { + for (const MetaCoreUiNodeDocument& node : document->Nodes) { + MetaCoreEnqueueCookDependency(context, node.Style.ImageAssetGuid, assetGuid, "UI.ImageAssetGuid"); + } + } + } else if (assetRecord->Type == "model") { + const std::filesystem::path relativePackagePath = + !assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath; + const auto package = packageService.ReadPackage(project.RootPath / relativePackagePath); + const auto modelDocument = package.has_value() + ? MetaCoreReadTypedPayload(*package, registry, "MetaCoreModelAssetDocument") + : std::nullopt; + if (modelDocument.has_value()) { + for (const MetaCoreMaterialAssetDocument& material : modelDocument->GeneratedMaterialAssets) { + MetaCoreCollectMaterialDependencies(material, context, assetGuid); + } + } + } +} + +[[nodiscard]] bool MetaCoreCookAssetAndDependencies( + const MetaCoreIAssetDatabaseService& assetDatabaseService, + const MetaCoreIPackageService& packageService, + const MetaCoreIReflectionRegistry& reflectionRegistry, + MetaCoreICookService& cookService, + const MetaCoreAssetGuid& rootAssetGuid, + MetaCoreBuildPlayerPackageResult& result, + std::unordered_set& cookedPathKeys, + std::string& outError +) { + MetaCoreCookDependencyQueueContext context{{}, {}, {}, result}; + std::unordered_set processed; + MetaCoreEnqueueCookDependency(context, rootAssetGuid, MetaCoreAssetGuid{}, "BuildRoot"); + + while (!context.Queue.empty()) { + const MetaCoreAssetGuid assetGuid = context.Queue.front(); + context.Queue.pop_front(); + if (!processed.insert(assetGuid).second) { + continue; + } + + MetaCoreCollectCookDependenciesForAsset( + assetDatabaseService, + packageService, + reflectionRegistry, + assetGuid, + context + ); + + const auto assetRecord = assetDatabaseService.FindAssetByGuid(assetGuid); + if (!assetRecord.has_value()) { + if (const auto resolvedGeneratedAsset = MetaCoreResolveGeneratedAssetDocument( + assetDatabaseService, + packageService, + reflectionRegistry, + assetGuid); resolvedGeneratedAsset.has_value()) { + MetaCoreSetCookDependencyResolvedAsset(context, assetGuid, resolvedGeneratedAsset->AssetRecord); + MetaCoreSetCookDependencyStatus( + context, + assetGuid, + "SkippedGeneratedSubAsset", + "Cook parent model asset instead" + ); + MetaCoreEnqueueCookDependency( + context, + resolvedGeneratedAsset->AssetRecord.Guid, + assetGuid, + "GeneratedSubAsset.ParentModel" + ); + continue; + } + outError = "Asset dependency is not registered: " + assetGuid.ToString(); + MetaCoreSetCookDependencyStatus(context, assetGuid, "Missing", outError); + return false; + } + + MetaCoreSetCookDependencyResolvedAsset(context, assetGuid, *assetRecord); + MetaCoreSetCookDependencyStatus(context, assetGuid, "Cooking"); + if (!cookService.CookAsset(assetGuid)) { + outError = "Failed to cook asset dependency: " + assetGuid.ToString(); + MetaCoreSetCookDependencyStatus(context, assetGuid, "Failed", outError); + return false; + } + + const std::filesystem::path cookedPath = cookService.GetCookedPathForAsset(assetGuid); + MetaCoreSetCookDependencyStatus(context, assetGuid, "Cooked"); + MetaCoreSetCookDependencyCookedPath(context, assetGuid, cookedPath); + if (!cookedPath.empty() && cookedPathKeys.insert(cookedPath.generic_string()).second) { + result.CookedAssets.push_back(cookedPath); + } + } + + return true; +} + class MetaCoreBuiltinCookService final : public MetaCoreICookService { public: [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.CookService"; } @@ -4098,6 +4663,36 @@ private: mutable bool ManifestLoaded_ = false; }; +class MetaCoreBuiltinBuildService final : public MetaCoreIBuildService { +public: + [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.BuildService"; } + + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + AssetDatabaseService_ = moduleRegistry.ResolveService(); + PackageService_ = moduleRegistry.ResolveService(); + CookService_ = moduleRegistry.ResolveService(); + ReflectionRegistry_ = moduleRegistry.ResolveService(); + } + + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + AssetDatabaseService_.reset(); + PackageService_.reset(); + CookService_.reset(); + ReflectionRegistry_.reset(); + } + + [[nodiscard]] MetaCoreBuildPlayerPackageResult BuildPlayerPackage( + const MetaCoreBuildPlayerPackageRequest& request + ) override; + +private: + std::shared_ptr AssetDatabaseService_{}; + std::shared_ptr PackageService_{}; + std::shared_ptr CookService_{}; + std::shared_ptr ReflectionRegistry_{}; +}; + class MetaCoreBuiltinImportPipelineService final : public MetaCoreIImportPipelineService { public: [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.ImportPipeline"; } @@ -4172,6 +4767,11 @@ public: const MetaCoreMaterialAssetDocument& document ) override; + [[nodiscard]] bool ApplyMaterialAssetPreviewToScene( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& materialGuid + ) const override; + [[nodiscard]] std::optional ResolveGeneratedAsset( const MetaCoreAssetGuid& assetGuid ) const override; @@ -4501,7 +5101,9 @@ bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( if (assetRecord.has_value()) { const std::filesystem::path absolutePath = AssetDatabaseService_->GetProjectDescriptor().RootPath / assetRecord->RelativePath; if (MetaCoreIsMaterialPath(absolutePath)) { - if (!MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, document, ReflectionRegistry_->GetTypeRegistry())) { + MetaCoreMaterialAssetDocument savedDocument = document; + savedDocument.AssetGuid = materialGuid; + if (!MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, savedDocument, ReflectionRegistry_->GetTypeRegistry())) { return false; } @@ -4527,13 +5129,17 @@ bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( MetaCoreModelAssetDocument editableDocument = resolved->Document; editableDocument.GeneratedMaterialAssets[resolved->GeneratedIndex] = document; editableDocument.GeneratedMaterialAssets[resolved->GeneratedIndex].AssetGuid = materialGuid; - return MetaCoreWriteImportedGltfAssetDocument( + const bool saved = MetaCoreWriteImportedGltfAssetDocument( *AssetDatabaseService_, *PackageService_, *ReflectionRegistry_, resolved->AssetRecord, editableDocument ); + if (saved) { + ModelCache_[resolved->AssetRecord.Guid] = editableDocument; + } + return saved; } } } @@ -4541,6 +5147,49 @@ bool MetaCoreBuiltinAssetEditingService::SaveMaterialAsset( return false; } +bool MetaCoreBuiltinAssetEditingService::ApplyMaterialAssetPreviewToScene( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& materialGuid +) const { + if (!materialGuid.IsValid()) { + return false; + } + + const auto materialAsset = LoadMaterialAsset(materialGuid); + if (!materialAsset.has_value()) { + return false; + } + + bool applied = false; + const auto loadTexture = [&](const MetaCoreAssetGuid& textureGuid) -> std::optional { + return LoadTextureAsset(textureGuid); + }; + + for (MetaCoreGameObject& sceneObject : editorContext.GetScene().GetGameObjects()) { + if (!sceneObject.HasComponent()) { + continue; + } + + MetaCoreMeshRendererComponent& meshRenderer = sceneObject.GetComponent(); + const auto materialIterator = std::find( + meshRenderer.MaterialAssetGuids.begin(), + meshRenderer.MaterialAssetGuids.end(), + materialGuid + ); + if (materialIterator == meshRenderer.MaterialAssetGuids.end()) { + continue; + } + + MetaCoreSyncMaterialAssetPreviewToMeshRenderer(meshRenderer, *materialAsset, loadTexture); + applied = true; + } + + if (applied) { + editorContext.GetScene().IncrementRevision(); + } + return applied; +} + std::optional MetaCoreBuiltinAssetEditingService::ResolveGeneratedAsset( const MetaCoreAssetGuid& assetGuid ) const { @@ -4625,6 +5274,189 @@ void MetaCoreBuiltinCookService::SaveManifest() const { output.write(reinterpret_cast(bytes->data()), static_cast(bytes->size())); } +MetaCoreBuildPlayerPackageResult MetaCoreBuiltinBuildService::BuildPlayerPackage( + const MetaCoreBuildPlayerPackageRequest& request +) { + MetaCoreBuildPlayerPackageResult result; + if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || CookService_ == nullptr || ReflectionRegistry_ == nullptr) { + result.Error = "BuildService dependencies are not available"; + return result; + } + if (!AssetDatabaseService_->HasProject()) { + result.Error = "No MetaCore project is open"; + return result; + } + + (void)AssetDatabaseService_->Refresh(); + + const MetaCoreProjectDescriptor& project = AssetDatabaseService_->GetProjectDescriptor(); + const MetaCoreTypeRegistry& registry = ReflectionRegistry_->GetTypeRegistry(); + const std::filesystem::path runtimeDirectory = + !project.RuntimePath.empty() ? project.RuntimePath : (project.RootPath / "Runtime"); + auto runtimeProject = MetaCoreReadRuntimeProjectDocument( + runtimeDirectory / "ProjectRuntime.mcruntimecfg", + registry + ).value_or(MetaCoreBuildDefaultRuntimeProjectDocument(project)); + MetaCoreApplyRuntimeProjectDefaults(runtimeProject, project); + const std::vector runtimeProjectPathIssues = + MetaCoreValidateRuntimeProjectPaths(runtimeProject); + if (!runtimeProjectPathIssues.empty()) { + result.Error = "Unsafe Runtime project " + runtimeProjectPathIssues.front().Message; + return result; + } + MetaCoreRuntimeProjectDocument packagedRuntimeProject = runtimeProject; + if (request.UseCookedAssetsInPackage) { + packagedRuntimeProject.UseCookedAssets = true; + } + + result.OutputRoot = MetaCoreBuildPackageOutputRoot(project, runtimeProject, request); + std::error_code errorCode; + std::filesystem::create_directories(result.OutputRoot, errorCode); + if (errorCode) { + result.Error = "Failed to create build output directory: " + result.OutputRoot.generic_string(); + return result; + } + + if (request.CookBeforePackage) { + if (runtimeProject.StartupScenePath.empty()) { + result.Error = "Build Settings does not define a startup scene"; + return result; + } + const auto sceneRecord = AssetDatabaseService_->FindAssetByRelativePath(runtimeProject.StartupScenePath); + if (!sceneRecord.has_value()) { + result.Error = "Startup scene is not registered in AssetDatabase: " + runtimeProject.StartupScenePath.generic_string(); + return result; + } + std::unordered_set cookedPathKeys; + std::string dependencyCookError; + if (!MetaCoreCookAssetAndDependencies( + *AssetDatabaseService_, + *PackageService_, + *ReflectionRegistry_, + *CookService_, + sceneRecord->Guid, + result, + cookedPathKeys, + dependencyCookError)) { + result.Error = dependencyCookError.empty() + ? ("Failed to cook startup scene: " + runtimeProject.StartupScenePath.generic_string()) + : dependencyCookError; + return result; + } + + if (!runtimeProject.StartupUiPath.empty() && std::filesystem::exists(project.RootPath / runtimeProject.StartupUiPath)) { + const auto uiRecord = AssetDatabaseService_->FindAssetByRelativePath(runtimeProject.StartupUiPath); + if (!uiRecord.has_value()) { + result.Error = "Startup UI is not registered in AssetDatabase: " + runtimeProject.StartupUiPath.generic_string(); + return result; + } + if (!MetaCoreCookAssetAndDependencies( + *AssetDatabaseService_, + *PackageService_, + *ReflectionRegistry_, + *CookService_, + uiRecord->Guid, + result, + cookedPathKeys, + dependencyCookError)) { + result.Error = dependencyCookError.empty() + ? ("Failed to cook startup UI: " + runtimeProject.StartupUiPath.generic_string()) + : dependencyCookError; + return result; + } + } + } + + const std::filesystem::path playerExecutable = !request.PlayerExecutablePath.empty() + ? request.PlayerExecutablePath + : MetaCoreResolveDefaultPlayerExecutablePath(); + if (!std::filesystem::exists(playerExecutable)) { + result.Error = "MetaCorePlayer executable was not found: " + playerExecutable.generic_string(); + return result; + } + if (!MetaCoreCopyFileForPackage(playerExecutable, result.OutputRoot / "MetaCorePlayer.exe", result)) { + result.Error = "Failed to copy MetaCorePlayer executable"; + return result; + } + + const std::filesystem::path playerDirectory = playerExecutable.parent_path(); + (void)MetaCoreCopyFileForPackage(playerDirectory / "uiBlit.filamat", result.OutputRoot / "uiBlit.filamat", result); + if (std::filesystem::exists(playerDirectory)) { + for (const auto& entry : std::filesystem::directory_iterator(playerDirectory)) { + if (entry.is_regular_file() && entry.path().extension() == ".dll") { + (void)MetaCoreCopyFileForPackage(entry.path(), result.OutputRoot / entry.path().filename(), result); + } + } + } + + if (!MetaCoreCopyFileForPackage(project.RootPath / "MetaCore.project.json", result.OutputRoot / "MetaCore.project.json", result)) { + result.Error = "Failed to copy MetaCore.project.json"; + return result; + } + + if (request.CopyLooseProjectContent) { + MetaCoreCopyDirectoryForPackage(project.ScenesPath, result.OutputRoot / "Scenes", result); + MetaCoreCopyDirectoryForPackage(project.AssetsPath, result.OutputRoot / "Assets", result); + } + + if (request.CopyRuntimeConfig) { + const std::filesystem::path packageRuntimeDirectoryRelative = + MetaCoreBuildProjectRuntimeDirectoryRelativePath(project); + MetaCoreCopyDirectoryForPackage( + runtimeDirectory, + result.OutputRoot / packageRuntimeDirectoryRelative, + result + ); + std::string runtimeConfigCopyError; + if (!MetaCoreCopyOptionalRelativeFileForPackage( + project, + packagedRuntimeProject.DataSourcesPath, + result, + runtimeConfigCopyError) || + !MetaCoreCopyOptionalRelativeFileForPackage( + project, + packagedRuntimeProject.BindingsPath, + result, + runtimeConfigCopyError)) { + result.Error = runtimeConfigCopyError; + return result; + } + const std::filesystem::path dataSourcesPath = project.RootPath / packagedRuntimeProject.DataSourcesPath.lexically_normal(); + if (std::filesystem::exists(dataSourcesPath)) { + const auto packagedSourcesDocument = MetaCoreReadRuntimeDataSourcesDocument(dataSourcesPath, registry); + if (!packagedSourcesDocument.has_value()) { + result.Error = "RuntimeData sources document is unreadable: " + packagedRuntimeProject.DataSourcesPath.generic_string(); + return result; + } + if (!MetaCoreCopyRuntimeDataReplayFilesForPackage( + project, + *packagedSourcesDocument, + result, + runtimeConfigCopyError)) { + result.Error = runtimeConfigCopyError; + return result; + } + } + if (!MetaCoreWriteRuntimeProjectDocument( + result.OutputRoot / packageRuntimeDirectoryRelative / "ProjectRuntime.mcruntimecfg", + packagedRuntimeProject, + registry)) { + result.Error = "Failed to write packaged ProjectRuntime.mcruntimecfg"; + return result; + } + result.CopiedFiles.push_back(packageRuntimeDirectoryRelative / "ProjectRuntime.mcruntimecfg"); + } + + MetaCoreCopyDirectoryForPackage( + project.RootPath / "Library" / "Cooked", + result.OutputRoot / "Library" / "Cooked", + result + ); + + result.Success = true; + return result; +} + bool MetaCoreBuiltinImportPipelineService::RefreshImports() { if (AssetDatabaseService_ == nullptr || PackageService_ == nullptr || ReflectionRegistry_ == nullptr || !AssetDatabaseService_->HasProject()) { return false; @@ -5243,6 +6075,7 @@ public: ) override; [[nodiscard]] bool ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) override; [[nodiscard]] bool RevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) override; + [[nodiscard]] bool BreakSelectedPrefabInstance(MetaCoreEditorContext& editorContext) override; private: [[nodiscard]] std::optional LoadPrefabPackage(const MetaCoreAssetGuid& prefabAssetGuid) const; @@ -5275,9 +6108,19 @@ public: void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { ReflectionRegistry_ = moduleRegistry.ResolveService(); Descriptors_.clear(); + const MetaCoreTypeRegistry* typeRegistry = ReflectionRegistry_ != nullptr ? &ReflectionRegistry_->GetTypeRegistry() : nullptr; + Descriptors_.push_back(MetaCoreComponentDescriptor{ "Transform", "Transform", + "Transform", + typeRegistry != nullptr ? typeRegistry->FindStruct() : nullptr, + [](MetaCoreGameObject& gameObject) -> void* { + return &gameObject.GetComponent(); + }, + [](const MetaCoreGameObject& gameObject) -> const void* { + return &gameObject.GetComponent(); + }, [](const MetaCoreGameObject&) { return true; }, [](MetaCoreGameObject&) { return false; }, [](MetaCoreGameObject&) { return false; }, @@ -5291,11 +6134,20 @@ public: [](MetaCoreGameObject& gameObject, std::span payload, const MetaCoreTypeRegistry& registry) { return MetaCoreDeserializeComponentValue(payload, gameObject.GetComponent(), registry); }, - nullptr + nullptr, + {} }); Descriptors_.push_back(MetaCoreComponentDescriptor{ "Camera", "Camera", + "Rendering", + typeRegistry != nullptr ? typeRegistry->FindStruct() : nullptr, + [](MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }, + [](const MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }, [](const MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }, [](MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { @@ -5332,11 +6184,20 @@ public: gameObject.AddComponent(component); return true; }, - MetaCoreDrawCameraComponentInspector + MetaCoreDrawCameraComponentInspector, + {} }); Descriptors_.push_back(MetaCoreComponentDescriptor{ "Light", "Light", + "Rendering", + typeRegistry != nullptr ? typeRegistry->FindStruct() : nullptr, + [](MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }, + [](const MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }, [](const MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }, [](MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { @@ -5373,11 +6234,20 @@ public: gameObject.AddComponent(component); return true; }, - MetaCoreDrawLightComponentInspector + MetaCoreDrawLightComponentInspector, + {} }); Descriptors_.push_back(MetaCoreComponentDescriptor{ "MeshRenderer", "Mesh Renderer", + "Rendering", + typeRegistry != nullptr ? typeRegistry->FindStruct() : nullptr, + [](MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }, + [](const MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }, [](const MetaCoreGameObject& gameObject) { return gameObject.HasComponent(); }, [](MetaCoreGameObject& gameObject) { if (gameObject.HasComponent()) { @@ -5414,7 +6284,8 @@ public: gameObject.AddComponent(component); return true; }, - MetaCoreDrawMeshRendererComponentInspector + MetaCoreDrawMeshRendererComponentInspector, + {} }); } @@ -5436,6 +6307,22 @@ public: return iterator == Descriptors_.end() ? nullptr : &(*iterator); } + [[nodiscard]] bool RegisterComponentDescriptor(MetaCoreComponentDescriptor descriptor) override { + if (descriptor.TypeId.empty() || FindDescriptor(descriptor.TypeId) != nullptr) { + return false; + } + + if (descriptor.DisplayName.empty()) { + descriptor.DisplayName = descriptor.TypeId; + } + if (descriptor.Category.empty()) { + descriptor.Category = "Scripts"; + } + + Descriptors_.push_back(std::move(descriptor)); + return true; + } + [[nodiscard]] bool CopyComponent(std::string_view typeId, const MetaCoreGameObject& gameObject) override { if (ReflectionRegistry_ == nullptr) { return false; @@ -5488,8 +6375,152 @@ private: class MetaCoreBuiltinPlayModeService final : public MetaCoreIPlayModeService { public: [[nodiscard]] std::string GetServiceId() const override { return "MetaCore.PlayMode"; } - [[nodiscard]] MetaCorePlayModeState GetState() const override { return MetaCorePlayModeState::Edit; } - [[nodiscard]] bool CanEnterPlayMode() const override { return false; } + void Startup(MetaCoreEditorModuleRegistry& moduleRegistry) override { + ComponentRegistry_ = moduleRegistry.ResolveService(); + } + void Shutdown(MetaCoreEditorModuleRegistry& moduleRegistry) override { + (void)moduleRegistry; + State_ = MetaCorePlayModeState::Edit; + EditStateSnapshot_.reset(); + StartedLifecycleComponents_.clear(); + ComponentRegistry_.reset(); + } + + [[nodiscard]] MetaCorePlayModeState GetState() const override { return State_; } + [[nodiscard]] bool CanEnterPlayMode() const override { return State_ == MetaCorePlayModeState::Edit; } + [[nodiscard]] bool EnterPlayMode(MetaCoreEditorContext& editorContext) override { + if (!CanEnterPlayMode()) { + return false; + } + EditStateSnapshot_ = editorContext.CaptureStateSnapshot(); + StartedLifecycleComponents_.clear(); + ElapsedPlayTimeSeconds_ = 0.0F; + State_ = MetaCorePlayModeState::Playing; + InvokeLifecycleStart(editorContext); + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Entered Play Mode"); + return true; + } + [[nodiscard]] bool ExitPlayMode(MetaCoreEditorContext& editorContext) override { + if (State_ == MetaCorePlayModeState::Edit) { + return false; + } + InvokeLifecycleDestroy(editorContext); + if (EditStateSnapshot_.has_value()) { + editorContext.RestoreStateSnapshot(*EditStateSnapshot_); + } + EditStateSnapshot_.reset(); + StartedLifecycleComponents_.clear(); + ElapsedPlayTimeSeconds_ = 0.0F; + State_ = MetaCorePlayModeState::Edit; + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Exited Play Mode"); + return true; + } + [[nodiscard]] bool PausePlayMode(MetaCoreEditorContext& editorContext) override { + if (State_ != MetaCorePlayModeState::Playing) { + return false; + } + State_ = MetaCorePlayModeState::Paused; + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Paused Play Mode"); + return true; + } + [[nodiscard]] bool ResumePlayMode(MetaCoreEditorContext& editorContext) override { + if (State_ != MetaCorePlayModeState::Paused) { + return false; + } + State_ = MetaCorePlayModeState::Playing; + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "PlayMode", "Resumed Play Mode"); + return true; + } + [[nodiscard]] bool StepPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) override { + if (State_ != MetaCorePlayModeState::Paused) { + return false; + } + const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F); + InvokeLifecycleStart(editorContext); + InvokeLifecycleUpdate(editorContext, sanitizedDeltaSeconds); + ElapsedPlayTimeSeconds_ += sanitizedDeltaSeconds; + return true; + } + [[nodiscard]] float GetElapsedPlayTimeSeconds() const override { return ElapsedPlayTimeSeconds_; } + void TickPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) override { + if (State_ != MetaCorePlayModeState::Playing) { + return; + } + + const float sanitizedDeltaSeconds = std::max(deltaSeconds, 0.0F); + InvokeLifecycleStart(editorContext); + InvokeLifecycleUpdate(editorContext, sanitizedDeltaSeconds); + ElapsedPlayTimeSeconds_ += sanitizedDeltaSeconds; + } + +private: + [[nodiscard]] std::string BuildLifecycleKey(MetaCoreId objectId, std::string_view componentTypeId) const { + return std::to_string(objectId) + ":" + std::string(componentTypeId); + } + + template + void ForEachLifecycleComponent(MetaCoreEditorContext& editorContext, TCallback&& callback) { + if (ComponentRegistry_ == nullptr) { + return; + } + + const std::vector objectIds = editorContext.GetScene().BuildHierarchyPreorder(); + for (const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor : ComponentRegistry_->GetComponentDescriptors()) { + for (MetaCoreId objectId : objectIds) { + MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(objectId); + if (!gameObject || !descriptor.HasComponent || !descriptor.HasComponent(gameObject)) { + continue; + } + callback(descriptor, gameObject); + } + } + } + + void InvokeLifecycleStart(MetaCoreEditorContext& editorContext) { + ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) { + const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId); + if (StartedLifecycleComponents_.contains(key)) { + return; + } + + StartedLifecycleComponents_.insert(key); + if (descriptor.Lifecycle.OnStart) { + descriptor.Lifecycle.OnStart(editorContext, gameObject); + } + }); + } + + void InvokeLifecycleUpdate(MetaCoreEditorContext& editorContext, float deltaSeconds) { + ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) { + if (!descriptor.Lifecycle.OnUpdate) { + return; + } + const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId); + if (!StartedLifecycleComponents_.contains(key)) { + return; + } + descriptor.Lifecycle.OnUpdate(editorContext, gameObject, deltaSeconds); + }); + } + + void InvokeLifecycleDestroy(MetaCoreEditorContext& editorContext) { + ForEachLifecycleComponent(editorContext, [&](const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, MetaCoreGameObject& gameObject) { + if (!descriptor.Lifecycle.OnDestroy) { + return; + } + const std::string key = BuildLifecycleKey(gameObject.GetId(), descriptor.TypeId); + if (!StartedLifecycleComponents_.contains(key)) { + return; + } + descriptor.Lifecycle.OnDestroy(editorContext, gameObject); + }); + } + + MetaCorePlayModeState State_ = MetaCorePlayModeState::Edit; + float ElapsedPlayTimeSeconds_ = 0.0F; + std::optional EditStateSnapshot_{}; + std::unordered_set StartedLifecycleComponents_{}; + std::shared_ptr ComponentRegistry_{}; }; std::optional MetaCoreBuiltinPrefabService::FindPrefabAsset(const MetaCoreAssetGuid& prefabAssetGuid) const { @@ -5850,6 +6881,35 @@ bool MetaCoreBuiltinPrefabService::RevertSelectedPrefabInstance(MetaCoreEditorCo return reverted; } +bool MetaCoreBuiltinPrefabService::BreakSelectedPrefabInstance(MetaCoreEditorContext& editorContext) { + const MetaCoreId activeObjectId = editorContext.GetActiveObjectId(); + if (activeObjectId == 0) { + return false; + } + + const auto instanceRootId = MetaCoreFindPrefabInstanceRootId(editorContext.GetScene(), activeObjectId); + if (!instanceRootId.has_value()) { + return false; + } + + const bool broken = editorContext.ExecuteSnapshotCommand("Break Prefab Instance", [&]() { + bool removedAnyMetadata = false; + for (MetaCoreId objectId : editorContext.GetScene().GetSubtreeObjectIds(*instanceRootId)) { + MetaCoreGameObject gameObject = editorContext.GetScene().FindGameObject(objectId); + if (gameObject && gameObject.HasComponent()) { + gameObject.RemoveComponent(); + removedAnyMetadata = true; + } + } + return removedAnyMetadata; + }); + + if (broken) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Prefab", "Break Prefab Instance"); + } + return broken; +} + bool MetaCoreBuiltinClipboardService::DuplicateSelection(MetaCoreEditorContext& editorContext) { const std::vector selectedIds = editorContext.GetSelectedObjectIds(); if (selectedIds.empty()) { @@ -6226,6 +7286,7 @@ public: moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); + moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); moduleRegistry.RegisterService(std::make_shared()); diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index b21f7de..8212abf 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -1,5 +1,6 @@ #include "MetaCoreBuiltinEditorModule.h" #include +#include #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" @@ -17,18 +18,22 @@ #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" #include +#include #include #include #include #include +#include #include #include #include #include #include +#include #include #include +#include #include #include #include @@ -46,6 +51,28 @@ namespace { return std::filesystem::path(sourcePath.string() + ".mcmeta"); } +void MetaCoreRevealInExplorer(const std::filesystem::path& absolutePath) { +#if defined(_WIN32) + const std::filesystem::path normalizedPath = absolutePath.lexically_normal(); + if (std::filesystem::is_directory(normalizedPath)) { + ShellExecuteW( + nullptr, + L"open", + normalizedPath.wstring().c_str(), + nullptr, + nullptr, + SW_SHOWNORMAL + ); + return; + } + + const std::wstring parameters = L"/select,\"" + normalizedPath.wstring() + L"\""; + ShellExecuteW(nullptr, L"open", L"explorer.exe", parameters.c_str(), nullptr, SW_SHOWNORMAL); +#else + (void)absolutePath; +#endif +} + void MetaCoreLaunchPlayer(MetaCoreEditorContext& editorContext, MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIScenePersistenceService& scenePersistenceService); void MetaCorePollPlayerProcess(MetaCoreEditorContext& editorContext); @@ -295,23 +322,13 @@ void DrawMaterialTextureSlotSummary( } [[nodiscard]] std::optional MetaCoreBuildUiDocumentSummary( - const MetaCorePackageDocument& package, - const MetaCoreTypeRegistry& registry + const MetaCoreUiDocument& document ) { - const auto document = MetaCoreReadTypedPackagePayload( - package, - registry, - "MetaCoreUiDocument" - ); - if (!document.has_value()) { - return std::nullopt; - } - MetaCoreUiDocumentSummary summary; - summary.Name = document->Name; - summary.RootCount = document->RootNodeIds.size(); - summary.NodeCount = document->Nodes.size(); - for (const MetaCoreUiNodeDocument& node : document->Nodes) { + summary.Name = document.Name; + summary.RootCount = document.RootNodeIds.size(); + summary.NodeCount = document.Nodes.size(); + for (const MetaCoreUiNodeDocument& node : document.Nodes) { switch (node.Type) { case MetaCoreUiNodeType::Text: ++summary.TextCount; @@ -331,6 +348,22 @@ void DrawMaterialTextureSlotSummary( return summary; } +[[nodiscard]] std::optional MetaCoreBuildUiDocumentSummary( + const MetaCorePackageDocument& package, + const MetaCoreTypeRegistry& registry +) { + const auto document = MetaCoreReadTypedPackagePayload( + package, + registry, + "MetaCoreUiDocument" + ); + if (!document.has_value()) { + return std::nullopt; + } + + return MetaCoreBuildUiDocumentSummary(*document); +} + struct MetaCoreMaterialTextureReference { MetaCoreAssetGuid MaterialGuid{}; std::string MaterialName{}; @@ -496,6 +529,11 @@ void MetaCoreApplyMaterialPreviewToScene( return prefabService != nullptr && prefabService->RevertSelectedPrefabInstance(editorContext); } +[[nodiscard]] bool MetaCoreBreakSelectedPrefabInstance(MetaCoreEditorContext& editorContext) { + const auto prefabService = editorContext.GetModuleRegistry().ResolveService(); + return prefabService != nullptr && prefabService->BreakSelectedPrefabInstance(editorContext); +} + void MetaCoreHandleUndo(MetaCoreEditorContext& editorContext) { if (editorContext.UndoCommand()) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Editor", "已撤销操作"); @@ -600,6 +638,33 @@ void MetaCoreMoveProjectPath( } } +void MetaCoreReimportProjectAsset(MetaCoreEditorContext& editorContext, const MetaCoreAssetGuid& assetGuid) { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || !assetGuid.IsValid()) { + return; + } + + const auto assetRecord = assetDatabaseService->FindAssetByGuid(assetGuid); + if (!assetRecord.has_value()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Assets", "Reimport failed: asset not found"); + return; + } + + if (assetDatabaseService->ReimportAsset(assetGuid)) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Assets", + "Reimported asset: " + assetRecord->RelativePath.generic_string() + ); + } else { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Assets", + "Reimport failed: " + assetRecord->RelativePath.generic_string() + ); + } +} + void MetaCoreEnsureProjectStartupScene(MetaCoreEditorContext& editorContext) { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); @@ -729,6 +794,8 @@ std::unordered_map& MetaCoreGetInspectorEd return snapshots; } +void MetaCoreTrackInspectorEdit(MetaCoreEditorContext& editorContext, const char* commandLabel, bool allowMerge); + template void MetaCoreForEachSelectedObject(MetaCoreEditorContext& editorContext, TAccessor&& accessor) { for (const MetaCoreId selectedId : editorContext.GetSelectedObjectIds()) { @@ -793,6 +860,541 @@ void MetaCoreApplyValueToSelectedObjects(MetaCoreEditorContext& editorContext, T }); } +[[nodiscard]] const char* MetaCoreGetFieldInspectorLabel(const MetaCoreFieldDescriptor& field) { + return field.Editor.DisplayName.empty() ? field.Name.c_str() : field.Editor.DisplayName.c_str(); +} + +template +[[nodiscard]] std::optional MetaCoreReadReflectedFieldValue( + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field, + const MetaCoreGameObject& gameObject +) { + if (!descriptor.ConstComponent || !field.ConstValue || !descriptor.HasComponent(gameObject)) { + return std::nullopt; + } + + const void* component = descriptor.ConstComponent(gameObject); + if (component == nullptr) { + return std::nullopt; + } + + const void* value = field.ConstValue(component); + if (value == nullptr) { + return std::nullopt; + } + + return *static_cast(value); +} + +template +[[nodiscard]] std::optional MetaCoreGetSharedReflectedFieldValue( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field, + TComparer&& comparer +) { + return MetaCoreGetSharedSelectedValue( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> std::optional { + return MetaCoreReadReflectedFieldValue(descriptor, field, selectedObject); + }, + std::forward(comparer) + ); +} + +template +void MetaCoreApplyReflectedFieldValueToSelectedObjects( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field, + const TValue& value +) { + MetaCoreApplyValueToSelectedObjects( + editorContext, + [&](MetaCoreGameObject& selectedObject) -> TValue* { + if (!descriptor.MutableComponent || !field.MutableValue || !descriptor.HasComponent(selectedObject)) { + return nullptr; + } + + void* component = descriptor.MutableComponent(selectedObject); + if (component == nullptr) { + return nullptr; + } + + return static_cast(field.MutableValue(component)); + }, + value + ); +} + +template +void MetaCoreDrawReflectedScalarField( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field, + ImGuiDataType dataType, + TComparer&& comparer +) { + const auto sharedValue = MetaCoreGetSharedReflectedFieldValue(editorContext, descriptor, field, std::forward(comparer)); + TValue value = sharedValue.value_or(MetaCoreReadReflectedFieldValue(descriptor, field, gameObject).value_or(TValue{})); + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("%s: Mixed", MetaCoreGetFieldInspectorLabel(field)); + } + + const float speed = field.Editor.Step.has_value() ? static_cast(*field.Editor.Step) : 0.1F; + const TValue minValue = field.Editor.Min.has_value() ? static_cast(*field.Editor.Min) : TValue{}; + const TValue maxValue = field.Editor.Max.has_value() ? static_cast(*field.Editor.Max) : TValue{}; + const void* minPointer = field.Editor.Min.has_value() ? &minValue : nullptr; + const void* maxPointer = field.Editor.Max.has_value() ? &maxValue : nullptr; + + if (field.Editor.ReadOnly) { + ImGui::BeginDisabled(); + } + ImGui::DragScalar(MetaCoreGetFieldInspectorLabel(field), dataType, &value, speed, minPointer, maxPointer); + MetaCoreTrackInspectorEdit(editorContext, "Modify reflected component property", true); + if (field.Editor.ReadOnly) { + ImGui::EndDisabled(); + } + + if (!field.Editor.ReadOnly && ImGui::IsItemEdited()) { + MetaCoreApplyReflectedFieldValueToSelectedObjects(editorContext, descriptor, field, value); + } +} + +void MetaCoreDrawReflectedBoolField( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field +) { + const auto sharedValue = MetaCoreGetSharedReflectedFieldValue( + editorContext, + descriptor, + field, + [](bool lhs, bool rhs) { return lhs == rhs; } + ); + bool value = sharedValue.value_or(MetaCoreReadReflectedFieldValue(descriptor, field, gameObject).value_or(false)); + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("%s: Mixed", MetaCoreGetFieldInspectorLabel(field)); + } + + if (field.Editor.ReadOnly) { + ImGui::BeginDisabled(); + } + ImGui::Checkbox(MetaCoreGetFieldInspectorLabel(field), &value); + MetaCoreTrackInspectorEdit(editorContext, "Modify reflected component property", false); + if (field.Editor.ReadOnly) { + ImGui::EndDisabled(); + } + + if (!field.Editor.ReadOnly && ImGui::IsItemEdited()) { + MetaCoreApplyReflectedFieldValueToSelectedObjects(editorContext, descriptor, field, value); + } +} + +void MetaCoreDrawReflectedVec3Field( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field +) { + const auto sharedValue = MetaCoreGetSharedReflectedFieldValue( + editorContext, + descriptor, + field, + [](const glm::vec3& lhs, const glm::vec3& rhs) { return MetaCoreNearlyEqualVec3(lhs, rhs); } + ); + glm::vec3 value = sharedValue.value_or(MetaCoreReadReflectedFieldValue(descriptor, field, gameObject).value_or(glm::vec3(0.0F))); + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("%s: Mixed", MetaCoreGetFieldInspectorLabel(field)); + } + + const float speed = field.Editor.Step.has_value() ? static_cast(*field.Editor.Step) : 0.05F; + const float minValue = field.Editor.Min.has_value() ? static_cast(*field.Editor.Min) : 0.0F; + const float maxValue = field.Editor.Max.has_value() ? static_cast(*field.Editor.Max) : 0.0F; + + if (field.Editor.ReadOnly) { + ImGui::BeginDisabled(); + } + ImGui::DragFloat3( + MetaCoreGetFieldInspectorLabel(field), + &value.x, + speed, + field.Editor.Min.has_value() ? minValue : 0.0F, + field.Editor.Max.has_value() ? maxValue : 0.0F + ); + MetaCoreTrackInspectorEdit(editorContext, "Modify reflected component property", true); + if (field.Editor.ReadOnly) { + ImGui::EndDisabled(); + } + + if (!field.Editor.ReadOnly && ImGui::IsItemEdited()) { + MetaCoreApplyReflectedFieldValueToSelectedObjects(editorContext, descriptor, field, value); + } +} + +void MetaCoreDrawReflectedStringField( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field +) { + const auto sharedValue = MetaCoreGetSharedReflectedFieldValue( + editorContext, + descriptor, + field, + [](const std::string& lhs, const std::string& rhs) { return lhs == rhs; } + ); + std::string value = sharedValue.value_or(MetaCoreReadReflectedFieldValue(descriptor, field, gameObject).value_or(std::string{})); + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("%s: Mixed", MetaCoreGetFieldInspectorLabel(field)); + value.clear(); + } + + std::array buffer{}; + std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str()); + + if (field.Editor.ReadOnly) { + ImGui::BeginDisabled(); + } + ImGui::InputText(MetaCoreGetFieldInspectorLabel(field), buffer.data(), buffer.size()); + MetaCoreTrackInspectorEdit(editorContext, "Modify reflected component property", true); + if (field.Editor.ReadOnly) { + ImGui::EndDisabled(); + } + + if (!field.Editor.ReadOnly && ImGui::IsItemEdited()) { + MetaCoreApplyReflectedFieldValueToSelectedObjects(editorContext, descriptor, field, buffer.data()); + } +} + +void MetaCoreDrawReflectedPathField( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field +) { + const auto sharedValue = MetaCoreGetSharedReflectedFieldValue( + editorContext, + descriptor, + field, + [](const std::filesystem::path& lhs, const std::filesystem::path& rhs) { return lhs == rhs; } + ); + const std::filesystem::path pathValue = + sharedValue.value_or(MetaCoreReadReflectedFieldValue(descriptor, field, gameObject).value_or(std::filesystem::path{})); + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("%s: Mixed", MetaCoreGetFieldInspectorLabel(field)); + } + + std::array buffer{}; + std::snprintf(buffer.data(), buffer.size(), "%s", pathValue.generic_string().c_str()); + + if (field.Editor.ReadOnly) { + ImGui::BeginDisabled(); + } + ImGui::InputText(MetaCoreGetFieldInspectorLabel(field), buffer.data(), buffer.size()); + MetaCoreTrackInspectorEdit(editorContext, "Modify reflected component property", true); + if (field.Editor.ReadOnly) { + ImGui::EndDisabled(); + } + + if (!field.Editor.ReadOnly && ImGui::IsItemEdited()) { + MetaCoreApplyReflectedFieldValueToSelectedObjects( + editorContext, + descriptor, + field, + std::filesystem::path(buffer.data()) + ); + } +} + +[[nodiscard]] std::optional MetaCoreReadReflectedEnumIntegralValue( + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field, + const MetaCoreGameObject& gameObject +) { + if (!descriptor.ConstComponent || !field.ConstValue || !descriptor.HasComponent(gameObject)) { + return std::nullopt; + } + + const void* component = descriptor.ConstComponent(gameObject); + const void* value = component != nullptr ? field.ConstValue(component) : nullptr; + if (value == nullptr) { + return std::nullopt; + } + + switch (field.Size) { + case sizeof(std::int8_t): { + std::int8_t typedValue = 0; + std::memcpy(&typedValue, value, sizeof(typedValue)); + return typedValue; + } + case sizeof(std::int16_t): { + std::int16_t typedValue = 0; + std::memcpy(&typedValue, value, sizeof(typedValue)); + return typedValue; + } + case sizeof(std::int32_t): { + std::int32_t typedValue = 0; + std::memcpy(&typedValue, value, sizeof(typedValue)); + return typedValue; + } + case sizeof(std::int64_t): { + std::int64_t typedValue = 0; + std::memcpy(&typedValue, value, sizeof(typedValue)); + return typedValue; + } + default: + return std::nullopt; + } +} + +void MetaCoreWriteReflectedEnumIntegralValue( + const MetaCoreFieldDescriptor& field, + void* value, + std::int64_t enumValue +) { + if (value == nullptr) { + return; + } + + switch (field.Size) { + case sizeof(std::int8_t): { + const auto typedValue = static_cast(enumValue); + std::memcpy(value, &typedValue, sizeof(typedValue)); + break; + } + case sizeof(std::int16_t): { + const auto typedValue = static_cast(enumValue); + std::memcpy(value, &typedValue, sizeof(typedValue)); + break; + } + case sizeof(std::int32_t): { + const auto typedValue = static_cast(enumValue); + std::memcpy(value, &typedValue, sizeof(typedValue)); + break; + } + case sizeof(std::int64_t): { + const auto typedValue = static_cast(enumValue); + std::memcpy(value, &typedValue, sizeof(typedValue)); + break; + } + default: + break; + } +} + +void MetaCoreApplyReflectedEnumValueToSelectedObjects( + MetaCoreEditorContext& editorContext, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field, + std::int64_t enumValue +) { + MetaCoreForEachSelectedObject(editorContext, [&](MetaCoreGameObject& selectedObject) { + if (!descriptor.MutableComponent || !field.MutableValue || !descriptor.HasComponent(selectedObject)) { + return; + } + + void* component = descriptor.MutableComponent(selectedObject); + void* fieldValue = component != nullptr ? field.MutableValue(component) : nullptr; + MetaCoreWriteReflectedEnumIntegralValue(field, fieldValue, enumValue); + }); +} + +void MetaCoreDrawReflectedEnumField( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field +) { + const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); + const MetaCoreEnumDescriptor* enumDescriptor = + reflectionRegistry != nullptr ? reflectionRegistry->GetTypeRegistry().FindEnumByRuntimeType(field.RuntimeType) : nullptr; + if (enumDescriptor == nullptr || enumDescriptor->Values.empty()) { + ImGui::TextDisabled("%s: <%s>", MetaCoreGetFieldInspectorLabel(field), field.TypeName.c_str()); + return; + } + + const auto sharedValue = MetaCoreGetSharedSelectedValue( + editorContext, + [&](MetaCoreGameObject& selectedObject) { + return MetaCoreReadReflectedEnumIntegralValue(descriptor, field, selectedObject); + }, + [](std::int64_t lhs, std::int64_t rhs) { return lhs == rhs; } + ); + std::int64_t value = sharedValue.value_or( + MetaCoreReadReflectedEnumIntegralValue(descriptor, field, gameObject).value_or(enumDescriptor->Values.front().Value) + ); + const bool multiEdit = editorContext.GetSelectedObjectIds().size() > 1; + if (multiEdit && !sharedValue.has_value()) { + ImGui::TextDisabled("%s: Mixed", MetaCoreGetFieldInspectorLabel(field)); + } + + const auto currentValueIterator = std::find_if( + enumDescriptor->Values.begin(), + enumDescriptor->Values.end(), + [&](const MetaCoreEnumValueDescriptor& enumValue) { + return enumValue.Value == value; + } + ); + const std::string currentLabel = + currentValueIterator != enumDescriptor->Values.end() + ? currentValueIterator->Name + : std::to_string(value); + + if (field.Editor.ReadOnly) { + ImGui::BeginDisabled(); + } + const bool comboOpen = ImGui::BeginCombo(MetaCoreGetFieldInspectorLabel(field), currentLabel.c_str()); + MetaCoreTrackInspectorEdit(editorContext, "Modify reflected component property", false); + if (comboOpen) { + for (const MetaCoreEnumValueDescriptor& enumValue : enumDescriptor->Values) { + const bool selected = enumValue.Value == value; + if (ImGui::Selectable(enumValue.Name.c_str(), selected)) { + value = enumValue.Value; + MetaCoreApplyReflectedEnumValueToSelectedObjects(editorContext, descriptor, field, value); + } + if (selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + if (field.Editor.ReadOnly) { + ImGui::EndDisabled(); + } +} + +void MetaCoreDrawReflectedComponentField( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor, + const MetaCoreFieldDescriptor& field +) { + if (field.Editor.Hidden || !field.MutableValue || !field.ConstValue) { + return; + } + + ImGui::PushID(field.Name.c_str()); + switch (field.ValueKind) { + case MetaCoreFieldValueKind::Bool: + MetaCoreDrawReflectedBoolField(editorContext, gameObject, descriptor, field); + break; + case MetaCoreFieldValueKind::FloatingPoint: + if (field.RuntimeType == std::type_index(typeid(float))) { + MetaCoreDrawReflectedScalarField( + editorContext, + gameObject, + descriptor, + field, + ImGuiDataType_Float, + [](float lhs, float rhs) { return MetaCoreNearlyEqual(lhs, rhs); } + ); + } else if (field.RuntimeType == std::type_index(typeid(double))) { + MetaCoreDrawReflectedScalarField( + editorContext, + gameObject, + descriptor, + field, + ImGuiDataType_Double, + [](double lhs, double rhs) { return std::abs(lhs - rhs) <= 0.0001; } + ); + } + break; + case MetaCoreFieldValueKind::SignedInteger: + if (field.RuntimeType == std::type_index(typeid(std::int32_t))) { + MetaCoreDrawReflectedScalarField( + editorContext, + gameObject, + descriptor, + field, + ImGuiDataType_S32, + [](std::int32_t lhs, std::int32_t rhs) { return lhs == rhs; } + ); + } else if (field.RuntimeType == std::type_index(typeid(std::int64_t))) { + MetaCoreDrawReflectedScalarField( + editorContext, + gameObject, + descriptor, + field, + ImGuiDataType_S64, + [](std::int64_t lhs, std::int64_t rhs) { return lhs == rhs; } + ); + } + break; + case MetaCoreFieldValueKind::UnsignedInteger: + if (field.RuntimeType == std::type_index(typeid(std::uint32_t))) { + MetaCoreDrawReflectedScalarField( + editorContext, + gameObject, + descriptor, + field, + ImGuiDataType_U32, + [](std::uint32_t lhs, std::uint32_t rhs) { return lhs == rhs; } + ); + } else if (field.RuntimeType == std::type_index(typeid(std::uint64_t))) { + MetaCoreDrawReflectedScalarField( + editorContext, + gameObject, + descriptor, + field, + ImGuiDataType_U64, + [](std::uint64_t lhs, std::uint64_t rhs) { return lhs == rhs; } + ); + } + break; + case MetaCoreFieldValueKind::String: + MetaCoreDrawReflectedStringField(editorContext, gameObject, descriptor, field); + break; + case MetaCoreFieldValueKind::Path: + MetaCoreDrawReflectedPathField(editorContext, gameObject, descriptor, field); + break; + case MetaCoreFieldValueKind::Vec3: + MetaCoreDrawReflectedVec3Field(editorContext, gameObject, descriptor, field); + break; + case MetaCoreFieldValueKind::Enum: + MetaCoreDrawReflectedEnumField(editorContext, gameObject, descriptor, field); + break; + default: + ImGui::TextDisabled("%s: <%s>", MetaCoreGetFieldInspectorLabel(field), field.TypeName.c_str()); + break; + } + + if (!field.Editor.Tooltip.empty() && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) { + ImGui::SetTooltip("%s", field.Editor.Tooltip.c_str()); + } + ImGui::PopID(); +} + +void MetaCoreDrawReflectedComponentInspector( + MetaCoreEditorContext& editorContext, + MetaCoreGameObject& gameObject, + const MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor& descriptor +) { + if (descriptor.ReflectedType == nullptr || !descriptor.MutableComponent || !descriptor.ConstComponent) { + ImGui::TextDisabled("No reflected inspector available."); + return; + } + + std::string currentGroup; + for (const MetaCoreFieldDescriptor& field : descriptor.ReflectedType->Fields) { + if (!field.Editor.Group.empty() && field.Editor.Group != currentGroup) { + currentGroup = field.Editor.Group; + ImGui::Spacing(); + ImGui::TextDisabled("%s", currentGroup.c_str()); + ImGui::Separator(); + } + MetaCoreDrawReflectedComponentField(editorContext, gameObject, descriptor, field); + } +} + template [[nodiscard]] std::optional MetaCoreReadTypedPackagePayload( const MetaCorePackageDocument& package, @@ -938,13 +1540,13 @@ template ) { const std::filesystem::path baseDirectory = relativeDirectory.empty() || relativeDirectory.string().rfind("Assets", 0) != 0 - ? std::filesystem::path("Assets") / "Ui" + ? std::filesystem::path("Assets") / "UI" : relativeDirectory; - std::filesystem::path candidate = baseDirectory / "UiDocument.mcui"; + std::filesystem::path candidate = baseDirectory / "UiDocument.mcui.json"; std::size_t suffix = 1; while (std::filesystem::exists(projectDescriptor.RootPath / candidate)) { - candidate = baseDirectory / ("UiDocument" + std::to_string(suffix++) + ".mcui"); + candidate = baseDirectory / ("UiDocument" + std::to_string(suffix++) + ".mcui.json"); } return candidate.lexically_normal(); } @@ -956,38 +1558,32 @@ template MetaCoreAssetGuid& createdAssetGuid ) { const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); - const auto packageService = editorContext.GetModuleRegistry().ResolveService(); const auto reflectionRegistry = editorContext.GetModuleRegistry().ResolveService(); - if (assetDatabaseService == nullptr || packageService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { + if (assetDatabaseService == nullptr || reflectionRegistry == nullptr || !assetDatabaseService->HasProject()) { return false; } createdRelativePath = MetaCoreBuildUniqueUiDocumentPath(assetDatabaseService->GetProjectDescriptor(), relativeDirectory); (void)std::filesystem::create_directories((assetDatabaseService->GetProjectDescriptor().RootPath / createdRelativePath).parent_path()); - createdAssetGuid = MetaCoreAssetGuid::Generate(); MetaCoreUiDocument document; - document.Name = createdRelativePath.stem().string(); + document.Name = createdRelativePath.stem().stem().string(); - const auto payload = MetaCoreSerializeToBytes(document, reflectionRegistry->GetTypeRegistry()); - if (!payload.has_value()) { + const std::filesystem::path absolutePath = assetDatabaseService->GetProjectDescriptor().RootPath / createdRelativePath; + if (!MetaCoreSceneSerializer::SaveUiToJson(absolutePath, document, reflectionRegistry->GetTypeRegistry())) { return false; } - MetaCorePackageDocument package = MetaCoreBuildTypedPackage( - MetaCorePackageType::Asset, - createdAssetGuid, - createdRelativePath.filename().string(), - "MetaCoreUiDocument", - 0, - *payload - ); - - if (!packageService->WritePackage(assetDatabaseService->GetProjectDescriptor().RootPath / createdRelativePath, std::move(package))) { + if (!assetDatabaseService->Refresh()) { return false; } - return assetDatabaseService->Refresh(); + const auto createdRecord = assetDatabaseService->FindAssetByRelativePath(createdRelativePath); + if (!createdRecord.has_value()) { + return false; + } + createdAssetGuid = createdRecord->Guid; + return true; } [[nodiscard]] std::string MetaCoreMakeUniqueUiNodeId(const MetaCoreUiDocument& document, std::string_view prefix) { @@ -1917,6 +2513,9 @@ public: if (ImGui::MenuItem("还原当前 Prefab", nullptr, false, editorContext.GetActiveObjectId() != 0)) { (void)MetaCoreRevertSelectedPrefabInstance(editorContext); } + if (ImGui::MenuItem("Break Prefab Instance", nullptr, false, editorContext.GetActiveObjectId() != 0)) { + (void)MetaCoreBreakSelectedPrefabInstance(editorContext); + } ImGui::Separator(); if (ImGui::MenuItem("复制", "Ctrl+D", false, !editorContext.GetSelectedObjectIds().empty())) { (void)MetaCoreDuplicateSelection(editorContext); @@ -2529,10 +3128,7 @@ void DrawGeneratedMaterialDetails( if (modified) { if (assetEditingService->SaveMaterialAsset(materialAsset.AssetGuid, materialAsset)) { - if (const auto modelDocument = assetEditingService->LoadModelAsset(resolvedMaterial->SourceAsset.Guid); - modelDocument.has_value()) { - MetaCoreApplyMaterialPreviewToScene(editorContext, *modelDocument); - } + (void)assetEditingService->ApplyMaterialAssetPreviewToScene(editorContext, materialAsset.AssetGuid); ImGui::TextColored(ImVec4(0.32F, 0.85F, 0.42F, 1.0F), "已保存材质资源修改"); } else { ImGui::TextColored(ImVec4(0.92F, 0.32F, 0.28F, 1.0F), "材质资源保存失败"); @@ -2937,7 +3533,6 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid); if (!assetRecord.has_value()) return; - const auto importPipelineService = editorContext.GetModuleRegistry().ResolveService(); const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); const auto importedDocument = assetEditingService != nullptr ? assetEditingService->LoadModelAsset(selectedAsset.Guid) : std::nullopt; @@ -3192,8 +3787,7 @@ void DrawModelAssetDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD ImGui::Separator(); if (ImGui::Button("Reimport")) { - if (importPipelineService != nullptr && importPipelineService->ReimportAsset(selectedAsset.Guid)) { - (void)assetDatabaseService.Refresh(); + if (assetDatabaseService.ReimportAsset(selectedAsset.Guid)) { editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Import", "模型重新导入完成"); } else { editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Import", "模型重新导入失败"); @@ -3469,9 +4063,20 @@ void DrawUiDocumentDetails(MetaCoreEditorContext& editorContext, MetaCoreIAssetD const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid); if (!assetRecord.has_value()) return; - const auto package = packageService.ReadPackage(assetDatabaseService.GetProjectDescriptor().RootPath / (!assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath)); - if (!package) return; - const auto summary = MetaCoreBuildUiDocumentSummary(*package, reflectionRegistry.GetTypeRegistry()); + const std::filesystem::path sourcePath = + assetDatabaseService.GetProjectDescriptor().RootPath / + (!assetRecord->PackagePath.empty() ? assetRecord->PackagePath : assetRecord->RelativePath); + std::optional summary; + if (sourcePath.filename().string().ends_with(".mcui.json") || sourcePath.extension() == ".json") { + if (const auto document = MetaCoreSceneSerializer::LoadUiFromJson(sourcePath, reflectionRegistry.GetTypeRegistry())) { + summary = MetaCoreBuildUiDocumentSummary(*document); + } + } + if (!summary.has_value()) { + const auto package = packageService.ReadPackage(sourcePath); + if (!package) return; + summary = MetaCoreBuildUiDocumentSummary(*package, reflectionRegistry.GetTypeRegistry()); + } if (!summary) return; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4)); @@ -3621,18 +4226,24 @@ void DrawMaterialAssetDetails( MetaCoreIAssetDatabaseService& assetDatabaseService, MetaCoreIReflectionRegistry& reflectionRegistry ) { + (void)reflectionRegistry; const MetaCoreSelectedAssetState& selectedAsset = editorContext.GetSelectedAsset(); const auto assetRecord = assetDatabaseService.FindAssetByGuid(selectedAsset.Guid); if (!assetRecord.has_value()) return; - const std::filesystem::path absolutePath = assetDatabaseService.GetProjectDescriptor().RootPath / assetRecord->RelativePath; + const auto assetEditingService = editorContext.GetModuleRegistry().ResolveService(); + if (assetEditingService == nullptr) { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Asset editing service unavailable"); + return; + } - auto materialDocOpt = MetaCoreSceneSerializer::LoadMaterialFromJson(absolutePath, reflectionRegistry.GetTypeRegistry()); + auto materialDocOpt = assetEditingService->LoadMaterialAsset(selectedAsset.Guid); if (!materialDocOpt.has_value()) { ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "加载材质失败!"); return; } MetaCoreMaterialAssetDocument materialDoc = std::move(*materialDocOpt); + materialDoc.AssetGuid = selectedAsset.Guid; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 4)); if (ImGui::BeginChild("MaterialInspector", ImVec2(0, 0), true, 0)) { @@ -3705,22 +4316,8 @@ void DrawMaterialAssetDetails( drawTextureCombo("遮蔽贴图 (Occlusion/Ao Texture)", materialDoc.AoTexture); if (changed) { - if (MetaCoreSceneSerializer::SaveMaterialToJson(absolutePath, materialDoc, reflectionRegistry.GetTypeRegistry())) { - const std::filesystem::path metaPath = assetDatabaseService.GetProjectDescriptor().RootPath / MetaCoreBuildMetaPath(assetRecord->RelativePath); - const std::uint64_t newHash = MetaCoreHashFile(absolutePath).value_or(0); - std::error_code ec; - if (std::filesystem::exists(metaPath, ec)) { - try { - std::ifstream input(metaPath); - nlohmann::json json; - input >> json; - input.close(); - json["source_hash"] = newHash; - std::ofstream output(metaPath); - output << json.dump(4); - output.close(); - } catch (...) {} - } + if (assetEditingService->SaveMaterialAsset(selectedAsset.Guid, materialDoc)) { + (void)assetEditingService->ApplyMaterialAssetPreviewToScene(editorContext, selectedAsset.Guid); assetDatabaseService.Refresh(); } } @@ -3813,13 +4410,17 @@ public: editorContext.SetSelectedProjectDirectory(d); } if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) { - if (ImGui::MenuItem("重命名...")) { + if (ImGui::MenuItem("Reveal in Explorer")) { + MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / d); + } + ImGui::Separator(); + if (ImGui::MenuItem("Rename...")) { PendingRenamePath_ = d; std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", d.filename().string().c_str()); openRenamePopup = true; } - if (ImGui::MenuItem("删除")) { - assetDatabaseService->DeletePath(d); + if (ImGui::MenuItem("Delete")) { + (void)assetDatabaseService->DeletePath(d); } ImGui::EndPopup(); } @@ -3846,13 +4447,43 @@ public: } MetaCoreBeginProjectAssetDragDropSource(a); if (ImGui::BeginPopupContextItem(nullptr, ImGuiPopupFlags_MouseButtonRight)) { - if (ImGui::MenuItem("重命名...")) { + if (ImGui::MenuItem("Open")) { + if (a.Type == "scene" && scenePersistenceService) { + (void)scenePersistenceService->LoadScene(editorContext, a.RelativePath); + } else if (a.Type == "model") { + (void)MetaCoreInstantiateModelAsset(editorContext, a.Guid, std::nullopt); + } else if (a.Type == "prefab") { + (void)MetaCoreInstantiatePrefab(editorContext, a.Guid, std::nullopt); + } else { + editorContext.SelectAsset(MetaCoreSelectedAssetState{ + a.Guid, + a.RelativePath, + a.Type, + a.StorageKind + }); + editorContext.ClearSelectedAssetSubId(); + editorContext.ClearSelection(); + } + } + if (ImGui::MenuItem("Reveal in Explorer")) { + MetaCoreRevealInExplorer(assetDatabaseService->GetProjectDescriptor().RootPath / a.RelativePath); + } + if (ImGui::MenuItem("Copy GUID")) { + const std::string guidText = a.Guid.ToString(); + ImGui::SetClipboardText(guidText.c_str()); + } + ImGui::Separator(); + if (ImGui::MenuItem("Reimport", nullptr, false, !a.SourcePath.empty())) { + MetaCoreReimportProjectAsset(editorContext, a.Guid); + } + ImGui::Separator(); + if (ImGui::MenuItem("Rename...")) { PendingRenamePath_ = a.RelativePath; std::snprintf(RenameBuffer_.data(), RenameBuffer_.size(), "%s", a.RelativePath.filename().string().c_str()); openRenamePopup = true; } - if (ImGui::MenuItem("删除")) { - assetDatabaseService->DeletePath(a.RelativePath); + if (ImGui::MenuItem("Delete")) { + (void)assetDatabaseService->DeletePath(a.RelativePath); } ImGui::EndPopup(); } @@ -3912,6 +4543,28 @@ public: if (ImGui::MenuItem("新建材质")) { CreateNewMaterial(editorContext, *assetDatabaseService); } + if (ImGui::MenuItem("New UI Document")) { + std::filesystem::path createdRelativePath; + MetaCoreAssetGuid createdAssetGuid{}; + if (MetaCoreCreateUiDocumentAsset( + editorContext, + editorContext.GetSelectedProjectDirectory(), + createdRelativePath, + createdAssetGuid + )) { + const auto createdRecord = assetDatabaseService->FindAssetByRelativePath(createdRelativePath); + editorContext.SelectAsset(MetaCoreSelectedAssetState{ + createdAssetGuid, + createdRelativePath, + "ui_document", + createdRecord.has_value() + ? createdRecord->StorageKind + : MetaCoreAssetStorageKind::SourceFile + }); + editorContext.ClearSelectedAssetSubId(); + editorContext.ClearSelection(); + } + } ImGui::EndPopup(); } @@ -3921,7 +4574,7 @@ public: if (ImGui::Button("确定", ImVec2(120, 0))) { std::string newName(RenameBuffer_.data()); if (!newName.empty()) { - assetDatabaseService->RenamePath(PendingRenamePath_, newName); + (void)assetDatabaseService->RenamePath(PendingRenamePath_, newName); } ImGui::CloseCurrentPopup(); } @@ -3976,6 +4629,886 @@ public: } }; +class MetaCoreRuntimeDataPanelProvider final : public MetaCoreIEditorPanelProvider { +public: + std::string GetPanelId() const override { return "RuntimeData"; } + std::string GetPanelTitle() const override { return "Runtime Data"; } + bool IsOpenByDefault() const override { return false; } + + void DrawPanel(MetaCoreEditorContext& editorContext) override { + const auto assetDatabaseService = + editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + ImGui::TextUnformatted("No MetaCore project is open."); + return; + } + if (!editorContext.EnsureRuntimeDataConfigLoaded()) { + ImGui::TextUnformatted("RuntimeData config is not available."); + return; + } + + MetaCoreRuntimeDataSourcesDocument& sourcesDocument = editorContext.AccessRuntimeDataSourcesDocument(); + MetaCoreRuntimeBindingsDocument& bindingsDocument = editorContext.AccessRuntimeBindingsDocument(); + + ImGui::Text( + "Sources: %zu DataPoints: %zu SceneBindings: %zu UiBindings: %zu", + sourcesDocument.Sources.size(), + sourcesDocument.DataPoints.size(), + bindingsDocument.Bindings.size(), + bindingsDocument.UiBindings.size() + ); + ImGui::Separator(); + + if (ImGui::Button("Save Runtime Data", ImVec2(180.0F, 26.0F))) { + (void)editorContext.SaveRuntimeDataConfig(); + LastIssues_ = editorContext.ValidateRuntimeDataConfig(); + } + ImGui::SameLine(); + if (ImGui::Button("Validate", ImVec2(120.0F, 26.0F))) { + LastIssues_ = editorContext.ValidateRuntimeDataConfig(); + const bool hasValidationError = HasValidationError(LastIssues_); + editorContext.AddConsoleMessage( + LastIssues_.empty() ? MetaCoreLogLevel::Info : (hasValidationError ? MetaCoreLogLevel::Error : MetaCoreLogLevel::Warning), + "RuntimeData", + "RuntimeData validation issues=" + std::to_string(LastIssues_.size()) + ); + } + ImGui::SameLine(); + if (ImGui::Button("Add Mock Source", ImVec2(150.0F, 26.0F))) { + AddSource(sourcesDocument, "mock"); + } + ImGui::SameLine(); + if (ImGui::Button("Add Replay Source", ImVec2(160.0F, 26.0F))) { + AddSource(sourcesDocument, "file_replay"); + } + ImGui::SameLine(); + if (ImGui::Button("Add TCP Source", ImVec2(140.0F, 26.0F))) { + AddSource(sourcesDocument, "tcp"); + } + + DrawValidationIssues(); + DrawDiagnostics(editorContext); + + ImGui::Separator(); + DrawSources(sourcesDocument); + ImGui::Separator(); + DrawDataPoints(sourcesDocument); + ImGui::Separator(); + DrawBindings(editorContext, sourcesDocument, bindingsDocument); + ImGui::Separator(); + DrawUiBindings(sourcesDocument, bindingsDocument); + } + +private: + static MetaCoreRuntimeValueType DefaultValueTypeForTarget(MetaCoreRuntimeBindingTarget target) { + switch (target) { + case MetaCoreRuntimeBindingTarget::MeshRendererVisible: + return MetaCoreRuntimeValueType::Bool; + case MetaCoreRuntimeBindingTarget::LightIntensity: + return MetaCoreRuntimeValueType::Double; + case MetaCoreRuntimeBindingTarget::TransformPosition: + case MetaCoreRuntimeBindingTarget::MeshRendererBaseColor: + case MetaCoreRuntimeBindingTarget::LightColor: + default: + return MetaCoreRuntimeValueType::Vec3; + } + } + + static std::string BuildUniqueId( + std::string_view base, + const std::function& exists + ) { + std::string candidate(base); + if (!exists(candidate)) { + return candidate; + } + for (std::size_t suffix = 2; suffix < 10000; ++suffix) { + candidate = std::string(base) + "." + std::to_string(suffix); + if (!exists(candidate)) { + return candidate; + } + } + return std::string(base) + ".new"; + } + + static void EnsureSetting(MetaCoreDataSourceDefinition& source, std::string key, std::string value) { + const auto iterator = std::find_if( + source.ConnectionSettings.begin(), + source.ConnectionSettings.end(), + [&](const MetaCoreDataSourceSetting& setting) { + return setting.Key == key; + } + ); + if (iterator == source.ConnectionSettings.end()) { + source.ConnectionSettings.push_back(MetaCoreDataSourceSetting{std::move(key), std::move(value)}); + } + } + + static void EnsureDefaultSettings(MetaCoreDataSourceDefinition& source) { + if (source.AdapterType == "file_replay") { + EnsureSetting(source, "file_path", "Runtime/RuntimeReplay.mcstream"); + } else if (source.AdapterType == "tcp") { + EnsureSetting(source, "host", "127.0.0.1"); + EnsureSetting(source, "port", "9000"); + } + } + + static void AddSource(MetaCoreRuntimeDataSourcesDocument& document, std::string adapterType) { + const std::string sourceId = BuildUniqueId( + adapterType == "mock" ? "mock-source" : (adapterType == "tcp" ? "tcp-source" : "replay-source"), + [&](std::string_view id) { + return std::any_of(document.Sources.begin(), document.Sources.end(), [&](const MetaCoreDataSourceDefinition& source) { + return source.Id == id; + }); + } + ); + + MetaCoreDataSourceDefinition source; + source.Id = sourceId; + source.AdapterType = std::move(adapterType); + source.DisplayName = sourceId; + source.AutoConnect = true; + source.ReconnectDelayMs = 1000; + EnsureDefaultSettings(source); + document.Sources.push_back(std::move(source)); + } + + static void AddDataPoint( + MetaCoreRuntimeDataSourcesDocument& document, + MetaCoreRuntimeValueType valueType + ) { + const std::string dataPointId = BuildUniqueId( + "data.point", + [&](std::string_view id) { + return std::any_of(document.DataPoints.begin(), document.DataPoints.end(), [&](const MetaCoreDataPointDefinition& point) { + return point.Id == id; + }); + } + ); + + MetaCoreDataPointDefinition point; + point.Id = dataPointId; + point.SourceId = document.Sources.empty() ? std::string{} : document.Sources.front().Id; + point.ExternalAddress = dataPointId; + point.ValueType = valueType; + document.DataPoints.push_back(std::move(point)); + } + + static void AddBindingToSelection( + MetaCoreEditorContext& editorContext, + MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + MetaCoreRuntimeBindingsDocument& bindingsDocument, + MetaCoreRuntimeBindingTarget target + ) { + const MetaCoreId selectedObjectId = editorContext.GetActiveObjectId(); + if (selectedObjectId == 0) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "RuntimeData", "Select a scene object before creating a binding"); + return; + } + if (sourcesDocument.DataPoints.empty()) { + AddDataPoint(sourcesDocument, DefaultValueTypeForTarget(target)); + } + + const std::string bindingId = BuildUniqueId( + "binding." + sourcesDocument.DataPoints.front().Id, + [&](std::string_view id) { + return std::any_of(bindingsDocument.Bindings.begin(), bindingsDocument.Bindings.end(), [&](const MetaCoreSceneBindingDefinition& binding) { + return binding.BindingId == id; + }); + } + ); + + MetaCoreSceneBindingDefinition binding; + binding.BindingId = bindingId; + binding.DataPointId = sourcesDocument.DataPoints.front().Id; + binding.TargetObjectId = selectedObjectId; + binding.Target = target; + binding.MissingDataPolicy = MetaCoreRuntimeMissingDataPolicy::KeepLastValue; + bindingsDocument.Bindings.push_back(std::move(binding)); + } + + static void AddUiTextBinding( + MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + MetaCoreRuntimeBindingsDocument& bindingsDocument, + std::string targetNodeId + ) { + if (sourcesDocument.DataPoints.empty()) { + AddDataPoint(sourcesDocument, MetaCoreRuntimeValueType::String); + } + + const std::string bindingId = BuildUniqueId( + "binding.ui." + sourcesDocument.DataPoints.front().Id, + [&](std::string_view id) { + const bool sceneBindingExists = std::any_of( + bindingsDocument.Bindings.begin(), + bindingsDocument.Bindings.end(), + [&](const MetaCoreSceneBindingDefinition& binding) { + return binding.BindingId == id; + } + ); + const bool uiBindingExists = std::any_of( + bindingsDocument.UiBindings.begin(), + bindingsDocument.UiBindings.end(), + [&](const MetaCoreUiBindingDefinition& binding) { + return binding.BindingId == id; + } + ); + return sceneBindingExists || uiBindingExists; + } + ); + + MetaCoreUiBindingDefinition binding; + binding.BindingId = bindingId; + binding.DataPointId = sourcesDocument.DataPoints.front().Id; + binding.TargetNodeId = std::move(targetNodeId); + binding.Target = MetaCoreRuntimeUiBindingTarget::Text; + binding.MissingDataPolicy = MetaCoreRuntimeMissingDataPolicy::KeepLastValue; + bindingsDocument.UiBindings.push_back(std::move(binding)); + } + + static void DrawAdapterCombo(MetaCoreDataSourceDefinition& source, const std::string& idSuffix) { + static constexpr const char* adapters[] = {"mock", "file_replay", "tcp"}; + int selectedIndex = 0; + for (int index = 0; index < static_cast(std::size(adapters)); ++index) { + if (source.AdapterType == adapters[index]) { + selectedIndex = index; + break; + } + } + if (ImGui::Combo(("Adapter##" + idSuffix).c_str(), &selectedIndex, adapters, static_cast(std::size(adapters)))) { + source.AdapterType = adapters[selectedIndex]; + EnsureDefaultSettings(source); + } + } + + static void DrawValueTypeCombo(MetaCoreRuntimeValueType& type, const std::string& idSuffix) { + static constexpr MetaCoreRuntimeValueType values[] = { + MetaCoreRuntimeValueType::Bool, + MetaCoreRuntimeValueType::Int64, + MetaCoreRuntimeValueType::Double, + MetaCoreRuntimeValueType::String, + MetaCoreRuntimeValueType::Vec3 + }; + int selectedIndex = 0; + for (int index = 0; index < static_cast(std::size(values)); ++index) { + if (type == values[index]) { + selectedIndex = index; + break; + } + } + const char* labels[] = {"Bool", "Int64", "Double", "String", "Vec3"}; + if (ImGui::Combo(("Type##" + idSuffix).c_str(), &selectedIndex, labels, static_cast(std::size(labels)))) { + type = values[selectedIndex]; + } + } + + static void DrawTargetCombo(MetaCoreRuntimeBindingTarget& target, const std::string& idSuffix) { + static constexpr MetaCoreRuntimeBindingTarget values[] = { + MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCoreRuntimeBindingTarget::MeshRendererBaseColor, + MetaCoreRuntimeBindingTarget::LightIntensity, + MetaCoreRuntimeBindingTarget::LightColor + }; + int selectedIndex = 0; + for (int index = 0; index < static_cast(std::size(values)); ++index) { + if (target == values[index]) { + selectedIndex = index; + break; + } + } + const char* labels[] = { + "Transform Position", + "MeshRenderer Visible", + "MeshRenderer BaseColor", + "Light Intensity", + "Light Color" + }; + if (ImGui::Combo(("Target##" + idSuffix).c_str(), &selectedIndex, labels, static_cast(std::size(labels)))) { + target = values[selectedIndex]; + } + } + + static void DrawMissingPolicyCombo(MetaCoreRuntimeMissingDataPolicy& policy, const std::string& idSuffix) { + static constexpr MetaCoreRuntimeMissingDataPolicy values[] = { + MetaCoreRuntimeMissingDataPolicy::KeepLastValue, + MetaCoreRuntimeMissingDataPolicy::ResetToDefault, + MetaCoreRuntimeMissingDataPolicy::MarkFaultOnly + }; + int selectedIndex = 0; + for (int index = 0; index < static_cast(std::size(values)); ++index) { + if (policy == values[index]) { + selectedIndex = index; + break; + } + } + const char* labels[] = {"Keep Last Value", "Reset To Default", "Mark Fault Only"}; + if (ImGui::Combo(("Missing Policy##" + idSuffix).c_str(), &selectedIndex, labels, static_cast(std::size(labels)))) { + policy = values[selectedIndex]; + } + } + + static void DrawSourceCombo( + const MetaCoreRuntimeDataSourcesDocument& document, + std::string& sourceId, + const std::string& idSuffix + ) { + const std::string preview = sourceId.empty() ? "" : sourceId; + if (ImGui::BeginCombo(("Source##" + idSuffix).c_str(), preview.c_str())) { + for (const MetaCoreDataSourceDefinition& source : document.Sources) { + const bool selected = source.Id == sourceId; + if (ImGui::Selectable(source.Id.c_str(), selected)) { + sourceId = source.Id; + } + } + ImGui::EndCombo(); + } + } + + static void DrawDataPointCombo( + const MetaCoreRuntimeDataSourcesDocument& document, + std::string& dataPointId, + const std::string& idSuffix + ) { + const std::string preview = dataPointId.empty() ? "" : dataPointId; + if (ImGui::BeginCombo(("Data Point##" + idSuffix).c_str(), preview.c_str())) { + for (const MetaCoreDataPointDefinition& point : document.DataPoints) { + const bool selected = point.Id == dataPointId; + if (ImGui::Selectable(point.Id.c_str(), selected)) { + dataPointId = point.Id; + } + } + ImGui::EndCombo(); + } + } + + static bool HasValidationError(const std::vector& issues) { + return std::any_of( + issues.begin(), + issues.end(), + [](const MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error; + } + ); + } + + void DrawValidationIssues() const { + if (LastIssues_.empty()) { + ImGui::TextDisabled("Validation: no issues from last validation."); + return; + } + + const bool hasValidationError = HasValidationError(LastIssues_); + ImGui::TextColored( + hasValidationError ? ImVec4(0.95F, 0.25F, 0.25F, 1.0F) : ImVec4(0.95F, 0.75F, 0.25F, 1.0F), + "Validation issues: %zu", + LastIssues_.size() + ); + for (const MetaCoreRuntimeConfigIssue& issue : LastIssues_) { + const char* severity = issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error ? "Error" : "Warning"; + ImGui::BulletText("[%s] %s: %s", severity, issue.Scope.c_str(), issue.Message.c_str()); + } + } + + void DrawDiagnostics(const MetaCoreEditorContext& editorContext) const { + const auto diagnostics = editorContext.LoadRuntimeDiagnosticsSnapshot(); + if (!diagnostics.has_value()) { + return; + } + + ImGui::Text( + "Diagnostics: sources=%zu bindings=%zu faults=%s", + diagnostics->SourceStatuses.size(), + diagnostics->BindingStatuses.size(), + diagnostics->HasFaults ? "true" : "false" + ); + for (const MetaCoreRuntimeDataSourceStatus& status : diagnostics->SourceStatuses) { + ImGui::BulletText( + "Source %s state=%d error=%s", + status.SourceId.c_str(), + static_cast(status.State), + status.LastError.c_str() + ); + } + for (const MetaCoreRuntimeBindingStatus& status : diagnostics->BindingStatuses) { + if (!status.Healthy || status.Stale) { + ImGui::BulletText( + "Binding %s stale=%s error=%s", + status.BindingId.c_str(), + status.Stale ? "true" : "false", + status.LastError.c_str() + ); + } + } + } + + void DrawSources(MetaCoreRuntimeDataSourcesDocument& document) { + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (!ImGui::CollapsingHeader("Sources")) { + return; + } + for (std::size_t index = 0; index < document.Sources.size();) { + MetaCoreDataSourceDefinition& source = document.Sources[index]; + const std::string idSuffix = "Source" + std::to_string(index); + const std::string label = source.Id.empty() + ? ("##" + idSuffix) + : (source.Id + "##" + idSuffix); + bool remove = false; + if (ImGui::TreeNode(label.c_str())) { + ImGui::InputText(("Id##" + idSuffix).c_str(), &source.Id); + ImGui::InputText(("Display Name##" + idSuffix).c_str(), &source.DisplayName); + DrawAdapterCombo(source, idSuffix); + ImGui::Checkbox(("Auto Connect##" + idSuffix).c_str(), &source.AutoConnect); + ImGui::InputScalar(("Reconnect Delay Ms##" + idSuffix).c_str(), ImGuiDataType_U64, &source.ReconnectDelayMs); + + ImGui::TextUnformatted("Connection Settings"); + for (std::size_t settingIndex = 0; settingIndex < source.ConnectionSettings.size();) { + MetaCoreDataSourceSetting& setting = source.ConnectionSettings[settingIndex]; + const std::string settingSuffix = idSuffix + ".Setting" + std::to_string(settingIndex); + ImGui::PushID(settingSuffix.c_str()); + ImGui::InputText("Key", &setting.Key); + ImGui::InputText("Value", &setting.Value); + const bool removeSetting = ImGui::SmallButton("Remove Setting"); + ImGui::PopID(); + if (removeSetting) { + source.ConnectionSettings.erase(source.ConnectionSettings.begin() + static_cast(settingIndex)); + } else { + ++settingIndex; + } + } + if (ImGui::SmallButton(("Add Setting##" + idSuffix).c_str())) { + source.ConnectionSettings.push_back(MetaCoreDataSourceSetting{"key", "value"}); + } + ImGui::SameLine(); + if (ImGui::SmallButton(("Remove Source##" + idSuffix).c_str())) { + remove = true; + } + ImGui::TreePop(); + } + + if (remove) { + document.Sources.erase(document.Sources.begin() + static_cast(index)); + } else { + ++index; + } + } + } + + void DrawDataPoints(MetaCoreRuntimeDataSourcesDocument& document) { + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (!ImGui::CollapsingHeader("Data Points")) { + return; + } + if (ImGui::Button("Add Bool Data Point")) { + AddDataPoint(document, MetaCoreRuntimeValueType::Bool); + } + ImGui::SameLine(); + if (ImGui::Button("Add Vec3 Data Point")) { + AddDataPoint(document, MetaCoreRuntimeValueType::Vec3); + } + ImGui::SameLine(); + if (ImGui::Button("Add Double Data Point")) { + AddDataPoint(document, MetaCoreRuntimeValueType::Double); + } + + for (std::size_t index = 0; index < document.DataPoints.size();) { + MetaCoreDataPointDefinition& point = document.DataPoints[index]; + const std::string idSuffix = "DataPoint" + std::to_string(index); + const std::string label = point.Id.empty() + ? ("##" + idSuffix) + : (point.Id + "##" + idSuffix); + bool remove = false; + if (ImGui::TreeNode(label.c_str())) { + ImGui::InputText(("Id##" + idSuffix).c_str(), &point.Id); + DrawSourceCombo(document, point.SourceId, idSuffix); + ImGui::InputText(("External Address##" + idSuffix).c_str(), &point.ExternalAddress); + DrawValueTypeCombo(point.ValueType, idSuffix); + if (ImGui::SmallButton(("Remove Data Point##" + idSuffix).c_str())) { + remove = true; + } + ImGui::TreePop(); + } + + if (remove) { + document.DataPoints.erase(document.DataPoints.begin() + static_cast(index)); + } else { + ++index; + } + } + } + + void DrawBindings( + MetaCoreEditorContext& editorContext, + MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + MetaCoreRuntimeBindingsDocument& bindingsDocument + ) { + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (!ImGui::CollapsingHeader("Bindings")) { + return; + } + + const MetaCoreId selectedObjectId = editorContext.GetActiveObjectId(); + ImGui::Text("Active Object Id: %llu", static_cast(selectedObjectId)); + if (ImGui::Button("Bind Position To Selection")) { + AddBindingToSelection(editorContext, sourcesDocument, bindingsDocument, MetaCoreRuntimeBindingTarget::TransformPosition); + } + ImGui::SameLine(); + if (ImGui::Button("Bind Visible To Selection")) { + AddBindingToSelection(editorContext, sourcesDocument, bindingsDocument, MetaCoreRuntimeBindingTarget::MeshRendererVisible); + } + ImGui::SameLine(); + if (ImGui::Button("Bind Color To Selection")) { + AddBindingToSelection(editorContext, sourcesDocument, bindingsDocument, MetaCoreRuntimeBindingTarget::MeshRendererBaseColor); + } + + for (std::size_t index = 0; index < bindingsDocument.Bindings.size();) { + MetaCoreSceneBindingDefinition& binding = bindingsDocument.Bindings[index]; + const std::string idSuffix = "Binding" + std::to_string(index); + const std::string label = binding.BindingId.empty() + ? ("##" + idSuffix) + : (binding.BindingId + "##" + idSuffix); + bool remove = false; + if (ImGui::TreeNode(label.c_str())) { + ImGui::InputText(("Id##" + idSuffix).c_str(), &binding.BindingId); + DrawDataPointCombo(sourcesDocument, binding.DataPointId, idSuffix); + ImGui::InputScalar(("Target Object Id##" + idSuffix).c_str(), ImGuiDataType_U64, &binding.TargetObjectId); + if (selectedObjectId != 0 && ImGui::SmallButton(("Use Active Object##" + idSuffix).c_str())) { + binding.TargetObjectId = selectedObjectId; + } + DrawTargetCombo(binding.Target, idSuffix); + DrawMissingPolicyCombo(binding.MissingDataPolicy, idSuffix); + if (ImGui::SmallButton(("Remove Binding##" + idSuffix).c_str())) { + remove = true; + } + ImGui::TreePop(); + } + + if (remove) { + bindingsDocument.Bindings.erase(bindingsDocument.Bindings.begin() + static_cast(index)); + } else { + ++index; + } + } + } + + void DrawUiBindings( + MetaCoreRuntimeDataSourcesDocument& sourcesDocument, + MetaCoreRuntimeBindingsDocument& bindingsDocument + ) { + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (!ImGui::CollapsingHeader("UI Bindings")) { + return; + } + + if (ImGui::Button("Bind Text To runtime.status")) { + AddUiTextBinding(sourcesDocument, bindingsDocument, "runtime.status"); + } + + for (std::size_t index = 0; index < bindingsDocument.UiBindings.size();) { + MetaCoreUiBindingDefinition& binding = bindingsDocument.UiBindings[index]; + const std::string idSuffix = "UiBinding" + std::to_string(index); + const std::string label = binding.BindingId.empty() + ? ("##" + idSuffix) + : (binding.BindingId + "##" + idSuffix); + bool remove = false; + if (ImGui::TreeNode(label.c_str())) { + ImGui::InputText(("Id##" + idSuffix).c_str(), &binding.BindingId); + DrawDataPointCombo(sourcesDocument, binding.DataPointId, idSuffix); + ImGui::InputText(("Target Node Id##" + idSuffix).c_str(), &binding.TargetNodeId); + ImGui::TextUnformatted("Target: Text"); + binding.Target = MetaCoreRuntimeUiBindingTarget::Text; + DrawMissingPolicyCombo(binding.MissingDataPolicy, idSuffix); + if (ImGui::SmallButton(("Remove UI Binding##" + idSuffix).c_str())) { + remove = true; + } + ImGui::TreePop(); + } + + if (remove) { + bindingsDocument.UiBindings.erase(bindingsDocument.UiBindings.begin() + static_cast(index)); + } else { + ++index; + } + } + } + + std::vector LastIssues_{}; +}; + +class MetaCoreBuildSettingsPanelProvider final : public MetaCoreIEditorPanelProvider { +public: + std::string GetPanelId() const override { return "BuildSettings"; } + std::string GetPanelTitle() const override { return "Build Settings"; } + bool IsOpenByDefault() const override { return false; } + + void DrawPanel(MetaCoreEditorContext& editorContext) override { + const auto assetDatabaseService = + editorContext.GetModuleRegistry().ResolveService(); + const auto buildService = + editorContext.GetModuleRegistry().ResolveService(); + + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + ImGui::TextUnformatted("No MetaCore project is open."); + return; + } + + const MetaCoreProjectDescriptor& project = assetDatabaseService->GetProjectDescriptor(); + BindProjectDefaults(project); + + ImGui::Text("Project: %s", project.Name.c_str()); + ImGui::Text("Root: %s", project.RootPath.string().c_str()); + ImGui::Text("Project Startup Scene: %s", project.StartupScenePath.generic_string().c_str()); + ImGui::Separator(); + + ImGui::InputText("Startup Scene", StartupScenePathBuffer_.data(), StartupScenePathBuffer_.size()); + ImGui::InputText("Startup UI", StartupUiPathBuffer_.data(), StartupUiPathBuffer_.size()); + ImGui::InputText("Build Profile", BuildProfileNameBuffer_.data(), BuildProfileNameBuffer_.size()); + ImGui::InputText("Target Platform", TargetPlatformBuffer_.data(), TargetPlatformBuffer_.size()); + ImGui::InputText("Player Exe", PlayerExecutablePathBuffer_.data(), PlayerExecutablePathBuffer_.size()); + ImGui::InputText("Output Dir", OutputDirectoryBuffer_.data(), OutputDirectoryBuffer_.size()); + ImGui::InputText("Cooked Assets Dir", CookedAssetsDirectoryBuffer_.data(), CookedAssetsDirectoryBuffer_.size()); + ImGui::Checkbox("Use cooked assets", &UseCookedAssetsInPackage_); + ImGui::Separator(); + ImGui::TextUnformatted("Runtime Data"); + ImGui::InputText("Data Sources", DataSourcesPathBuffer_.data(), DataSourcesPathBuffer_.size()); + ImGui::InputText("Bindings", BindingsPathBuffer_.data(), BindingsPathBuffer_.size()); + ImGui::InputText("Diagnostics", DiagnosticsPathBuffer_.data(), DiagnosticsPathBuffer_.size()); + ImGui::Separator(); + ImGui::Checkbox("Cook before package", &CookBeforePackage_); + ImGui::Checkbox("Copy loose Assets/Scenes", &CopyLooseContent_); + ImGui::Checkbox("Copy Runtime config", &CopyRuntimeConfig_); + + const bool canBuild = buildService != nullptr; + if (!canBuild) { + ImGui::BeginDisabled(); + } + if (ImGui::Button("Save Build Settings", ImVec2(-1.0F, 28.0F))) { + (void)SaveBuildSettings(editorContext, project, true); + } + if (ImGui::Button("Build Player Package", ImVec2(-1.0F, 28.0F))) { + BuildPackage(editorContext, *buildService, false); + } + if (ImGui::Button("Build Cooked-Only Package", ImVec2(-1.0F, 28.0F))) { + BuildPackage(editorContext, *buildService, true); + } + if (!canBuild) { + ImGui::EndDisabled(); + } + + if (!LastOutputRoot_.empty()) { + ImGui::Separator(); + ImGui::Text("Last Output: %s", LastOutputRoot_.string().c_str()); + } + } + +private: + static void CopyPathToBuffer(std::array& buffer, const std::filesystem::path& path) { + std::snprintf(buffer.data(), buffer.size(), "%s", path.string().c_str()); + } + + static void CopyStringToBuffer(std::array& buffer, const std::string& value) { + std::snprintf(buffer.data(), buffer.size(), "%s", value.c_str()); + } + + static std::filesystem::path BuildRuntimeDirectory(const MetaCoreProjectDescriptor& project) { + return !project.RuntimePath.empty() ? project.RuntimePath : (project.RootPath / "Runtime"); + } + + static std::filesystem::path BuildRuntimeDirectoryRelativePath(const MetaCoreProjectDescriptor& project) { + return MetaCoreBuildRuntimeDirectoryRelativePath(project.RootPath, project.RuntimePath); + } + + static MetaCoreRuntimeProjectDocument BuildDefaultRuntimeProjectDocument( + const MetaCoreProjectDescriptor& project + ) { + const std::filesystem::path runtimeDirectoryRelative = BuildRuntimeDirectoryRelativePath(project); + MetaCoreRuntimeProjectDocument document = + MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative); + document.StartupScenePath = !project.StartupScenePath.empty() + ? project.StartupScenePath + : (std::filesystem::path("Scenes") / "Main.mcscene.json"); + document.OutputDirectory = !project.BuildPath.empty() + ? project.BuildPath.lexically_relative(project.RootPath) + : (std::filesystem::path("Build") / "Windows"); + return document; + } + + static void ApplyRuntimeProjectDefaults( + MetaCoreRuntimeProjectDocument& document, + const MetaCoreProjectDescriptor& project + ) { + const MetaCoreRuntimeProjectDocument defaults = BuildDefaultRuntimeProjectDocument(project); + if (document.StartupScenePath.empty()) { + document.StartupScenePath = defaults.StartupScenePath; + } + if (document.OutputDirectory.empty()) { + document.OutputDirectory = defaults.OutputDirectory; + } + MetaCoreApplyRuntimeProjectDefaults(document, BuildRuntimeDirectoryRelativePath(project)); + } + + static MetaCoreRuntimeProjectDocument LoadRuntimeProjectDocument( + const MetaCoreProjectDescriptor& project + ) { + const MetaCoreTypeRegistry registry = MetaCoreBuildRuntimePanelTypeRegistry(); + MetaCoreRuntimeProjectDocument document = MetaCoreReadRuntimeProjectDocument( + BuildRuntimeDirectory(project) / "ProjectRuntime.mcruntimecfg", + registry + ).value_or(BuildDefaultRuntimeProjectDocument(project)); + ApplyRuntimeProjectDefaults(document, project); + return document; + } + + void CopyDocumentToBuffers(const MetaCoreRuntimeProjectDocument& document) { + CopyPathToBuffer(StartupScenePathBuffer_, document.StartupScenePath); + CopyPathToBuffer(StartupUiPathBuffer_, document.StartupUiPath); + CopyStringToBuffer(BuildProfileNameBuffer_, document.BuildProfileName); + CopyStringToBuffer(TargetPlatformBuffer_, document.TargetPlatform); + CopyPathToBuffer(OutputDirectoryBuffer_, document.OutputDirectory); + CopyPathToBuffer(CookedAssetsDirectoryBuffer_, document.CookedAssetsDirectory); + CopyPathToBuffer(DataSourcesPathBuffer_, document.DataSourcesPath); + CopyPathToBuffer(BindingsPathBuffer_, document.BindingsPath); + CopyPathToBuffer(DiagnosticsPathBuffer_, document.DiagnosticsPath); + UseCookedAssetsInPackage_ = document.UseCookedAssets; + } + + void BindProjectDefaults(const MetaCoreProjectDescriptor& project) { + if (BoundProjectRoot_ == project.RootPath) { + return; + } + + BoundProjectRoot_ = project.RootPath; + CopyDocumentToBuffers(LoadRuntimeProjectDocument(project)); + PlayerExecutablePathBuffer_[0] = '\0'; + CookBeforePackage_ = true; + CopyLooseContent_ = true; + CopyRuntimeConfig_ = true; + LastOutputRoot_.clear(); + } + + MetaCoreRuntimeProjectDocument BuildDocumentFromBuffers( + const MetaCoreProjectDescriptor& project + ) const { + MetaCoreRuntimeProjectDocument document = LoadRuntimeProjectDocument(project); + document.StartupScenePath = std::filesystem::path(StartupScenePathBuffer_.data()).lexically_normal(); + document.StartupUiPath = std::filesystem::path(StartupUiPathBuffer_.data()).lexically_normal(); + document.BuildProfileName = BuildProfileNameBuffer_.data(); + document.TargetPlatform = TargetPlatformBuffer_.data(); + document.OutputDirectory = std::filesystem::path(OutputDirectoryBuffer_.data()).lexically_normal(); + document.CookedAssetsDirectory = std::filesystem::path(CookedAssetsDirectoryBuffer_.data()).lexically_normal(); + document.DataSourcesPath = std::filesystem::path(DataSourcesPathBuffer_.data()).lexically_normal(); + document.BindingsPath = std::filesystem::path(BindingsPathBuffer_.data()).lexically_normal(); + document.DiagnosticsPath = std::filesystem::path(DiagnosticsPathBuffer_.data()).lexically_normal(); + document.UseCookedAssets = UseCookedAssetsInPackage_; + ApplyRuntimeProjectDefaults(document, project); + return document; + } + + bool SaveBuildSettings( + MetaCoreEditorContext& editorContext, + const MetaCoreProjectDescriptor& project, + bool reportSuccess + ) { + const MetaCoreTypeRegistry registry = MetaCoreBuildRuntimePanelTypeRegistry(); + const std::filesystem::path runtimeProjectPath = + BuildRuntimeDirectory(project) / "ProjectRuntime.mcruntimecfg"; + const MetaCoreRuntimeProjectDocument document = BuildDocumentFromBuffers(project); + const std::vector validationIssues = + MetaCoreValidateRuntimeProjectPaths(document); + if (!validationIssues.empty()) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Build", + "Build Settings validation failed: " + validationIssues.front().Message + ); + return false; + } + if (!MetaCoreWriteRuntimeProjectDocument(runtimeProjectPath, document, registry)) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Build", + "Failed to save Build Settings: " + runtimeProjectPath.string() + ); + return false; + } + + if (reportSuccess) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Build", + "Build Settings saved: " + runtimeProjectPath.string() + ); + } + return true; + } + + void BuildPackage( + MetaCoreEditorContext& editorContext, + MetaCoreIBuildService& buildService, + bool cookedOnly + ) { + const auto assetDatabaseService = + editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Build", "No MetaCore project is open"); + return; + } + if (!SaveBuildSettings(editorContext, assetDatabaseService->GetProjectDescriptor(), false)) { + return; + } + + MetaCoreBuildPlayerPackageRequest request; + if (PlayerExecutablePathBuffer_[0] != '\0') { + request.PlayerExecutablePath = PlayerExecutablePathBuffer_.data(); + } + if (OutputDirectoryBuffer_[0] != '\0') { + request.OutputDirectory = OutputDirectoryBuffer_.data(); + } + request.CookBeforePackage = CookBeforePackage_; + request.CopyLooseProjectContent = cookedOnly ? false : CopyLooseContent_; + request.CopyRuntimeConfig = CopyRuntimeConfig_; + request.UseCookedAssetsInPackage = cookedOnly ? true : UseCookedAssetsInPackage_; + + const MetaCoreBuildPlayerPackageResult result = buildService.BuildPlayerPackage(request); + if (!result.Success) { + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Error, + "Build", + "Player package failed: " + result.Error + + " dependencies=" + std::to_string(result.DependencyReport.size()) + ); + return; + } + + LastOutputRoot_ = result.OutputRoot; + editorContext.AddConsoleMessage( + MetaCoreLogLevel::Info, + "Build", + "Player package created: " + result.OutputRoot.string() + + " copied=" + std::to_string(result.CopiedFiles.size()) + + " cooked=" + std::to_string(result.CookedAssets.size()) + + " dependencies=" + std::to_string(result.DependencyReport.size()) + ); + } + + std::filesystem::path BoundProjectRoot_{}; + std::array StartupScenePathBuffer_{}; + std::array StartupUiPathBuffer_{}; + std::array BuildProfileNameBuffer_{}; + std::array TargetPlatformBuffer_{}; + std::array PlayerExecutablePathBuffer_{}; + std::array OutputDirectoryBuffer_{}; + std::array CookedAssetsDirectoryBuffer_{}; + std::array DataSourcesPathBuffer_{}; + std::array BindingsPathBuffer_{}; + std::array DiagnosticsPathBuffer_{}; + bool CookBeforePackage_ = true; + bool CopyLooseContent_ = true; + bool CopyRuntimeConfig_ = true; + bool UseCookedAssetsInPackage_ = false; + std::filesystem::path LastOutputRoot_{}; +}; + class MetaCoreTransformInspectorDrawer final : public MetaCoreIInspectorDrawer { public: std::string GetDrawerId() const override { return "Transform"; } @@ -4423,6 +5956,10 @@ public: if (ImGui::SmallButton("Revert")) { (void)MetaCoreRevertSelectedPrefabInstance(editorContext); } + ImGui::SameLine(); + if (ImGui::SmallButton("Break")) { + (void)MetaCoreBreakSelectedPrefabInstance(editorContext); + } ImGui::TextDisabled( "Asset: %s | RootId: %llu | PrefabObjectId: %llu", prefabAsset.has_value() ? prefabAsset->RelativePath.generic_string().c_str() : "", @@ -4511,6 +6048,8 @@ public: if (descriptor.DrawInspector) { descriptor.DrawInspector(editorContext, selectedObject); + } else { + MetaCoreDrawReflectedComponentInspector(editorContext, selectedObject, descriptor); } } ImGui::PopID(); @@ -4557,6 +6096,8 @@ public: moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterPanelProvider(std::make_unique()); + moduleRegistry.RegisterPanelProvider(std::make_unique()); + moduleRegistry.RegisterPanelProvider(std::make_unique()); moduleRegistry.RegisterInspectorDrawer(std::make_unique()); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index a2e2558..441ef13 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -19,6 +19,7 @@ #define GLM_ENABLE_EXPERIMENTAL #include #include +#include #include #include #include @@ -30,6 +31,9 @@ #include #include #include +#include +#include +#include #include #include @@ -68,6 +72,42 @@ constexpr bool GMetaCoreEnableImGuizmo = true; constexpr bool GMetaCoreEnableImGuizmo = true; #endif +constexpr const char* GMetaCoreEditorLayoutRelativePath = "Library/Editor/imgui.ini"; + +[[nodiscard]] const char* MetaCoreGetInfernuxDockWindowId(std::string_view panelId) { + if (panelId == "Hierarchy") { + return "hierarchy"; + } + if (panelId == "Inspector") { + return "inspector"; + } + if (panelId == "Project") { + return "project"; + } + if (panelId == "Console") { + return "console"; + } + if (panelId == "RuntimeData") { + return "runtime_data"; + } + if (panelId == "BuildSettings") { + return "build_settings"; + } + return ""; +} + +[[nodiscard]] std::string MetaCoreBuildDockWindowName(const MetaCoreIEditorPanelProvider& panelProvider) { + const char* stableId = MetaCoreGetInfernuxDockWindowId(panelProvider.GetPanelId()); + if (stableId == nullptr || stableId[0] == '\0') { + return panelProvider.GetPanelTitle(); + } + + std::string windowName = panelProvider.GetPanelTitle(); + windowName += "###"; + windowName += stableId; + return windowName; +} + [[nodiscard]] bool MetaCoreInstantiateProjectAssetToScene( MetaCoreEditorContext& editorContext, const MetaCoreProjectAssetDragDropPayload& payload @@ -703,6 +743,7 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { int MetaCoreEditorApp::Run() { while (!Window_.ShouldClose()) { Window_.BeginFrame(); + SyncImGuiLayoutIniPath(); // Filament 接管,不再使用 OpenGL3 后端的 NewFrame ImGui_ImplWin32_NewFrame(); @@ -711,6 +752,11 @@ int MetaCoreEditorApp::Run() { ImGuizmo::BeginFrame(); } + if (const auto playModeService = ModuleRegistry_.ResolveService(); + playModeService != nullptr) { + playModeService->TickPlayMode(*EditorContext_, Window_.GetDeltaSeconds()); + } + DrawEditorFrame(); ImGui::Render(); @@ -755,11 +801,8 @@ bool MetaCoreEditorApp::InitializeImGui() { ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; -#if defined(_DEBUG) - // Avoid loading persisted docking/layout ini in Debug; malformed/legacy ini may trigger large allocations. io.IniFilename = nullptr; io.LogFilename = nullptr; -#endif MetaCoreTraceStartup("metacore.ui: font setup begin"); MetaCoreConfigureChineseFont(); MetaCoreTraceStartup("metacore.ui: font setup done"); @@ -780,295 +823,622 @@ void MetaCoreEditorApp::ShutdownImGui() { ImGui::DestroyContext(); } +void MetaCoreEditorApp::SyncImGuiLayoutIniPath() { + if (EditorContext_ == nullptr) { + return; + } + + std::filesystem::path projectRoot{}; + if (const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + projectRoot = assetDatabaseService->GetProjectDescriptor().RootPath.lexically_normal(); + } + + if (projectRoot == ImGuiLayoutProjectRoot_) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + if (!ImGuiIniPath_.empty()) { + ImGui::SaveIniSettingsToDisk(ImGuiIniPath_.c_str()); + } + + ImGuiLayoutProjectRoot_ = projectRoot; + ImGuiIniPath_.clear(); + io.IniFilename = nullptr; + + if (projectRoot.empty()) { + if (EditorContext_ != nullptr) { + EditorContext_->SetDockLayoutBuilt(false); + } + return; + } + + const std::filesystem::path layoutIniPath = projectRoot / GMetaCoreEditorLayoutRelativePath; + const bool hasSavedLayout = std::filesystem::exists(layoutIniPath); + std::error_code errorCode; + std::filesystem::create_directories(layoutIniPath.parent_path(), errorCode); + + ImGui::ClearIniSettings(); + ImGuiIniPath_ = layoutIniPath.string(); + io.IniFilename = ImGuiIniPath_.c_str(); + if (hasSavedLayout) { + ImGui::LoadIniSettingsFromDisk(ImGuiIniPath_.c_str()); + } + EditorContext_->SetDockLayoutBuilt(hasSavedLayout); +} + void MetaCoreEditorApp::DrawEditorFrame() { SceneInteractionService_.HandleShortcuts(*EditorContext_); - const ImGuiViewport* mainViewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(mainViewport->WorkPos); - ImGui::SetNextWindowSize(mainViewport->WorkSize); - ImGui::SetNextWindowViewport(mainViewport->ID); + { + const ImGuiViewport* mainViewport = ImGui::GetMainViewport(); + constexpr float statusBarHeight = 24.0F; + ImGui::SetNextWindowPos(mainViewport->WorkPos); + ImGui::SetNextWindowSize(ImVec2(mainViewport->WorkSize.x, mainViewport->WorkSize.y - statusBarHeight)); + ImGui::SetNextWindowViewport(mainViewport->ID); - constexpr ImGuiWindowFlags hostWindowFlags = - ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoBackground | - ImGuiWindowFlags_NoBringToFrontOnFocus | - ImGuiWindowFlags_NoNavFocus | - ImGuiWindowFlags_MenuBar; + constexpr ImGuiWindowFlags hostWindowFlags = + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoBringToFrontOnFocus | + ImGuiWindowFlags_NoNavFocus | + ImGuiWindowFlags_MenuBar; - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); - ImGui::Begin("MetaCoreMainDockSpace", nullptr, hostWindowFlags); - ImGui::PopStyleVar(3); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + ImGui::Begin("DockSpaceWindow", nullptr, hostWindowFlags); + ImGui::PopStyleVar(3); - if (ImGui::BeginMenuBar()) { - for (const auto& menuProvider : ModuleRegistry_.GetMenuProviders()) { - menuProvider->DrawMenuBar(*EditorContext_); + if (ImGui::BeginMenuBar()) { + for (const auto& menuProvider : ModuleRegistry_.GetMenuProviders()) { + menuProvider->DrawMenuBar(*EditorContext_); + } + ImGui::EndMenuBar(); } - // Center Play/Pause/Stop buttons - const float buttonWidth = 32.0F; - const float totalWidth = buttonWidth * 3 + ImGui::GetStyle().ItemSpacing.x * 2; - ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalWidth) * 0.5F); - - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2F, 0.2F, 0.2F, 0.0F)); - if (ImGui::Button(" > ", ImVec2(buttonWidth, 0))) { /* Play */ } - ImGui::SameLine(); - if (ImGui::Button(" ||", ImVec2(buttonWidth, 0))) { /* Pause */ } - ImGui::SameLine(); - if (ImGui::Button(" >>", ImVec2(buttonWidth, 0))) { /* Step */ } - ImGui::PopStyleColor(); - - ImGui::EndMenuBar(); - } - - const ImGuiID dockSpaceId = ImGui::GetID("MetaCoreDockSpaceId"); - ImGui::DockSpace(dockSpaceId, ImVec2(0.0F, 0.0F), ImGuiDockNodeFlags_PassthruCentralNode); - EnsureDefaultDockLayout(dockSpaceId); - ImGui::End(); - - for (const auto& panelProvider : ModuleRegistry_.GetPanelProviders()) { - if (panelProvider->GetPanelId() == "Scene") { - continue; + const ImGuiID dockSpaceId = ImGui::GetID("MainDockSpace"); + ImGui::DockSpace(dockSpaceId, ImVec2(0.0F, 0.0F), ImGuiDockNodeFlags_None); + if (!EditorContext_->HasDockLayoutBuilt()) { + EnsureDefaultDockLayout(dockSpaceId); } - - bool& panelOpen = ModuleRegistry_.AccessPanelOpenState(panelProvider->GetPanelId()); - if (!panelOpen) { - continue; - } - - bool pushedTransparentColors = false; - if (panelProvider->WantsTransparentBackground()) { - ImGui::SetNextWindowBgAlpha(0.0F); - ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0F, 0.0F, 0.0F, 0.0F)); - ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0F, 0.0F, 0.0F, 0.0F)); - ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0.0F, 0.0F, 0.0F, 0.0F)); - pushedTransparentColors = true; - } - - bool pushedPadding = false; - if (panelProvider->WantsZeroPadding()) { - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); - pushedPadding = true; - } - - const ImGuiWindowFlags panelWindowFlags = static_cast(panelProvider->GetWindowFlags()); - ImGui::Begin(panelProvider->GetPanelTitle().c_str(), &panelOpen, panelWindowFlags); - panelProvider->DrawPanel(*EditorContext_); ImGui::End(); - if (pushedPadding) { - ImGui::PopStyleVar(); + DrawEditorToolbar(); + + for (const auto& panelProvider : ModuleRegistry_.GetPanelProviders()) { + if (panelProvider->GetPanelId() == "Scene") { + continue; + } + + bool& panelOpen = ModuleRegistry_.AccessPanelOpenState(panelProvider->GetPanelId()); + if (!panelOpen) { + continue; + } + + bool pushedTransparentColors = false; + if (panelProvider->WantsTransparentBackground()) { + ImGui::SetNextWindowBgAlpha(0.0F); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0F, 0.0F, 0.0F, 0.0F)); + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.0F, 0.0F, 0.0F, 0.0F)); + ImGui::PushStyleColor(ImGuiCol_PopupBg, ImVec4(0.0F, 0.0F, 0.0F, 0.0F)); + pushedTransparentColors = true; + } + + bool pushedPadding = false; + if (panelProvider->WantsZeroPadding()) { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + pushedPadding = true; + } + + const ImGuiWindowFlags panelWindowFlags = static_cast(panelProvider->GetWindowFlags()); + const std::string panelWindowName = MetaCoreBuildDockWindowName(*panelProvider); + ImGui::Begin(panelWindowName.c_str(), &panelOpen, panelWindowFlags); + panelProvider->DrawPanel(*EditorContext_); + ImGui::End(); + + if (pushedPadding) { + ImGui::PopStyleVar(); + } + + if (pushedTransparentColors) { + ImGui::PopStyleColor(3); + } } - if (pushedTransparentColors) { - ImGui::PopStyleColor(3); + DrawSceneViewWindow(); + DrawGameViewWindow(); + DrawPlaceholderDockWindow("UI Editor###ui_editor", "UI Editor"); + DrawPlaceholderDockWindow("Anim Clip 2D###animclip2d_editor", "Animation Clip 2D"); + DrawPlaceholderDockWindow("Anim FSM###animfsm_editor", "Animation FSM"); + ApplyPendingDockTabSelections(); + DrawEditorStatusBar(); + } +} + +void MetaCoreEditorApp::DrawEditorToolbar() { + constexpr ImGuiWindowFlags toolbarWindowFlags = + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse | + ImGuiWindowFlags_NoSavedSettings; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0F, 4.0F)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0F, 4.0F)); + if (ImGui::Begin("Toolbar###toolbar", nullptr, toolbarWindowFlags)) { + auto drawButton = [](const char* label, bool selected, bool enabled = true) { + if (selected) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20F, 0.34F, 0.58F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25F, 0.40F, 0.68F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.16F, 0.29F, 0.50F, 1.0F)); + } else if (!enabled) { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.16F, 0.16F, 0.16F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.16F, 0.16F, 0.16F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.16F, 0.16F, 0.16F, 1.0F)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.45F, 0.45F, 0.45F, 1.0F)); + } + const bool clicked = enabled && ImGui::Button(label, ImVec2(0.0F, 24.0F)); + if (selected) { + ImGui::PopStyleColor(3); + } else if (!enabled) { + ImGui::PopStyleColor(4); + } + return clicked; + }; + + const float windowWidth = ImGui::GetWindowWidth(); + const float playControlsWidth = 300.0F; + ImGui::SetCursorPosX(std::max(8.0F, (windowWidth - playControlsWidth) * 0.5F)); + + if (const auto playModeService = ModuleRegistry_.ResolveService(); + playModeService != nullptr) { + const MetaCorePlayModeState playState = playModeService->GetState(); + const bool isPlaying = playState == MetaCorePlayModeState::Playing || playState == MetaCorePlayModeState::Paused; + const bool isPaused = playState == MetaCorePlayModeState::Paused; + if (playState == MetaCorePlayModeState::Edit) { + if (drawButton("Play", false)) { + (void)playModeService->EnterPlayMode(*EditorContext_); + } + } else { + if (drawButton("Stop", true)) { + (void)playModeService->ExitPlayMode(*EditorContext_); + } + } + ImGui::SameLine(0.0F, 4.0F); + if (isPaused) { + if (drawButton("Resume", true)) { + (void)playModeService->ResumePlayMode(*EditorContext_); + } + } else if (drawButton("Pause", false, isPlaying)) { + (void)playModeService->PausePlayMode(*EditorContext_); + } + ImGui::SameLine(0.0F, 4.0F); + if (drawButton("Step", false, isPaused)) { + (void)playModeService->StepPlayMode(*EditorContext_, 1.0F / 60.0F); + } + if (isPlaying) { + ImGui::SameLine(0.0F, 8.0F); + ImGui::Text("Time %.2fs", playModeService->GetElapsedPlayTimeSeconds()); + } + } else { + (void)drawButton("Play", false, false); + ImGui::SameLine(0.0F, 4.0F); + (void)drawButton("Pause", false, false); + ImGui::SameLine(0.0F, 4.0F); + (void)drawButton("Step", false, false); + } + + constexpr float rightControlsWidth = 180.0F; + ImGui::SameLine(std::max(8.0F, windowWidth - rightControlsWidth)); + if (ImGui::Button("Gizmos", ImVec2(78.0F, 24.0F))) { + ImGui::OpenPopup("MetaCoreToolbarGizmosPopup"); + } + ImGui::SameLine(0.0F, 4.0F); + if (ImGui::Button("Camera", ImVec2(78.0F, 24.0F))) { + ImGui::OpenPopup("MetaCoreToolbarCameraPopup"); + } + + if (ImGui::BeginPopup("MetaCoreToolbarGizmosPopup")) { + if (ImGui::MenuItem("View", nullptr, EditorContext_->GetGizmoOperation() == MetaCoreGizmoOperation::None)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::None); + } + if (ImGui::MenuItem("Move", nullptr, EditorContext_->GetGizmoOperation() == MetaCoreGizmoOperation::Translate)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::Translate); + } + if (ImGui::MenuItem("Rotate", nullptr, EditorContext_->GetGizmoOperation() == MetaCoreGizmoOperation::Rotate)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::Rotate); + } + if (ImGui::MenuItem("Scale", nullptr, EditorContext_->GetGizmoOperation() == MetaCoreGizmoOperation::Scale)) { + EditorContext_->SetGizmoOperation(MetaCoreGizmoOperation::Scale); + } + ImGui::Separator(); + if (ImGui::MenuItem("Local", nullptr, EditorContext_->GetGizmoMode() == MetaCoreGizmoMode::Local)) { + EditorContext_->SetGizmoMode(MetaCoreGizmoMode::Local); + } + if (ImGui::MenuItem("Global", nullptr, EditorContext_->GetGizmoMode() == MetaCoreGizmoMode::Global)) { + EditorContext_->SetGizmoMode(MetaCoreGizmoMode::Global); + } + ImGui::Separator(); + bool snapEnabled = EditorContext_->GetGizmoSnapSettings().Enabled; + if (ImGui::Checkbox("Snap", &snapEnabled)) { + EditorContext_->SetGizmoSnapEnabled(snapEnabled); + } + bool showGrid = EditorContext_->GetShowViewportGrid(); + if (ImGui::Checkbox("Show Grid", &showGrid)) { + EditorContext_->SetShowViewportGrid(showGrid); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopup("MetaCoreToolbarCameraPopup")) { + MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView(); + ImGui::TextUnformatted("Scene Camera"); + ImGui::Separator(); + bool cameraChanged = false; + float fieldOfViewDegrees = sceneView.VerticalFieldOfViewDegrees; + ImGui::SetNextItemWidth(180.0F); + if (ImGui::SliderFloat("FOV", &fieldOfViewDegrees, 20.0F, 110.0F, "%.1f")) { + sceneView.VerticalFieldOfViewDegrees = fieldOfViewDegrees; + cameraChanged = true; + } + ImGui::SetNextItemWidth(220.0F); + if (ImGui::DragFloat3("Position", glm::value_ptr(sceneView.CameraPosition), 0.05F)) { + cameraChanged = true; + } + ImGui::SetNextItemWidth(220.0F); + if (ImGui::DragFloat3("Target", glm::value_ptr(sceneView.CameraTarget), 0.05F)) { + cameraChanged = true; + } + if (cameraChanged) { + EditorContext_->GetCameraController().ApplySceneView(sceneView); + } + if (ImGui::Button("Focus Selection", ImVec2(-1.0F, 0.0F))) { + if (MetaCoreGameObject selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject) { + EditorContext_->GetCameraController().FocusGameObject(selectedObject); + } + } + if (ImGui::Button("Reset Camera", ImVec2(-1.0F, 0.0F))) { + MetaCoreSceneView defaultSceneView{}; + defaultSceneView.CameraPosition = {0.0F, 2.5F, 6.5F}; + defaultSceneView.CameraTarget = {0.0F, 0.7F, 0.0F}; + defaultSceneView.CameraUp = {0.0F, 1.0F, 0.0F}; + defaultSceneView.VerticalFieldOfViewDegrees = 60.0F; + EditorContext_->GetCameraController().ApplySceneView(defaultSceneView); + } + ImGui::EndPopup(); } } + ImGui::End(); + ImGui::PopStyleVar(2); +} - if (ModuleRegistry_.IsPanelOpen("Scene")) { - ImGuiDockNode* centralNode = ImGui::DockBuilderGetCentralNode(dockSpaceId); - if (centralNode != nullptr) { - // MetaCore main viewport keeps a Unity-like Scene / Game mental model. - ImGui::SetNextWindowPos(centralNode->Pos); - ImGui::SetNextWindowSize(ImVec2(centralNode->Size.x, 30.0F)); - ImGui::Begin("ViewportTabs", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav); +void MetaCoreEditorApp::DrawEditorStatusBar() { + const ImGuiViewport* mainViewport = ImGui::GetMainViewport(); + constexpr float statusBarHeight = 24.0F; + ImGui::SetNextWindowPos(ImVec2(mainViewport->WorkPos.x, mainViewport->WorkPos.y + mainViewport->WorkSize.y - statusBarHeight)); + ImGui::SetNextWindowSize(ImVec2(mainViewport->WorkSize.x, statusBarHeight)); + ImGui::SetNextWindowViewport(mainViewport->ID); - static int activeTab = 0; // 0: Scene, 1: Game - if (ImGui::Selectable(" 场景 ", activeTab == 0, 0, ImVec2(60, 0))) activeTab = 0; - ImGui::SameLine(); - if (ImGui::Selectable(" 游戏 ", activeTab == 1, 0, ImVec2(60, 0))) activeTab = 1; + constexpr ImGuiWindowFlags statusWindowFlags = + ImGuiWindowFlags_NoDocking | + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoSavedSettings; - ImGui::End(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0F, 3.0F)); + ImGui::Begin("MetaCoreStatusBar", nullptr, statusWindowFlags); + ImGui::TextUnformatted("MetaCore"); + ImGui::SameLine(0.0F, 16.0F); + if (const auto playModeService = ModuleRegistry_.ResolveService(); + playModeService != nullptr) { + const MetaCorePlayModeState playState = playModeService->GetState(); + const char* stateText = playState == MetaCorePlayModeState::Edit ? "Edit" : + (playState == MetaCorePlayModeState::Playing ? "Playing" : "Paused"); + ImGui::Text("Mode: %s", stateText); + } + if (const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { + ImGui::SameLine(0.0F, 16.0F); + const std::string projectRoot = assetDatabaseService->GetProjectDescriptor().RootPath.string(); + ImGui::Text("Project: %s", projectRoot.c_str()); + } + ImGui::End(); + ImGui::PopStyleVar(); +} - constexpr float sceneToolbarHeight = 34.0F; - constexpr float tabHeaderHeight = 30.0F; - MetaCoreSceneViewportState& viewportState = EditorContext_->GetSceneViewportState(); - const ImVec2 mainViewportPosition = ImGui::GetMainViewport()->Pos; - viewportState.Left = centralNode->Pos.x; - viewportState.Top = centralNode->Pos.y + sceneToolbarHeight + tabHeaderHeight; - viewportState.Width = centralNode->Size.x; - if (viewportState.Width < 1.0F) { - viewportState.Width = 1.0F; - } - viewportState.Height = centralNode->Size.y - sceneToolbarHeight - tabHeaderHeight; - if (viewportState.Height < 1.0F) { - viewportState.Height = 1.0F; - } +void MetaCoreEditorApp::DrawSceneViewWindow() { + if (!ModuleRegistry_.IsPanelOpen("Scene")) { + SceneInteractionService_.ResetFrameState(); + ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); + return; + } - // --- 补丁三重构开始 --- + bool& sceneOpen = ModuleRegistry_.AccessPanelOpenState("Scene"); + bool drewSceneViewport = false; + bool gizmoUsing = false; - // 1. 准备 GizmoCanvas 窗口属性 - ImGui::SetNextWindowPos(ImVec2(centralNode->Pos.x, centralNode->Pos.y + sceneToolbarHeight + tabHeaderHeight)); - ImGui::SetNextWindowSize(ImVec2(centralNode->Size.x, centralNode->Size.y - sceneToolbarHeight - tabHeaderHeight)); - ImGui::SetNextWindowBgAlpha(0.0F); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0F); - ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0F); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + constexpr ImGuiWindowFlags sceneWindowFlags = + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse; - // 【问题二修复】:如果当前没有处于资产拖拽过程中,则屏蔽输入以允许相机漫游操作; - // 如果正在拖放资产(如模型或预制体),则恢复输入以便 BeginDragDropTarget 能够正确接收放置。 - ImGuiWindowFlags gizmoCanvasFlags = - ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | - ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | - ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | - ImGuiWindowFlags_NoBackground; - - if (ImGui::GetDragDropPayload() == nullptr) { - gizmoCanvasFlags |= ImGuiWindowFlags_NoInputs; - } - - bool gizmoCanvasOpen = true; - bool gizmoUsing = false; - bool gizmoHovering = false; - - if (ImGui::Begin("MetaCoreSceneGizmoCanvas", &gizmoCanvasOpen, gizmoCanvasFlags)) { - // 2. 拿到最精确的屏幕绝对位置和画布大小 - ImVec2 viewportPos = ImGui::GetCursorScreenPos(); - ImVec2 viewportSize = ImGui::GetContentRegionAvail(); - - // 3. 全局统一更新 viewportState - viewportState.Left = viewportPos.x; - viewportState.Top = viewportPos.y; - viewportState.Width = viewportSize.x; - viewportState.Height = viewportSize.y; - - // 处理交互状态 - const ImVec2 mousePosition = ImGui::GetMousePos(); - viewportState.Hovered = - mousePosition.x >= viewportState.Left && - mousePosition.x <= viewportState.Left + viewportState.Width && - mousePosition.y >= viewportState.Top && - mousePosition.y <= viewportState.Top + viewportState.Height; - viewportState.Focused = viewportState.Hovered && !ImGui::GetIO().WantCaptureMouse; - - // 处理聚焦快捷键 - if (viewportState.Focused && !ImGui::GetIO().WantCaptureKeyboard && EditorContext_->GetInput().WasKeyPressed(MetaCoreInputKey::Focus)) { - if (MetaCoreGameObject selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject) { - EditorContext_->GetCameraController().FocusGameObject(selectedObject); - } - } - - // 4. 同步尺寸给 Filament (SetViewportRect 内部会处理 Resize) - ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{ - viewportState.Left, viewportState.Top, viewportState.Width, viewportState.Height - }); - - // 5. 更新相机并构建 SceneView (在渲染之前!) - gizmoUsing = ImGuizmo::IsUsing(); - if (!gizmoUsing) { - EditorContext_->GetCameraController().Update(viewportState, EditorContext_->GetInput()); - } - MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView(); - sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId(); - - // 6. 驱动 Filament 渲染 - ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView); - - // 7. 绘制画面 (应用 UV 翻转:Filament Y轴起点在底部,ImGui 在顶部) - void* texPtr = ViewportRenderer_.GetFilamentTexturePointer(); - if (texPtr) { - ImGui::Image(texPtr, viewportSize, ImVec2(0, 1), ImVec2(1, 0)); - } - - // 7.5. 视口拖拽接收器:从项目面板拖拽资产进入 3D 视口时,在此处响应放置并完成实例化 - if (ImGui::BeginDragDropTarget()) { - (void)MetaCoreHandleProjectAssetDrop(*EditorContext_, std::nullopt); - ImGui::EndDragDropTarget(); - } - - // 8. 绘制 Gizmo (此时坐标和矩阵已完全对齐) - if (GMetaCoreEnableImGuizmo) { - SceneInteractionService_.HandleGizmoManipulation(*EditorContext_); - gizmoHovering = ImGuizmo::IsOver(); - gizmoUsing = ImGuizmo::IsUsing(); - - // View Cube - const float cameraDistance = glm::distance(sceneView.CameraPosition, sceneView.CameraTarget); - const ImVec2 viewCubePos = ImVec2( - viewportState.Left + viewportState.Width - 110.0F, - viewportState.Top + 20.0F - ); - - glm::mat4 cubeViewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp); - const glm::mat4 originalCubeViewMatrix = cubeViewMatrix; - - ImGuizmo::ViewManipulate( - glm::value_ptr(cubeViewMatrix), - cameraDistance, - viewCubePos, - ImVec2(80, 80), - 0x10101010 - ); - - // 如果 ViewCube 被操作,同步回相机控制器 - if (cubeViewMatrix != originalCubeViewMatrix) { - const glm::mat4 cubeCameraWorldMatrix = glm::inverse(cubeViewMatrix); - MetaCoreSceneView updatedView = sceneView; - updatedView.CameraPosition = glm::vec3(cubeCameraWorldMatrix[3]); - EditorContext_->GetCameraController().ApplySceneView(updatedView); - } - } - - // 绘制 Overlays - MetaCoreDrawViewportGridOverlay(*EditorContext_, viewportState); - MetaCoreDrawWorldOriginOverlay(*EditorContext_, sceneView, viewportState); - MetaCoreDrawSelectionBoundsOverlay(*EditorContext_, Scene_, sceneView, viewportState); - MetaCoreDrawSelectionHierarchyOverlay(*EditorContext_, Scene_, sceneView, viewportState); - - // 启用资产拖拽到 3D 场景视口的高亮响应遮罩,并显示操作提示 - MetaCoreDrawSceneViewportDropTarget(*EditorContext_, viewportPos, viewportSize); - - // 处理拾取 - if (viewportState.Hovered && !gizmoHovering && !gizmoUsing && - !ImGui::GetIO().WantCaptureMouse && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - const MetaCoreId pickedObjectId = SceneInteractionService_.PickGameObjectFromViewport( - Scene_, - sceneView, - viewportState, - EditorContext_->GetInput().GetCursorPosition() - ); - SceneInteractionService_.ApplyViewportSelection(*EditorContext_, pickedObjectId); - } - } - ImGui::End(); - ImGui::PopStyleVar(3); - - SceneInteractionService_.HandleGizmoEndUse(*EditorContext_, gizmoUsing); - SceneInteractionService_.DrawViewportToolbar(*EditorContext_); - // --- 补丁三重构结束 --- - } else { - SceneInteractionService_.ResetFrameState(); - ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + if (ImGui::Begin("\u573A\u666F###scene_view", &sceneOpen, sceneWindowFlags)) { + MetaCoreSceneViewportState& viewportState = EditorContext_->GetSceneViewportState(); + ImVec2 viewportPos = ImGui::GetCursorScreenPos(); + ImVec2 viewportSize = ImGui::GetContentRegionAvail(); + if (viewportSize.x < 1.0F) { + viewportSize.x = 1.0F; } + if (viewportSize.y < 1.0F) { + viewportSize.y = 1.0F; + } + + viewportState.Left = viewportPos.x; + viewportState.Top = viewportPos.y; + viewportState.Width = viewportSize.x; + viewportState.Height = viewportSize.y; + + const ImVec2 mousePosition = ImGui::GetMousePos(); + viewportState.Hovered = + mousePosition.x >= viewportState.Left && + mousePosition.x <= viewportState.Left + viewportState.Width && + mousePosition.y >= viewportState.Top && + mousePosition.y <= viewportState.Top + viewportState.Height; + viewportState.Focused = viewportState.Hovered && ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); + + if (viewportState.Hovered && + !ImGui::GetIO().WantCaptureKeyboard && + EditorContext_->GetInput().WasKeyPressed(MetaCoreInputKey::Focus)) { + if (MetaCoreGameObject selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject) { + EditorContext_->GetCameraController().FocusGameObject(selectedObject); + } + } + + ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{ + viewportState.Left, viewportState.Top, viewportState.Width, viewportState.Height + }); + + gizmoUsing = ImGuizmo::IsUsing(); + if (!gizmoUsing && viewportState.Hovered) { + EditorContext_->GetCameraController().Update(viewportState, EditorContext_->GetInput()); + } + + MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView(); + sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId(); + ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView); + + if (void* texPtr = ViewportRenderer_.GetFilamentTexturePointer(); texPtr != nullptr) { + ImGui::Image(texPtr, viewportSize, ImVec2(0, 1), ImVec2(1, 0)); + } else { + ImGui::Dummy(viewportSize); + } + + if (ImGui::BeginDragDropTarget()) { + (void)MetaCoreHandleProjectAssetDrop(*EditorContext_, std::nullopt); + ImGui::EndDragDropTarget(); + } + + bool gizmoHovering = false; + if (GMetaCoreEnableImGuizmo) { + SceneInteractionService_.HandleGizmoManipulation(*EditorContext_); + gizmoHovering = ImGuizmo::IsOver(); + gizmoUsing = ImGuizmo::IsUsing(); + + const float cameraDistance = glm::distance(sceneView.CameraPosition, sceneView.CameraTarget); + const ImVec2 viewCubePos = ImVec2( + viewportState.Left + viewportState.Width - 110.0F, + viewportState.Top + 20.0F + ); + + glm::mat4 cubeViewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp); + const glm::mat4 originalCubeViewMatrix = cubeViewMatrix; + + ImGuizmo::ViewManipulate( + glm::value_ptr(cubeViewMatrix), + cameraDistance, + viewCubePos, + ImVec2(80, 80), + 0x10101010 + ); + + if (cubeViewMatrix != originalCubeViewMatrix) { + const glm::mat4 cubeCameraWorldMatrix = glm::inverse(cubeViewMatrix); + MetaCoreSceneView updatedView = sceneView; + updatedView.CameraPosition = glm::vec3(cubeCameraWorldMatrix[3]); + EditorContext_->GetCameraController().ApplySceneView(updatedView); + } + } + + MetaCoreDrawViewportGridOverlay(*EditorContext_, viewportState); + MetaCoreDrawWorldOriginOverlay(*EditorContext_, sceneView, viewportState); + MetaCoreDrawSelectionBoundsOverlay(*EditorContext_, Scene_, sceneView, viewportState); + MetaCoreDrawSelectionHierarchyOverlay(*EditorContext_, Scene_, sceneView, viewportState); + MetaCoreDrawSceneViewportDropTarget(*EditorContext_, viewportPos, viewportSize); + + if (viewportState.Hovered && !gizmoHovering && !gizmoUsing && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + const MetaCoreId pickedObjectId = SceneInteractionService_.PickGameObjectFromViewport( + Scene_, + sceneView, + viewportState, + EditorContext_->GetInput().GetCursorPosition() + ); + SceneInteractionService_.ApplyViewportSelection(*EditorContext_, pickedObjectId); + } + drewSceneViewport = true; + } + ImGui::End(); + ImGui::PopStyleVar(); + + if (drewSceneViewport) { + SceneInteractionService_.HandleGizmoEndUse(*EditorContext_, gizmoUsing); } else { SceneInteractionService_.ResetFrameState(); ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); } } +void MetaCoreEditorApp::DrawGameViewWindow() { + constexpr ImGuiWindowFlags gameWindowFlags = + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + if (ImGui::Begin("\u6E38\u620F###game_view", nullptr, gameWindowFlags)) { + const ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 size = ImGui::GetContentRegionAvail(); + if (size.x < 1.0F) { + size.x = 1.0F; + } + if (size.y < 1.0F) { + size.y = 1.0F; + } + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(origin, ImVec2(origin.x + size.x, origin.y + size.y), IM_COL32(18, 19, 22, 255)); + const char* label = "Game View"; + const ImVec2 textSize = ImGui::CalcTextSize(label); + drawList->AddText( + ImVec2(origin.x + (size.x - textSize.x) * 0.5F, origin.y + (size.y - textSize.y) * 0.5F), + IM_COL32(170, 178, 190, 255), + label + ); + ImGui::Dummy(size); + } + ImGui::End(); + ImGui::PopStyleVar(); +} + +void MetaCoreEditorApp::DrawPlaceholderDockWindow(const char* label, const char* caption) { + constexpr ImGuiWindowFlags placeholderWindowFlags = + ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0F, 0.0F)); + if (ImGui::Begin(label, nullptr, placeholderWindowFlags)) { + const ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 size = ImGui::GetContentRegionAvail(); + if (size.x < 1.0F) { + size.x = 1.0F; + } + if (size.y < 1.0F) { + size.y = 1.0F; + } + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddRectFilled(origin, ImVec2(origin.x + size.x, origin.y + size.y), IM_COL32(24, 25, 28, 255)); + const ImVec2 textSize = ImGui::CalcTextSize(caption); + drawList->AddText( + ImVec2(origin.x + (size.x - textSize.x) * 0.5F, origin.y + (size.y - textSize.y) * 0.5F), + IM_COL32(145, 152, 164, 255), + caption + ); + ImGui::Dummy(size); + } + ImGui::End(); + ImGui::PopStyleVar(); +} + +void MetaCoreEditorApp::QueueDockTabSelection(std::string windowId) { + if (windowId.empty()) { + return; + } + if (std::find(PendingDockTabSelections_.begin(), PendingDockTabSelections_.end(), windowId) == PendingDockTabSelections_.end()) { + PendingDockTabSelections_.push_back(std::move(windowId)); + } +} + +void MetaCoreEditorApp::ApplyPendingDockTabSelections() { + if (PendingDockTabSelections_.empty()) { + return; + } + + std::vector pending; + pending.swap(PendingDockTabSelections_); + + for (const std::string& windowId : pending) { + const std::string imguiName = "###" + windowId; + ImGuiWindow* window = ImGui::FindWindowByName(imguiName.c_str()); + if (window == nullptr) { + PendingDockTabSelections_.push_back(windowId); + continue; + } + + ImGuiDockNode* dockNode = window->DockNode; + if (dockNode != nullptr) { + dockNode->SelectedTabId = window->TabId; + dockNode->VisibleWindow = window; + if (dockNode->TabBar != nullptr) { + dockNode->TabBar->SelectedTabId = window->TabId; + dockNode->TabBar->NextSelectedTabId = window->TabId; + dockNode->TabBar->VisibleTabId = window->TabId; + } + ImGui::MarkIniSettingsDirty(window); + } + + ImGui::FocusWindow(window); + } +} + void MetaCoreEditorApp::EnsureDefaultDockLayout(unsigned int dockSpaceId) { if (EditorContext_->HasDockLayoutBuilt()) { return; } + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + constexpr float statusBarHeight = 24.0F; + ImGui::DockBuilderRemoveNode(dockSpaceId); ImGui::DockBuilderAddNode(dockSpaceId, ImGuiDockNodeFlags_DockSpace); - ImGui::DockBuilderSetNodeSize(dockSpaceId, ImGui::GetMainViewport()->Size); + ImGui::DockBuilderSetNodePos(dockSpaceId, viewport->WorkPos); + ImGui::DockBuilderSetNodeSize( + dockSpaceId, + ImVec2(viewport->WorkSize.x, viewport->WorkSize.y - statusBarHeight) + ); - ImGuiID mainNodeId = dockSpaceId; - const ImGuiID leftNodeId = ImGui::DockBuilderSplitNode(mainNodeId, ImGuiDir_Left, 0.18F, nullptr, &mainNodeId); - const ImGuiID rightNodeId = ImGui::DockBuilderSplitNode(mainNodeId, ImGuiDir_Right, 0.24F, nullptr, &mainNodeId); - const ImGuiID bottomNodeId = ImGui::DockBuilderSplitNode(mainNodeId, ImGuiDir_Down, 0.28F, nullptr, &mainNodeId); + ImGuiID dockMain = 0; + ImGuiID dockRight = 0; + ImGui::DockBuilderSplitNode(dockSpaceId, ImGuiDir_Right, 0.25F, &dockRight, &dockMain); - ImGui::DockBuilderDockWindow("\u5C42\u7EA7", leftNodeId); - ImGui::DockBuilderDockWindow("\u68C0\u67E5\u5668", rightNodeId); - ImGui::DockBuilderDockWindow("\u9879\u76EE", bottomNodeId); - ImGui::DockBuilderDockWindow("\u63A7\u5236\u53F0", bottomNodeId); + ImGuiID dockTop = 0; + ImGuiID dockBottom = 0; + ImGui::DockBuilderSplitNode(dockMain, ImGuiDir_Down, 0.30F, &dockBottom, &dockTop); + + ImGuiID dockLeft = 0; + ImGuiID dockCenterTop = 0; + ImGui::DockBuilderSplitNode(dockTop, ImGuiDir_Left, 0.20F, &dockLeft, &dockCenterTop); + + ImGuiID dockToolbar = 0; + ImGuiID dockScene = 0; + ImGui::DockBuilderSplitNode(dockCenterTop, ImGuiDir_Up, 0.04F, &dockToolbar, &dockScene); + ImGui::DockBuilderSetNodeSize(dockToolbar, ImVec2(viewport->WorkSize.x, 36.0F)); + + if (ImGuiDockNode* toolbarNode = ImGui::DockBuilderGetNode(dockToolbar); toolbarNode != nullptr) { + toolbarNode->SetLocalFlags( + toolbarNode->LocalFlags | + ImGuiDockNodeFlags_NoTabBar | + ImGuiDockNodeFlags_NoDockingSplit | + ImGuiDockNodeFlags_NoResize | + ImGuiDockNodeFlags_NoUndocking + ); + } + + ImGui::DockBuilderDockWindow("###hierarchy", dockLeft); + ImGui::DockBuilderDockWindow("###inspector", dockRight); + ImGui::DockBuilderDockWindow("###toolbar", dockToolbar); + ImGui::DockBuilderDockWindow("###scene_view", dockScene); + ImGui::DockBuilderDockWindow("###game_view", dockScene); + ImGui::DockBuilderDockWindow("###ui_editor", dockScene); + ImGui::DockBuilderDockWindow("###animclip2d_editor", dockScene); + ImGui::DockBuilderDockWindow("###animfsm_editor", dockScene); + ImGui::DockBuilderDockWindow("###console", dockBottom); + ImGui::DockBuilderDockWindow("###project", dockBottom); ImGui::DockBuilderFinish(dockSpaceId); + QueueDockTabSelection("scene_view"); EditorContext_->SetDockLayoutBuilt(true); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index 9bafa02..a42fd50 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -11,6 +11,8 @@ #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" +#include "MetaCoreScene/MetaCoreComponents.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" #include #include @@ -159,15 +161,242 @@ private: return registry; } -[[nodiscard]] MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument() { - MetaCoreRuntimeProjectDocument document; - document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; - document.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; - document.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; - document.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; +[[nodiscard]] MetaCoreRuntimeProjectDocument MetaCoreLoadRuntimeProjectDocument( + const std::filesystem::path& runtimeDirectory, + const std::filesystem::path& runtimeDirectoryRelative = std::filesystem::path("Runtime") +) { + const MetaCoreTypeRegistry runtimeRegistry = MetaCoreBuildRuntimeDataTypeRegistry(); + MetaCoreRuntimeProjectDocument document = MetaCoreReadRuntimeProjectDocument( + runtimeDirectory / "ProjectRuntime.mcruntimecfg", + runtimeRegistry + ).value_or(MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative)); + MetaCoreApplyRuntimeProjectDefaults(document, runtimeDirectoryRelative); return document; } +[[nodiscard]] MetaCoreTypeRegistry MetaCoreBuildUiDocumentTypeRegistry() { + MetaCoreTypeRegistry registry; + MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCoreRegisterSceneGeneratedTypes(registry); + return registry; +} + +[[nodiscard]] std::filesystem::path MetaCoreResolveProjectPath( + const std::filesystem::path& projectRoot, + const std::filesystem::path& path +) { + return path.is_absolute() ? path : (projectRoot / path).lexically_normal(); +} + +[[nodiscard]] const std::string* MetaCoreFindRuntimeDataSourceSetting( + const MetaCoreDataSourceDefinition& sourceDefinition, + std::string_view key +) { + const auto iterator = std::find_if( + sourceDefinition.ConnectionSettings.begin(), + sourceDefinition.ConnectionSettings.end(), + [key](const MetaCoreDataSourceSetting& setting) { + return setting.Key == key; + } + ); + return iterator == sourceDefinition.ConnectionSettings.end() ? nullptr : &iterator->Value; +} + +[[nodiscard]] std::vector MetaCoreValidateRuntimeDataReplayFilePaths( + const MetaCoreProjectDescriptor& project, + const MetaCoreRuntimeDataSourcesDocument& sourcesDocument +) { + std::vector issues; + for (const MetaCoreDataSourceDefinition& sourceDefinition : sourcesDocument.Sources) { + if (sourceDefinition.AdapterType != "file_replay") { + continue; + } + + const std::string* replayFilePath = MetaCoreFindRuntimeDataSourceSetting(sourceDefinition, "file_path"); + if (replayFilePath == nullptr || replayFilePath->empty()) { + continue; + } + + const std::filesystem::path relativeReplayPath(*replayFilePath); + if (MetaCoreIsUnsafeRuntimeProjectPath(relativeReplayPath)) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + sourceDefinition.Id, + "file_replay file_path must be a project-relative path without parent traversal" + }); + continue; + } + + if (!relativeReplayPath.is_absolute()) { + std::error_code errorCode; + if (!std::filesystem::exists(project.RootPath / relativeReplayPath.lexically_normal(), errorCode)) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Warning, + sourceDefinition.Id, + "file_replay file_path does not exist yet: " + relativeReplayPath.generic_string() + }); + } + } + } + return issues; +} + +[[nodiscard]] const char* MetaCoreRequiredComponentForRuntimeBindingTarget(MetaCoreRuntimeBindingTarget target) { + switch (target) { + case MetaCoreRuntimeBindingTarget::TransformPosition: + return "Transform"; + case MetaCoreRuntimeBindingTarget::MeshRendererVisible: + case MetaCoreRuntimeBindingTarget::MeshRendererBaseColor: + return "MeshRenderer"; + case MetaCoreRuntimeBindingTarget::LightIntensity: + case MetaCoreRuntimeBindingTarget::LightColor: + return "Light"; + } + return "Component"; +} + +[[nodiscard]] bool MetaCoreRuntimeBindingTargetHasRequiredComponent( + const MetaCoreGameObject& gameObject, + MetaCoreRuntimeBindingTarget target +) { + switch (target) { + case MetaCoreRuntimeBindingTarget::TransformPosition: + return gameObject.HasComponent(); + case MetaCoreRuntimeBindingTarget::MeshRendererVisible: + case MetaCoreRuntimeBindingTarget::MeshRendererBaseColor: + return gameObject.HasComponent(); + case MetaCoreRuntimeBindingTarget::LightIntensity: + case MetaCoreRuntimeBindingTarget::LightColor: + return gameObject.HasComponent(); + } + return false; +} + +[[nodiscard]] std::vector MetaCoreValidateRuntimeSceneBindingsAgainstScene( + const MetaCoreScene& scene, + const MetaCoreRuntimeBindingsDocument& bindingsDocument +) { + std::vector issues; + for (const MetaCoreSceneBindingDefinition& binding : bindingsDocument.Bindings) { + if (binding.TargetObjectId == 0) { + continue; + } + + const MetaCoreGameObject targetObject = scene.FindGameObject(binding.TargetObjectId); + if (!targetObject) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + binding.BindingId, + "Target scene object does not exist: " + std::to_string(binding.TargetObjectId) + }); + continue; + } + + if (!MetaCoreRuntimeBindingTargetHasRequiredComponent(targetObject, binding.Target)) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + binding.BindingId, + "Target scene object does not have required component: " + + std::string(MetaCoreRequiredComponentForRuntimeBindingTarget(binding.Target)) + }); + } + } + return issues; +} + +[[nodiscard]] bool MetaCoreRuntimeDataHasUiBindings( + const MetaCoreRuntimeBindingsDocument& bindingsDocument +) { + return std::any_of( + bindingsDocument.UiBindings.begin(), + bindingsDocument.UiBindings.end(), + [](const MetaCoreUiBindingDefinition& binding) { + return binding.Target == MetaCoreRuntimeUiBindingTarget::Text; + } + ); +} + +[[nodiscard]] std::vector MetaCoreValidateRuntimeUiBindingsAgainstStartupUi( + const MetaCoreProjectDescriptor& project, + const MetaCoreRuntimeProjectDocument& runtimeProjectDocument, + const MetaCoreRuntimeBindingsDocument& bindingsDocument +) { + std::vector issues; + if (!MetaCoreRuntimeDataHasUiBindings(bindingsDocument)) { + return issues; + } + + if (runtimeProjectDocument.StartupUiPath.empty()) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeProject", + "Startup UI is required when RuntimeData UI bindings are configured" + }); + return issues; + } + if (MetaCoreIsUnsafeRuntimeProjectPath(runtimeProjectDocument.StartupUiPath)) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeProject", + "Startup UI must be a project-relative path without parent traversal" + }); + return issues; + } + + const std::filesystem::path startupUiPath = + MetaCoreResolveProjectPath(project.RootPath, runtimeProjectDocument.StartupUiPath); + if (!std::filesystem::exists(startupUiPath)) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeProject", + "Startup UI does not exist: " + runtimeProjectDocument.StartupUiPath.generic_string() + }); + return issues; + } + + const MetaCoreTypeRegistry uiRegistry = MetaCoreBuildUiDocumentTypeRegistry(); + const auto startupUiDocument = MetaCoreSceneSerializer::LoadUiFromJson(startupUiPath, uiRegistry); + if (!startupUiDocument.has_value()) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeProject", + "Startup UI is unreadable: " + runtimeProjectDocument.StartupUiPath.generic_string() + }); + return issues; + } + + for (const MetaCoreUiBindingDefinition& binding : bindingsDocument.UiBindings) { + if (binding.Target != MetaCoreRuntimeUiBindingTarget::Text) { + continue; + } + + const auto nodeIterator = std::find_if( + startupUiDocument->Nodes.begin(), + startupUiDocument->Nodes.end(), + [&](const MetaCoreUiNodeDocument& node) { + return node.Id == binding.TargetNodeId; + } + ); + if (nodeIterator == startupUiDocument->Nodes.end()) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + binding.BindingId, + "Target UI node does not exist in Startup UI: " + binding.TargetNodeId + }); + continue; + } + if (nodeIterator->Type != MetaCoreUiNodeType::Text) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + binding.BindingId, + "Target UI node is not a Text node: " + binding.TargetNodeId + }); + } + } + + return issues; +} + } // namespace MetaCoreEditorContext::MetaCoreEditorContext( @@ -339,6 +568,9 @@ MetaCoreGizmoOperation MetaCoreEditorContext::GetGizmoOperation() const { return void MetaCoreEditorContext::SetGizmoOperation(MetaCoreGizmoOperation operation) { GizmoOperation_ = operation; } MetaCoreGizmoMode MetaCoreEditorContext::GetGizmoMode() const { return GizmoMode_; } void MetaCoreEditorContext::SetGizmoMode(MetaCoreGizmoMode mode) { GizmoMode_ = mode; } +const MetaCoreGizmoSnapSettings& MetaCoreEditorContext::GetGizmoSnapSettings() const { return GizmoSnapSettings_; } +void MetaCoreEditorContext::SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings) { GizmoSnapSettings_ = settings; } +void MetaCoreEditorContext::SetGizmoSnapEnabled(bool enabled) { GizmoSnapSettings_.Enabled = enabled; } bool MetaCoreEditorContext::GetShowViewportGrid() const { return ShowViewportGrid_; } void MetaCoreEditorContext::SetShowViewportGrid(bool show) { ShowViewportGrid_ = show; } bool MetaCoreEditorContext::GetShowWorldOrigin() const { return ShowWorldOrigin_; } @@ -460,7 +692,10 @@ bool MetaCoreEditorContext::EnsureRuntimeDataConfigLoaded() { return false; } + const MetaCoreProjectDescriptor& project = assetDatabaseService->GetProjectDescriptor(); const std::filesystem::path runtimeDirectory = ResolveRuntimeDirectory(); + const std::filesystem::path runtimeDirectoryRelative = + MetaCoreBuildRuntimeDirectoryRelativePath(project.RootPath, runtimeDirectory); std::error_code errorCode; std::filesystem::create_directories(runtimeDirectory, errorCode); @@ -469,12 +704,29 @@ bool MetaCoreEditorContext::EnsureRuntimeDataConfigLoaded() { runtimeDirectory / "ProjectRuntime.mcruntimecfg", runtimeRegistry ); + MetaCoreRuntimeProjectDocument runtimeProjectDocument = + loadedProjectRuntime.value_or(MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative)); + MetaCoreApplyRuntimeProjectDefaults(runtimeProjectDocument, runtimeDirectoryRelative); + const auto runtimeProjectPathIssues = MetaCoreValidateRuntimeProjectPaths(runtimeProjectDocument); + const bool hasRuntimeProjectPathErrors = std::any_of( + runtimeProjectPathIssues.begin(), + runtimeProjectPathIssues.end(), + [](const MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error; + } + ); + for (const MetaCoreRuntimeConfigIssue& issue : runtimeProjectPathIssues) { + AddConsoleMessage(MetaCoreLogLevel::Error, "RuntimeData", issue.Scope + ": " + issue.Message); + } + if (hasRuntimeProjectPathErrors) { + return false; + } const auto loadedSources = MetaCoreReadRuntimeDataSourcesDocument( - runtimeDirectory / "DataSources.mcruntime", + MetaCoreResolveProjectPath(project.RootPath, runtimeProjectDocument.DataSourcesPath), runtimeRegistry ); const auto loadedBindings = MetaCoreReadRuntimeBindingsDocument( - runtimeDirectory / "Bindings.mcruntime", + MetaCoreResolveProjectPath(project.RootPath, runtimeProjectDocument.BindingsPath), runtimeRegistry ); @@ -491,6 +743,99 @@ bool MetaCoreEditorContext::EnsureRuntimeDataConfigLoaded() { return true; } +std::vector MetaCoreEditorContext::ValidateRuntimeDataConfig() { + std::vector validationIssues; + if (!EnsureRuntimeDataConfigLoaded()) { + validationIssues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeData", + "RuntimeData config is not available" + }); + return validationIssues; + } + + const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + validationIssues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeData", + "Project is not available" + }); + return validationIssues; + } + + const std::filesystem::path runtimeDirectory = ResolveRuntimeDirectory(); + const std::filesystem::path runtimeDirectoryRelative = MetaCoreBuildRuntimeDirectoryRelativePath( + assetDatabaseService->GetProjectDescriptor().RootPath, + runtimeDirectory + ); + MetaCoreRuntimeProjectDocument runtimeProjectDocument = + MetaCoreLoadRuntimeProjectDocument(runtimeDirectory, runtimeDirectoryRelative); + validationIssues = MetaCoreValidateRuntimeDataDocuments(RuntimeDataSourcesDocument_, RuntimeBindingsDocument_); + + auto runtimeProjectPathIssues = MetaCoreValidateRuntimeProjectPaths(runtimeProjectDocument); + validationIssues.insert( + validationIssues.end(), + std::make_move_iterator(runtimeProjectPathIssues.begin()), + std::make_move_iterator(runtimeProjectPathIssues.end()) + ); + + auto replayPathIssues = MetaCoreValidateRuntimeDataReplayFilePaths( + assetDatabaseService->GetProjectDescriptor(), + RuntimeDataSourcesDocument_ + ); + validationIssues.insert( + validationIssues.end(), + std::make_move_iterator(replayPathIssues.begin()), + std::make_move_iterator(replayPathIssues.end()) + ); + + auto sceneBindingIssues = MetaCoreValidateRuntimeSceneBindingsAgainstScene(Scene_, RuntimeBindingsDocument_); + validationIssues.insert( + validationIssues.end(), + std::make_move_iterator(sceneBindingIssues.begin()), + std::make_move_iterator(sceneBindingIssues.end()) + ); + + auto uiValidationIssues = MetaCoreValidateRuntimeUiBindingsAgainstStartupUi( + assetDatabaseService->GetProjectDescriptor(), + runtimeProjectDocument, + RuntimeBindingsDocument_ + ); + validationIssues.insert( + validationIssues.end(), + std::make_move_iterator(uiValidationIssues.begin()), + std::make_move_iterator(uiValidationIssues.end()) + ); + + return validationIssues; +} + +std::optional MetaCoreEditorContext::LoadRuntimeDiagnosticsSnapshot() const { + const auto assetDatabaseService = ModuleRegistry_.ResolveService(); + if (assetDatabaseService == nullptr || !assetDatabaseService->HasProject()) { + return std::nullopt; + } + + const std::filesystem::path runtimeDirectory = ResolveRuntimeDirectory(); + const std::filesystem::path runtimeDirectoryRelative = MetaCoreBuildRuntimeDirectoryRelativePath( + assetDatabaseService->GetProjectDescriptor().RootPath, + runtimeDirectory + ); + const MetaCoreRuntimeProjectDocument runtimeProjectDocument = + MetaCoreLoadRuntimeProjectDocument(runtimeDirectory, runtimeDirectoryRelative); + if (MetaCoreIsUnsafeRuntimeProjectPath(runtimeProjectDocument.DiagnosticsPath)) { + return std::nullopt; + } + + const MetaCoreTypeRegistry runtimeRegistry = MetaCoreBuildRuntimeDataTypeRegistry(); + const std::filesystem::path diagnosticsPath = MetaCoreResolveProjectPath( + assetDatabaseService->GetProjectDescriptor().RootPath, + runtimeProjectDocument.DiagnosticsPath + ); + return MetaCoreReadRuntimeDiagnosticsSnapshot(diagnosticsPath, runtimeRegistry); +} + bool MetaCoreEditorContext::SaveRuntimeDataConfig() { if (!EnsureRuntimeDataConfigLoaded()) { return false; @@ -506,7 +851,13 @@ bool MetaCoreEditorContext::SaveRuntimeDataConfig() { std::error_code errorCode; std::filesystem::create_directories(runtimeDirectory, errorCode); - const auto validationIssues = MetaCoreValidateRuntimeDataDocuments(RuntimeDataSourcesDocument_, RuntimeBindingsDocument_); + const std::filesystem::path runtimeDirectoryRelative = MetaCoreBuildRuntimeDirectoryRelativePath( + assetDatabaseService->GetProjectDescriptor().RootPath, + runtimeDirectory + ); + MetaCoreRuntimeProjectDocument runtimeProjectDocument = + MetaCoreLoadRuntimeProjectDocument(runtimeDirectory, runtimeDirectoryRelative); + auto validationIssues = ValidateRuntimeDataConfig(); const bool hasValidationErrors = std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCoreRuntimeConfigIssue& issue) { return issue.Severity == MetaCoreRuntimeConfigIssueSeverity::Error; }); @@ -523,19 +874,28 @@ bool MetaCoreEditorContext::SaveRuntimeDataConfig() { } const MetaCoreTypeRegistry runtimeRegistry = MetaCoreBuildRuntimeDataTypeRegistry(); - const MetaCoreRuntimeProjectDocument runtimeProjectDocument = MetaCoreBuildDefaultRuntimeProjectDocument(); const bool wroteProjectRuntime = MetaCoreWriteRuntimeProjectDocument( runtimeDirectory / "ProjectRuntime.mcruntimecfg", runtimeProjectDocument, runtimeRegistry ); + const std::filesystem::path sourcesPath = MetaCoreResolveProjectPath( + assetDatabaseService->GetProjectDescriptor().RootPath, + runtimeProjectDocument.DataSourcesPath + ); + const std::filesystem::path bindingsPath = MetaCoreResolveProjectPath( + assetDatabaseService->GetProjectDescriptor().RootPath, + runtimeProjectDocument.BindingsPath + ); + std::filesystem::create_directories(sourcesPath.parent_path(), errorCode); + std::filesystem::create_directories(bindingsPath.parent_path(), errorCode); const bool wroteSources = MetaCoreWriteRuntimeDataSourcesDocument( - runtimeDirectory / "DataSources.mcruntime", + sourcesPath, RuntimeDataSourcesDocument_, runtimeRegistry ); const bool wroteBindings = MetaCoreWriteRuntimeBindingsDocument( - runtimeDirectory / "Bindings.mcruntime", + bindingsPath, RuntimeBindingsDocument_, runtimeRegistry ); @@ -646,7 +1006,8 @@ void MetaCoreEditorContext::RefreshSceneDirtyState() { std::filesystem::path MetaCoreEditorContext::ResolveRuntimeDirectory() const { if (const auto assetDatabaseService = ModuleRegistry_.ResolveService(); assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { - return assetDatabaseService->GetProjectDescriptor().RootPath / "Runtime"; + const MetaCoreProjectDescriptor& project = assetDatabaseService->GetProjectDescriptor(); + return !project.RuntimePath.empty() ? project.RuntimePath : (project.RootPath / "Runtime"); } return std::filesystem::current_path() / "Runtime"; } diff --git a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp index 83a7487..15bfaa4 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace MetaCore { namespace { @@ -51,6 +52,55 @@ glm::mat4 MetaCoreBuildWorldTransformMatrix(const MetaCoreScene& scene, MetaCore // 使用 MetaCoreTransformUtils.h 中的 MetaCoreApplyMatrixToTransform +std::vector MetaCoreBuildSelectionTransformRoots( + const MetaCoreScene& scene, + const std::vector& selectedObjectIds, + MetaCoreId manipulatedObjectId +) { + std::vector candidateIds; + candidateIds.reserve(selectedObjectIds.empty() ? 1 : selectedObjectIds.size()); + + const bool manipulatedIsSelected = + std::find(selectedObjectIds.begin(), selectedObjectIds.end(), manipulatedObjectId) != selectedObjectIds.end(); + if (manipulatedIsSelected) { + candidateIds = selectedObjectIds; + } else if (manipulatedObjectId != 0) { + candidateIds.push_back(manipulatedObjectId); + } + + std::unordered_set candidateSet(candidateIds.begin(), candidateIds.end()); + std::vector rootIds; + rootIds.reserve(candidateIds.size()); + + for (MetaCoreId objectId : candidateIds) { + if (objectId == 0 || !scene.FindGameObject(objectId)) { + continue; + } + + bool hasSelectedAncestor = false; + MetaCoreGameObject object = scene.FindGameObject(objectId); + MetaCoreId parentId = object ? object.GetParentId() : 0; + while (parentId != 0) { + if (candidateSet.contains(parentId)) { + hasSelectedAncestor = true; + break; + } + + const MetaCoreGameObject parentObject = scene.FindGameObject(parentId); + if (!parentObject) { + break; + } + parentId = parentObject.GetParentId(); + } + + if (!hasSelectedAncestor) { + rootIds.push_back(objectId); + } + } + + return rootIds; +} + ImGuizmo::OPERATION MetaCoreToImGuizmoOperation(MetaCoreGizmoOperation operation) { switch (operation) { case MetaCoreGizmoOperation::Translate: return ImGuizmo::TRANSLATE; @@ -346,6 +396,57 @@ void MetaCoreSceneInteractionService::FocusVisibleScene(MetaCoreEditorContext& e editorContext.GetCameraController().FocusBounds(center, radius); } +bool MetaCoreSceneInteractionService::ApplyWorldTransformDeltaToSelection( + MetaCoreEditorContext& editorContext, + MetaCoreId manipulatedObjectId, + const glm::mat4& originalWorldMatrix, + const glm::mat4& newWorldMatrix +) const { + if (manipulatedObjectId == 0 || !editorContext.GetScene().FindGameObject(manipulatedObjectId)) { + return false; + } + + if (newWorldMatrix == originalWorldMatrix) { + return false; + } + + const glm::mat4 deltaMatrix = newWorldMatrix * glm::inverse(originalWorldMatrix); + const std::vector rootIds = MetaCoreBuildSelectionTransformRoots( + editorContext.GetScene(), + editorContext.GetSelectedObjectIds(), + manipulatedObjectId + ); + if (rootIds.empty()) { + return false; + } + + bool changed = false; + for (MetaCoreId objectId : rootIds) { + MetaCoreGameObject object = editorContext.GetScene().FindGameObject(objectId); + if (!object) { + continue; + } + + const glm::mat4 currentWorldMatrix = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), objectId); + const glm::mat4 targetWorldMatrix = + objectId == manipulatedObjectId ? newWorldMatrix : deltaMatrix * currentWorldMatrix; + + glm::mat4 targetLocalMatrix = targetWorldMatrix; + if (object.GetParentId() != 0) { + const glm::mat4 parentWorldMatrix = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), object.GetParentId()); + targetLocalMatrix = glm::inverse(parentWorldMatrix) * targetWorldMatrix; + } + + MetaCoreApplyMatrixToTransform(targetLocalMatrix, object.GetComponent()); + changed = true; + } + + if (changed) { + editorContext.GetScene().IncrementRevision(); + } + return changed; +} + void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorContext& editorContext) { if (editorContext.GetGizmoOperation() == MetaCoreGizmoOperation::None) { return; @@ -391,28 +492,31 @@ void MetaCoreSceneInteractionService::HandleGizmoManipulation(MetaCoreEditorCont ImGuizmo::PushID(reinterpret_cast(selectedObject.GetId())); glm::mat4 gizmoMatrix = worldMatrixMetaCore; + const MetaCoreGizmoSnapSettings& snapSettings = editorContext.GetGizmoSnapSettings(); + const float snapStep = snapSettings.StepForOperation(currentOp); + const float snapValues[3] = {snapStep, snapStep, snapStep}; ImGuizmo::Manipulate( glm::value_ptr(gizmoViewMatrix), glm::value_ptr(gizmoProjectionMatrix), MetaCoreToImGuizmoOperation(currentOp), effectiveMode, - glm::value_ptr(gizmoMatrix) + glm::value_ptr(gizmoMatrix), + nullptr, + snapSettings.Enabled ? snapValues : nullptr ); const bool gizmoUsing = ImGuizmo::IsUsing(); ImGuizmo::PopID(); - if (gizmoUsing && gizmoMatrix != originalWorldMatrixMetaCore) { - glm::mat4 newLocalMatrix = gizmoMatrix; - if (selectedObject.GetParentId() != 0) { - const glm::mat4 parentWorldMatrixMetaCore = MetaCoreBuildWorldTransformMatrix(editorContext.GetScene(), selectedObject.GetParentId()); - newLocalMatrix = glm::inverse(parentWorldMatrixMetaCore) * gizmoMatrix; - } - - MetaCoreApplyMatrixToTransform(newLocalMatrix, selectedObject.GetComponent()); - } - HandleGizmoBeginUse(editorContext, gizmoUsing); + if (gizmoUsing && gizmoMatrix != originalWorldMatrixMetaCore) { + (void)ApplyWorldTransformDeltaToSelection( + editorContext, + selectedObject.GetId(), + originalWorldMatrixMetaCore, + gizmoMatrix + ); + } HandleGizmoEndUse(editorContext, gizmoUsing); } @@ -452,6 +556,31 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& return clicked; }; + if (const auto playModeService = editorContext.GetModuleRegistry().ResolveService(); + playModeService != nullptr) { + const MetaCorePlayModeState playState = playModeService->GetState(); + if (playState == MetaCorePlayModeState::Edit) { + if (drawToolbarToggle(" Play ", false)) { + (void)playModeService->EnterPlayMode(editorContext); + } + } else { + if (drawToolbarToggle(" Stop ", true)) { + (void)playModeService->ExitPlayMode(editorContext); + } + ImGui::SameLine(0, 2); + if (playState == MetaCorePlayModeState::Playing) { + if (drawToolbarToggle(" Pause ", false)) { + (void)playModeService->PausePlayMode(editorContext); + } + } else { + if (drawToolbarToggle(" Resume ", true)) { + (void)playModeService->ResumePlayMode(editorContext); + } + } + } + ImGui::SameLine(0, 12); + } + // Tools (Unity-Style Icons/Labels) if (drawToolbarToggle(" 视图 ", editorContext.GetGizmoOperation() == MetaCoreGizmoOperation::None)) { editorContext.SetGizmoOperation(MetaCoreGizmoOperation::None); @@ -480,6 +609,12 @@ void MetaCoreSceneInteractionService::DrawViewportToolbar(MetaCoreEditorContext& editorContext.SetGizmoMode(MetaCoreGizmoMode::Global); } + ImGui::SameLine(0, 12); + const MetaCoreGizmoSnapSettings& snapSettings = editorContext.GetGizmoSnapSettings(); + if (drawToolbarToggle(" Snap ", snapSettings.Enabled)) { + editorContext.SetGizmoSnapEnabled(!snapSettings.Enabled); + } + ImGui::SameLine(0, 12); const bool hasSelection = (bool)editorContext.GetSelectedGameObject(); if (!hasSelection) { @@ -565,6 +700,10 @@ void MetaCoreSceneInteractionService::HandleShortcuts(MetaCoreEditorContext& edi } } + if (ImGui::IsKeyPressed(ImGuiKey_X)) { + editorContext.SetGizmoSnapEnabled(!editorContext.GetGizmoSnapSettings().Enabled); + } + if (ImGui::IsKeyPressed(ImGuiKey_F) && (bool)editorContext.GetSelectedGameObject()) { FocusSelectedObject(editorContext); } diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h index 723169a..bf9858f 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h @@ -10,7 +10,9 @@ #include "MetaCoreRender/MetaCoreRenderDevice.h" #include "MetaCoreScene/MetaCoreScene.h" +#include #include +#include #include namespace MetaCore { @@ -30,7 +32,15 @@ private: bool InitializeImGui(); void ShutdownImGui(); void DrawEditorFrame(); + void DrawEditorToolbar(); + void DrawEditorStatusBar(); + void DrawSceneViewWindow(); + void DrawGameViewWindow(); + void DrawPlaceholderDockWindow(const char* label, const char* caption); + void SyncImGuiLayoutIniPath(); void EnsureDefaultDockLayout(unsigned int dockSpaceId); + void QueueDockTabSelection(std::string windowId); + void ApplyPendingDockTabSelections(); MetaCoreWindow Window_{}; MetaCoreRenderDevice RenderDevice_{}; @@ -40,7 +50,10 @@ private: MetaCoreEditorModuleRegistry ModuleRegistry_{}; MetaCoreSceneInteractionService SceneInteractionService_{}; std::vector> Modules_{}; + std::vector PendingDockTabSelections_{}; std::unique_ptr EditorContext_{}; + std::filesystem::path ImGuiLayoutProjectRoot_{}; + std::string ImGuiIniPath_{}; bool Initialized_ = false; }; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h index 1c8fd44..5e5ef5a 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorAssetTypes.h @@ -74,33 +74,4 @@ struct MetaCoreImportedAssetDocument { std::uint64_t SourceHash = 0; }; - -MC_STRUCT() -struct MetaCoreCookManifestEntry { - MC_GENERATED_BODY() - - MC_PROPERTY() - MetaCoreAssetGuid AssetGuid{}; - - MC_PROPERTY() - std::filesystem::path SourcePackagePath{}; - - MC_PROPERTY() - std::filesystem::path CookedPath{}; - - MC_PROPERTY() - std::uint64_t SourceHash = 0; - - MC_PROPERTY() - std::uint64_t CookedKey = 0; -}; - -MC_STRUCT() -struct MetaCoreCookManifestDocument { - MC_GENERATED_BODY() - - MC_PROPERTY() - std::vector Entries{}; -}; - } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h index c5a1989..406012d 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -42,6 +43,25 @@ enum class MetaCoreReparentTransformRule { KeepLocal }; +struct MetaCoreGizmoSnapSettings { + bool Enabled = false; + float TranslationStep = 0.5F; + float RotationStepDegrees = 15.0F; + float ScaleStep = 0.1F; + + [[nodiscard]] float StepForOperation(MetaCoreGizmoOperation operation) const { + switch (operation) { + case MetaCoreGizmoOperation::Rotate: + return RotationStepDegrees; + case MetaCoreGizmoOperation::Scale: + return ScaleStep; + case MetaCoreGizmoOperation::Translate: + default: + return TranslationStep; + } + } +}; + MC_STRUCT() struct MetaCoreEditorStateSnapshot { MC_GENERATED_BODY() @@ -140,6 +160,9 @@ public: void SetGizmoOperation(MetaCoreGizmoOperation operation); [[nodiscard]] MetaCoreGizmoMode GetGizmoMode() const; void SetGizmoMode(MetaCoreGizmoMode mode); + [[nodiscard]] const MetaCoreGizmoSnapSettings& GetGizmoSnapSettings() const; + void SetGizmoSnapSettings(const MetaCoreGizmoSnapSettings& settings); + void SetGizmoSnapEnabled(bool enabled); [[nodiscard]] bool GetShowViewportGrid() const; void SetShowViewportGrid(bool show); [[nodiscard]] bool GetShowWorldOrigin() const; @@ -164,6 +187,8 @@ public: [[nodiscard]] MetaCoreId ConsumeRenameRequestObjectId(); void AddConsoleMessage(MetaCoreLogLevel level, const std::string& category, const std::string& message); [[nodiscard]] bool EnsureRuntimeDataConfigLoaded(); + [[nodiscard]] std::vector ValidateRuntimeDataConfig(); + [[nodiscard]] std::optional LoadRuntimeDiagnosticsSnapshot() const; [[nodiscard]] bool SaveRuntimeDataConfig(); [[nodiscard]] MetaCoreRuntimeDataSourcesDocument& AccessRuntimeDataSourcesDocument(); [[nodiscard]] MetaCoreRuntimeBindingsDocument& AccessRuntimeBindingsDocument(); @@ -200,6 +225,7 @@ private: MetaCoreId SelectionAnchorId_ = 0; MetaCoreGizmoOperation GizmoOperation_ = MetaCoreGizmoOperation::Translate; MetaCoreGizmoMode GizmoMode_ = MetaCoreGizmoMode::Local; + MetaCoreGizmoSnapSettings GizmoSnapSettings_{}; bool ShowViewportGrid_ = true; bool ShowWorldOrigin_ = true; MetaCoreReparentTransformRule ReparentTransformRule_ = MetaCoreReparentTransformRule::KeepWorld; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h index c898b4d..f5f5637 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorServices.h @@ -144,6 +144,7 @@ public: [[nodiscard]] virtual std::vector GetDirectoriesUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::vector GetAssetsUnder(const std::filesystem::path& relativeDirectory) const = 0; [[nodiscard]] virtual std::optional FindAssetByRelativePath(const std::filesystem::path& relativePath) const = 0; + [[nodiscard]] virtual bool ReimportAsset(const MetaCoreAssetGuid& assetGuid) = 0; virtual bool Refresh() = 0; virtual bool CreateFolder(const std::filesystem::path& relativeDirectory) = 0; virtual bool RegisterScenePath(const std::filesystem::path& relativeScenePath, bool makeStartupScene) = 0; @@ -190,6 +191,11 @@ public: const MetaCoreMaterialAssetDocument& document ) = 0; + [[nodiscard]] virtual bool ApplyMaterialAssetPreviewToScene( + MetaCoreEditorContext& editorContext, + const MetaCoreAssetGuid& materialGuid + ) const = 0; + [[nodiscard]] virtual std::optional ResolveGeneratedAsset( const MetaCoreAssetGuid& assetGuid ) const = 0; @@ -202,6 +208,42 @@ public: [[nodiscard]] virtual std::filesystem::path GetCookedPathForAsset(const MetaCoreAssetGuid& assetGuid) const = 0; }; +struct MetaCoreBuildPlayerPackageRequest { + std::filesystem::path PlayerExecutablePath{}; + std::filesystem::path OutputDirectory{}; + bool CookBeforePackage = true; + bool CopyLooseProjectContent = true; + bool CopyRuntimeConfig = true; + bool UseCookedAssetsInPackage = false; +}; + +struct MetaCoreBuildDependencyReportEntry { + MetaCoreAssetGuid AssetGuid{}; + MetaCoreAssetGuid ReferencedBy{}; + std::string Reason{}; + std::string AssetType{}; + std::filesystem::path AssetPath{}; + std::filesystem::path CookedPath{}; + std::string Status{}; + std::string Message{}; +}; + +struct MetaCoreBuildPlayerPackageResult { + bool Success = false; + std::filesystem::path OutputRoot{}; + std::vector CopiedFiles{}; + std::vector CookedAssets{}; + std::vector DependencyReport{}; + std::string Error{}; +}; + +class MetaCoreIBuildService : public MetaCoreIEditorService { +public: + [[nodiscard]] virtual MetaCoreBuildPlayerPackageResult BuildPlayerPackage( + const MetaCoreBuildPlayerPackageRequest& request + ) = 0; +}; + class MetaCoreIScenePersistenceService : public MetaCoreIEditorService { public: [[nodiscard]] virtual bool HasOpenScene() const = 0; @@ -272,13 +314,26 @@ public: ) = 0; [[nodiscard]] virtual bool ApplySelectedPrefabInstance(MetaCoreEditorContext& editorContext) = 0; [[nodiscard]] virtual bool RevertSelectedPrefabInstance(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool BreakSelectedPrefabInstance(MetaCoreEditorContext& editorContext) = 0; }; class MetaCoreIComponentTypeRegistry : public MetaCoreIEditorService { public: + struct MetaCoreComponentLifecycle { + std::function OnStart{}; + std::function OnUpdate{}; + std::function OnDestroy{}; + std::function OnDrawGizmos{}; + std::function OnDrawGizmosSelected{}; + }; + struct MetaCoreComponentDescriptor { std::string TypeId{}; std::string DisplayName{}; + std::string Category{}; + const MetaCoreStructDescriptor* ReflectedType = nullptr; + std::function MutableComponent{}; + std::function ConstComponent{}; std::function HasComponent{}; std::function AddComponent{}; std::function RemoveComponent{}; @@ -286,11 +341,13 @@ public: std::function>(const MetaCoreGameObject&, const MetaCoreTypeRegistry&)> CopyComponentPayload{}; std::function, const MetaCoreTypeRegistry&)> PasteComponentPayload{}; std::function DrawInspector{}; + MetaCoreComponentLifecycle Lifecycle{}; }; [[nodiscard]] virtual std::vector GetRegisteredComponentTypeIds() const = 0; [[nodiscard]] virtual const std::vector& GetComponentDescriptors() const = 0; [[nodiscard]] virtual const MetaCoreComponentDescriptor* FindDescriptor(std::string_view typeId) const = 0; + [[nodiscard]] virtual bool RegisterComponentDescriptor(MetaCoreComponentDescriptor descriptor) = 0; [[nodiscard]] virtual bool CopyComponent(std::string_view typeId, const MetaCoreGameObject& gameObject) = 0; [[nodiscard]] virtual bool CanPasteComponent(std::string_view typeId) const = 0; [[nodiscard]] virtual bool PasteComponent(std::string_view typeId, MetaCoreGameObject& gameObject) const = 0; @@ -306,6 +363,13 @@ class MetaCoreIPlayModeService : public MetaCoreIEditorService { public: [[nodiscard]] virtual MetaCorePlayModeState GetState() const = 0; [[nodiscard]] virtual bool CanEnterPlayMode() const = 0; + [[nodiscard]] virtual bool EnterPlayMode(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool ExitPlayMode(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool PausePlayMode(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool ResumePlayMode(MetaCoreEditorContext& editorContext) = 0; + [[nodiscard]] virtual bool StepPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) = 0; + [[nodiscard]] virtual float GetElapsedPlayTimeSeconds() const = 0; + virtual void TickPlayMode(MetaCoreEditorContext& editorContext, float deltaSeconds) = 0; }; } // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h index 754d619..00e6d70 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h @@ -3,6 +3,7 @@ #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreRender/MetaCoreRenderTypes.h" +#include #include namespace MetaCore { @@ -25,6 +26,12 @@ public: void HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing); void FocusSelectedObject(MetaCoreEditorContext& editorContext) const; void FocusVisibleScene(MetaCoreEditorContext& editorContext) const; + [[nodiscard]] bool ApplyWorldTransformDeltaToSelection( + MetaCoreEditorContext& editorContext, + MetaCoreId manipulatedObjectId, + const glm::mat4& originalWorldMatrix, + const glm::mat4& newWorldMatrix + ) const; void HandleGizmoManipulation(MetaCoreEditorContext& editorContext); void DrawViewportToolbar(MetaCoreEditorContext& editorContext); void HandleShortcuts(MetaCoreEditorContext& editorContext); diff --git a/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp b/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp index 7eab158..301e3e1 100644 --- a/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp +++ b/Source/MetaCoreFoundation/Private/MetaCoreReflection.cpp @@ -14,6 +14,11 @@ const MetaCoreStructDescriptor* MetaCoreTypeRegistry::FindStructByName(std::stri return descriptorIterator == StructsByRuntimeType_.end() ? nullptr : &descriptorIterator->second; } +const MetaCoreEnumDescriptor* MetaCoreTypeRegistry::FindEnumByRuntimeType(std::type_index runtimeType) const { + const auto descriptorIterator = EnumsByRuntimeType_.find(runtimeType); + return descriptorIterator == EnumsByRuntimeType_.end() ? nullptr : &descriptorIterator->second; +} + void MetaCoreTypeRegistry::Clear() { StructsByRuntimeType_.clear(); StructNames_.clear(); diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h index 51a48cd..6d9ec36 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCorePackage.h @@ -142,6 +142,34 @@ struct MetaCoreCustomVersion { std::uint32_t Version = 0; }; +MC_STRUCT() +struct MetaCoreCookManifestEntry { + MC_GENERATED_BODY() + + MC_PROPERTY() + MetaCoreAssetGuid AssetGuid{}; + + MC_PROPERTY() + std::filesystem::path SourcePackagePath{}; + + MC_PROPERTY() + std::filesystem::path CookedPath{}; + + MC_PROPERTY() + std::uint64_t SourceHash = 0; + + MC_PROPERTY() + std::uint64_t CookedKey = 0; +}; + +MC_STRUCT() +struct MetaCoreCookManifestDocument { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::vector Entries{}; +}; + struct MetaCorePackageDocument { MetaCorePackageHeader Header{}; std::vector NameTable{}; @@ -193,4 +221,3 @@ private: }; } // namespace MetaCore - diff --git a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h index 4501d2c..e7241b2 100644 --- a/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h +++ b/Source/MetaCoreFoundation/Public/MetaCoreFoundation/MetaCoreReflection.h @@ -24,8 +24,46 @@ namespace MetaCore { using MetaCoreTypeId = std::uint64_t; +enum class MetaCoreFieldValueKind { + Unknown = 0, + Bool, + SignedInteger, + UnsignedInteger, + FloatingPoint, + String, + Path, + Vec3, + Enum, + Struct, + Vector, + Optional, + Array +}; + +struct MetaCoreFieldEditorMetadata { + std::string DisplayName{}; + std::string Group{}; + std::string Tooltip{}; + std::string RawSpec{}; + std::string ResourceType{}; + std::optional Min{}; + std::optional Max{}; + std::optional Step{}; + bool ReadOnly = false; + bool Hidden = false; + bool ResourceReference = false; +}; + struct MetaCoreFieldDescriptor { std::string Name{}; + std::string TypeName{}; + MetaCoreTypeId TypeId = 0; + MetaCoreFieldValueKind ValueKind = MetaCoreFieldValueKind::Unknown; + std::type_index RuntimeType = typeid(void); + std::size_t Size = 0; + MetaCoreFieldEditorMetadata Editor{}; + std::function MutableValue{}; + std::function ConstValue{}; std::function Serialize{}; std::function Deserialize{}; }; @@ -38,10 +76,16 @@ struct MetaCoreStructDescriptor { std::vector Fields{}; }; +struct MetaCoreEnumValueDescriptor { + std::string Name{}; + std::int64_t Value = 0; +}; + struct MetaCoreEnumDescriptor { MetaCoreTypeId TypeId = 0; std::string Name{}; std::type_index RuntimeType = typeid(void); + std::vector Values{}; }; class MetaCoreTypeRegistry { @@ -59,6 +103,7 @@ public: [[nodiscard]] const MetaCoreEnumDescriptor* FindEnum() const; [[nodiscard]] const MetaCoreStructDescriptor* FindStructByName(std::string_view name) const; + [[nodiscard]] const MetaCoreEnumDescriptor* FindEnumByRuntimeType(std::type_index runtimeType) const; void Clear(); private: @@ -75,7 +120,7 @@ public: } template - MetaCoreStructRegistrationBuilder& Field(std::string_view name); + MetaCoreStructRegistrationBuilder& Field(std::string_view name, MetaCoreFieldEditorMetadata editor = {}); private: MetaCoreStructDescriptor& Descriptor_; @@ -118,7 +163,20 @@ template ); template -void MetaCoreRegisterGeneratedEnum( +class MetaCoreEnumRegistrationBuilder { +public: + explicit MetaCoreEnumRegistrationBuilder(MetaCoreEnumDescriptor& descriptor) + : Descriptor_(descriptor) { + } + + MetaCoreEnumRegistrationBuilder& Value(std::string_view name, TEnum value); + +private: + MetaCoreEnumDescriptor& Descriptor_; +}; + +template +MetaCoreEnumRegistrationBuilder MetaCoreRegisterGeneratedEnum( MetaCoreTypeRegistry& registry, std::string_view name ); @@ -159,6 +217,46 @@ struct MetaCoreIsStdArray> : std::true_type { template inline constexpr bool GMetaCoreAlwaysFalse = false; +template +struct MetaCoreMemberPointerTraits; + +template +struct MetaCoreMemberPointerTraits { + using ObjectType = TObject; + using ValueType = TValue; +}; + +template +[[nodiscard]] constexpr MetaCoreFieldValueKind MetaCoreDetectFieldValueKind() { + using TValue = std::remove_cvref_t; + + if constexpr (std::is_same_v) { + return MetaCoreFieldValueKind::Bool; + } else if constexpr (std::is_integral_v && std::is_signed_v) { + return MetaCoreFieldValueKind::SignedInteger; + } else if constexpr (std::is_integral_v && std::is_unsigned_v) { + return MetaCoreFieldValueKind::UnsignedInteger; + } else if constexpr (std::is_floating_point_v) { + return MetaCoreFieldValueKind::FloatingPoint; + } else if constexpr (std::is_same_v) { + return MetaCoreFieldValueKind::String; + } else if constexpr (std::is_same_v) { + return MetaCoreFieldValueKind::Path; + } else if constexpr (std::is_same_v) { + return MetaCoreFieldValueKind::Vec3; + } else if constexpr (std::is_enum_v) { + return MetaCoreFieldValueKind::Enum; + } else if constexpr (MetaCoreIsVector::value) { + return MetaCoreFieldValueKind::Vector; + } else if constexpr (MetaCoreIsOptional::value) { + return MetaCoreFieldValueKind::Optional; + } else if constexpr (MetaCoreIsStdArray::value) { + return MetaCoreFieldValueKind::Array; + } else { + return MetaCoreFieldValueKind::Struct; + } +} + } // namespace Detail template @@ -179,6 +277,7 @@ MetaCoreEnumDescriptor& MetaCoreTypeRegistry::RegisterEnum(std::string_view name descriptor.TypeId = MetaCoreMakeTypeId(name); descriptor.Name = std::string(name); descriptor.RuntimeType = std::type_index(typeid(T)); + descriptor.Values.clear(); return descriptor; } @@ -196,9 +295,33 @@ const MetaCoreEnumDescriptor* MetaCoreTypeRegistry::FindEnum() const { template template -MetaCoreStructRegistrationBuilder& MetaCoreStructRegistrationBuilder::Field(std::string_view name) { +MetaCoreStructRegistrationBuilder& MetaCoreStructRegistrationBuilder::Field( + std::string_view name, + MetaCoreFieldEditorMetadata editor +) { + using FieldType = std::remove_cvref_t::ValueType>; + static_assert(std::is_same_v::ObjectType, T>); + + if (editor.DisplayName.empty()) { + editor.DisplayName = std::string(name); + } + Descriptor_.Fields.push_back(MetaCoreFieldDescriptor{ std::string(name), + typeid(FieldType).name(), + MetaCoreMakeTypeId(typeid(FieldType).name()), + Detail::MetaCoreDetectFieldValueKind(), + std::type_index(typeid(FieldType)), + sizeof(FieldType), + std::move(editor), + [](void* instance) -> void* { + auto& typedInstance = *static_cast(instance); + return &(typedInstance.*MemberPointer); + }, + [](const void* instance) -> const void* { + const auto& typedInstance = *static_cast(instance); + return &(typedInstance.*MemberPointer); + }, [](const void* instance, MetaCoreArchiveWriter& writer, const MetaCoreTypeRegistry& registry) { const auto& typedInstance = *static_cast(instance); return MetaCoreSerializeValue(writer, typedInstance.*MemberPointer, registry); @@ -211,6 +334,19 @@ MetaCoreStructRegistrationBuilder& MetaCoreStructRegistrationBuilder::Fiel return *this; } +template +MetaCoreEnumRegistrationBuilder& MetaCoreEnumRegistrationBuilder::Value( + std::string_view name, + TEnum value +) { + static_assert(std::is_enum_v); + Descriptor_.Values.push_back(MetaCoreEnumValueDescriptor{ + std::string(name), + static_cast(value) + }); + return *this; +} + template MetaCoreStructRegistrationBuilder MetaCoreRegisterGeneratedStruct( MetaCoreTypeRegistry& registry, @@ -221,8 +357,8 @@ MetaCoreStructRegistrationBuilder MetaCoreRegisterGeneratedStruct( } template -void MetaCoreRegisterGeneratedEnum(MetaCoreTypeRegistry& registry, std::string_view name) { - (void)registry.RegisterEnum(name); +MetaCoreEnumRegistrationBuilder MetaCoreRegisterGeneratedEnum(MetaCoreTypeRegistry& registry, std::string_view name) { + return MetaCoreEnumRegistrationBuilder(registry.RegisterEnum(name)); } template diff --git a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp index 02e9256..35942d6 100644 --- a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp @@ -94,6 +94,10 @@ void MetaCoreEditorViewportRenderer::RenderSceneToViewport(MetaCoreScene& scene, FilamentSceneBridge_.SyncScene(scene, false, useScenePrimaryCamera); } +void MetaCoreEditorViewportRenderer::SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) { + FilamentSceneBridge_.SetRuntimeUiOverlayFrame(frame, width, height); +} + void MetaCoreEditorViewportRenderer::RenderAll() { FilamentSceneBridge_.RenderAll(); } diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index db645e0..b5f661f 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -5,6 +5,7 @@ #include "MetaCoreRender/MetaCoreRenderTypes.h" #include "MetaCoreRender/MetaCoreImGuiHelper.h" #include "MetaCoreRender/MetaCoreSceneRenderSync.h" +#include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" #include #include "MetaCoreFoundation/MetaCoreAssetRegistry.h" @@ -150,6 +151,11 @@ public: } else { auto [width, height] = window.GetFramebufferSize(); View_->setViewport({0, 0, static_cast(width), static_cast(height)}); + + RuntimeUIView_ = Engine_->createView(); + RuntimeUiHelper_ = new MetaCoreImGuiHelper(Engine_, RuntimeUIView_, "", nullptr); + RuntimeUiHelper_->setDisplaySize(width, height); + RuntimeUIView_->setViewport({0, 0, static_cast(width), static_cast(height)}); } // 确保开启后处理(为了HDR) @@ -226,6 +232,7 @@ public: } SceneLightEntities_.clear(); EntitiesInScene_.clear(); + Engine_->flushAndWait(); // 销毁 gltfio 资源 std::cout << "[DEBUG] Destroying gltfio resources..." << std::endl; @@ -238,6 +245,15 @@ public: delete MaterialProvider_; MaterialProvider_ = nullptr; } + if (ImGuiHelper_) { + delete ImGuiHelper_; + ImGuiHelper_ = nullptr; + } + if (RuntimeUiHelper_) { + delete RuntimeUiHelper_; + RuntimeUiHelper_ = nullptr; + } + Engine_->flushAndWait(); // 销毁离屏渲染资源 if (RenderTarget_) { @@ -258,14 +274,14 @@ public: } // 销毁 View Engine_->destroy(View_); + if (RuntimeUIView_) { + Engine_->destroy(RuntimeUIView_); + RuntimeUIView_ = nullptr; + } if (UIView_) { Engine_->destroy(UIView_); UIView_ = nullptr; } - if (ImGuiHelper_) { - delete ImGuiHelper_; - ImGuiHelper_ = nullptr; - } // 销毁相机组件和实体 if (Camera_) { @@ -278,6 +294,7 @@ public: Engine_->destroy(Scene_); Engine_->destroy(Renderer_); Engine_->destroy(SwapChain_); + Engine_->flushAndWait(); delete NameManager_; NameManager_ = nullptr; @@ -899,11 +916,42 @@ public: filament::Camera::Fov::VERTICAL ); } + + void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) { + RuntimeUiFrame_ = frame; + RuntimeUiWidth_ = width; + RuntimeUiHeight_ = height; + } + void RenderAll() { if (!Renderer_ || !SwapChain_ || !View_) { return; } + bool renderRuntimeUi = false; + if (!RenderTarget_ && RuntimeUIView_ && RuntimeUiHelper_) { + const auto viewport = View_->getViewport(); + const int overlayWidth = RuntimeUiWidth_ > 0 ? RuntimeUiWidth_ : static_cast(viewport.width); + const int overlayHeight = RuntimeUiHeight_ > 0 ? RuntimeUiHeight_ : static_cast(viewport.height); + if (overlayWidth > 0 && overlayHeight > 0) { + RuntimeUiHelper_->setDisplaySize(overlayWidth, overlayHeight); + RuntimeUIView_->setViewport({ + 0, + 0, + static_cast(overlayWidth), + static_cast(overlayHeight) + }); + + const bool hasRuntimeUi = !RuntimeUiFrame_.Commands.empty(); + const bool preparedRuntimeUi = RuntimeUiHelper_->processRuntimeUiFrame( + RuntimeUiFrame_, + overlayWidth, + overlayHeight + ); + renderRuntimeUi = preparedRuntimeUi && hasRuntimeUi; + } + } + if (Renderer_->beginFrame(SwapChain_)) { if (RenderTarget_) { // 1. 渲染 3D 离屏视口 (输出到 RenderTarget) @@ -936,6 +984,13 @@ public: Renderer_->setClearOptions(options); Renderer_->render(View_); + + if (renderRuntimeUi) { + options.clearColor = {0.0f, 0.0f, 0.0f, 0.0f}; + options.clear = false; + Renderer_->setClearOptions(options); + Renderer_->render(RuntimeUIView_); + } } Renderer_->endFrame(); @@ -1289,6 +1344,11 @@ private: filament::View* UIView_ = nullptr; MetaCoreImGuiHelper* ImGuiHelper_ = nullptr; + filament::View* RuntimeUIView_ = nullptr; + MetaCoreImGuiHelper* RuntimeUiHelper_ = nullptr; + MetaCoreRuntimeUiFrame RuntimeUiFrame_{}; + int RuntimeUiWidth_ = 0; + int RuntimeUiHeight_ = 0; GLuint GLTextureId_ = 0; filament::Texture* FilamentTexture_ = nullptr; @@ -1331,6 +1391,10 @@ void MetaCoreFilamentSceneBridge::ApplySceneView(const MetaCoreSceneView& sceneV Impl_->ApplySceneView(sceneView); } +void MetaCoreFilamentSceneBridge::SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) { + Impl_->SetRuntimeUiOverlayFrame(frame, width, height); +} + void MetaCoreFilamentSceneBridge::RenderAll() { Impl_->RenderAll(); } diff --git a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp index 7af013f..46259c0 100644 --- a/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreImGuiHelper.cpp @@ -1,5 +1,11 @@ #include "MetaCoreRender/MetaCoreImGuiHelper.h" +#include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" +#include +#include +#include +#include +#include #include #include #include @@ -115,28 +121,74 @@ void MetaCoreImGuiHelper::createAtlasTexture(Engine* engine) { if (mMaterial2d) { mMaterial2d->setDefaultParameter("albedo", mTexture, mSampler); } + } MetaCoreImGuiHelper::~MetaCoreImGuiHelper() { - mEngine->destroy(mScene); - mEngine->destroy(mRenderable); - mEngine->destroyCameraComponent(mCameraEntity); + if (mView != nullptr) { + mView->setScene(nullptr); + mView->setCamera(nullptr); + } + if (mScene != nullptr) { + mScene->remove(mRenderable); + } for (auto& mi : mMaterial2dInstances) { - mEngine->destroy(mi); + if (mi != nullptr) { + mEngine->destroy(mi); + } + } + mMaterial2dInstances.clear(); + + if (mMaterial2d != nullptr) { + mEngine->destroy(mMaterial2d); + mMaterial2d = nullptr; + } + if (mTexture != nullptr) { + mEngine->destroy(mTexture); + mTexture = nullptr; + } + if (mWhiteTexture != nullptr) { + mEngine->destroy(mWhiteTexture); + mWhiteTexture = nullptr; } - mEngine->destroy(mMaterial2d); - mEngine->destroy(mTexture); for (auto& vb : mVertexBuffers) { - mEngine->destroy(vb); + if (vb != nullptr) { + mEngine->destroy(vb); + } } + mVertexBuffers.clear(); for (auto& ib : mIndexBuffers) { - mEngine->destroy(ib); + if (ib != nullptr) { + mEngine->destroy(ib); + } } + mIndexBuffers.clear(); - for (auto itextures: mImGuiTextures) { - mEngine->destroy(itextures); + for (auto* texture : mImGuiTextures) { + if (texture != nullptr) { + mEngine->destroy(texture); + } + } + mImGuiTextures.clear(); + for (auto& [handle, textureState] : mRuntimeUiTextures) { + (void)handle; + if (textureState.Texture != nullptr) { + mEngine->destroy(textureState.Texture); + textureState.Texture = nullptr; + } + } + mRuntimeUiTextures.clear(); + + if (mScene != nullptr) { + mEngine->destroy(mScene); + mScene = nullptr; + } + mEngine->destroy(mRenderable); + if (mCamera != nullptr) { + mEngine->destroyCameraComponent(mCameraEntity); + mCamera = nullptr; } EntityManager& em = utils::EntityManager::get(); @@ -263,6 +315,247 @@ void MetaCoreImGuiHelper::processImGuiCommands(ImDrawData* commands, const ImGui } } +filament::Texture* MetaCoreImGuiHelper::getWhiteTexture() { + if (mWhiteTexture != nullptr) { + return mWhiteTexture; + } + + const std::uint8_t whitePixel[4] = {255, 255, 255, 255}; + void* whitePixelData = malloc(sizeof(whitePixel)); + if (whitePixelData == nullptr) { + return nullptr; + } + std::memcpy(whitePixelData, whitePixel, sizeof(whitePixel)); + + Texture::PixelBufferDescriptor whitePixelBuffer( + whitePixelData, + sizeof(whitePixel), + Texture::Format::RGBA, + Texture::Type::UBYTE, + [](void* buffer, size_t, void*) { + free(buffer); + }, + nullptr); + + mWhiteTexture = Texture::Builder() + .width(1) + .height(1) + .levels(1) + .format(Texture::InternalFormat::RGBA8) + .sampler(Texture::Sampler::SAMPLER_2D) + .build(*mEngine); + mWhiteTexture->setImage(*mEngine, 0, std::move(whitePixelBuffer)); + return mWhiteTexture; +} + +filament::Texture* MetaCoreImGuiHelper::syncRuntimeUiTexture(const MetaCoreRuntimeUiFrame& frame, std::uint64_t handle) { + if (handle == 0) { + return nullptr; + } + + const auto textureIterator = std::find_if( + frame.Textures.begin(), + frame.Textures.end(), + [handle](const MetaCoreRuntimeUiTexture& texture) { + return texture.Handle == handle; + }); + if (textureIterator == frame.Textures.end()) { + return nullptr; + } + + const MetaCoreRuntimeUiTexture& texture = *textureIterator; + if (texture.Width <= 0 || texture.Height <= 0) { + return nullptr; + } + + const std::size_t expectedByteCount = + static_cast(texture.Width) * static_cast(texture.Height) * 4U; + if (expectedByteCount == 0 || texture.Rgba.size() < expectedByteCount) { + return nullptr; + } + + RuntimeUiTextureState& state = mRuntimeUiTextures[handle]; + if (state.Texture != nullptr && + state.Width == texture.Width && + state.Height == texture.Height && + state.Revision == texture.Revision) { + return state.Texture; + } + + if (state.Texture != nullptr) { + mEngine->destroy(state.Texture); + state.Texture = nullptr; + } + + void* pixelData = malloc(expectedByteCount); + if (pixelData == nullptr) { + return nullptr; + } + std::memcpy(pixelData, texture.Rgba.data(), expectedByteCount); + + Texture::PixelBufferDescriptor pixelBuffer( + pixelData, + expectedByteCount, + Texture::Format::RGBA, + Texture::Type::UBYTE, + [](void* buffer, size_t, void*) { + free(buffer); + }, + nullptr); + + state.Texture = Texture::Builder() + .width(static_cast(texture.Width)) + .height(static_cast(texture.Height)) + .levels(1) + .format(Texture::InternalFormat::RGBA8) + .sampler(Texture::Sampler::SAMPLER_2D) + .build(*mEngine); + state.Texture->setImage(*mEngine, 0, std::move(pixelBuffer)); + state.Width = texture.Width; + state.Height = texture.Height; + state.Revision = texture.Revision; + return state.Texture; +} + +bool MetaCoreImGuiHelper::processRuntimeUiFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) { + ImGui::SetCurrentContext(mImGuiContext); + + mHasSynced = false; + auto& rcm = mEngine->getRenderableManager(); + rcm.destroy(mRenderable); + + if (width <= 0 || height <= 0) { + return false; + } + if (frame.Commands.empty() || frame.Vertices.empty() || frame.Indices.empty()) { + return true; + } + if (frame.Vertices.size() > static_cast(std::numeric_limits::max())) { + return false; + } + + std::unordered_set activeTextureHandles; + activeTextureHandles.reserve(frame.Textures.size()); + for (const MetaCoreRuntimeUiTexture& texture : frame.Textures) { + if (texture.Handle != 0) { + activeTextureHandles.insert(texture.Handle); + } + } + for (auto iterator = mRuntimeUiTextures.begin(); iterator != mRuntimeUiTextures.end();) { + if (activeTextureHandles.contains(iterator->first)) { + ++iterator; + continue; + } + if (iterator->second.Texture != nullptr) { + mEngine->destroy(iterator->second.Texture); + } + iterator = mRuntimeUiTextures.erase(iterator); + } + + createBuffers(1); + + std::vector vertices; + vertices.reserve(frame.Vertices.size()); + for (const MetaCoreRuntimeUiDrawVertex& vertex : frame.Vertices) { + ImDrawVert converted{}; + converted.pos = ImVec2(vertex.X, vertex.Y); + converted.uv = ImVec2(vertex.U, vertex.V); + converted.col = IM_COL32(vertex.R, vertex.G, vertex.B, vertex.A); + vertices.push_back(converted); + } + + std::vector indices; + indices.reserve(frame.Indices.size()); + for (const std::uint32_t index : frame.Indices) { + if (index > static_cast(std::numeric_limits::max())) { + return false; + } + indices.push_back(static_cast(index)); + } + + populateVertexData( + 0, + vertices.size() * sizeof(ImDrawVert), + vertices.data(), + indices.size() * sizeof(std::uint16_t), + indices.data()); + + std::size_t primitiveCount = 0; + for (const MetaCoreRuntimeUiDrawCommand& command : frame.Commands) { + if (command.IndexCount == 0 || + command.VertexCount == 0 || + command.VertexOffset + command.VertexCount > frame.Vertices.size() || + command.IndexOffset + command.IndexCount > frame.Indices.size()) { + continue; + } + ++primitiveCount; + } + if (primitiveCount == 0) { + return true; + } + + auto rbuilder = RenderableManager::Builder(primitiveCount); + rbuilder.boundingBox({{ 0, 0, 0 }, { 10000, 10000, 10000 }}).culling(false); + + int primitiveIndex = 0; + int material2dIndex = 0; + for (const MetaCoreRuntimeUiDrawCommand& command : frame.Commands) { + if (command.IndexCount == 0 || + command.VertexCount == 0 || + command.VertexOffset + command.VertexCount > frame.Vertices.size() || + command.IndexOffset + command.IndexCount > frame.Indices.size()) { + continue; + } + + if (material2dIndex == static_cast(mMaterial2dInstances.size())) { + if (mMaterial2d) { + mMaterial2dInstances.push_back(mMaterial2d->createInstance()); + } else { + mMaterial2dInstances.push_back(nullptr); + } + } + MaterialInstance* materialInstance = mMaterial2dInstances[material2dIndex++]; + if (!materialInstance) { + continue; + } + + if (command.ScissorEnabled) { + const int left = std::clamp(command.ScissorLeft, 0, width); + const int top = std::clamp(command.ScissorTop, 0, height); + const int right = std::clamp(command.ScissorRight, left, width); + const int bottom = std::clamp(command.ScissorBottom, top, height); + materialInstance->setScissor( + static_cast(left), + static_cast(height - bottom), + static_cast(right - left), + static_cast(bottom - top)); + } else { + materialInstance->unsetScissor(); + } + + filament::Texture* runtimeTexture = syncRuntimeUiTexture(frame, command.TextureHandle); + filament::Texture* fallbackTexture = getWhiteTexture(); + materialInstance->setParameter( + "albedo", + runtimeTexture != nullptr ? runtimeTexture : (fallbackTexture != nullptr ? fallbackTexture : mTexture), + mSampler); + + rbuilder + .geometry(primitiveIndex, RenderableManager::PrimitiveType::TRIANGLES, + mVertexBuffers[0], mIndexBuffers[0], + static_cast(command.IndexOffset), + static_cast(command.IndexCount)) + .blendOrder(primitiveIndex, static_cast(primitiveIndex)) + .material(primitiveIndex, materialInstance); + ++primitiveIndex; + } + + if (primitiveIndex > 0) { + rbuilder.build(*mEngine, mRenderable); + } + return true; +} + void MetaCoreImGuiHelper::createVertexBuffer(size_t bufferIndex, size_t capacity) { syncThreads(); if (bufferIndex < mVertexBuffers.size() && mVertexBuffers[bufferIndex]) { diff --git a/Source/MetaCoreRender/Private/MetaCoreRuntimeUiRenderer.cpp b/Source/MetaCoreRender/Private/MetaCoreRuntimeUiRenderer.cpp new file mode 100644 index 0000000..37f3702 --- /dev/null +++ b/Source/MetaCoreRender/Private/MetaCoreRuntimeUiRenderer.cpp @@ -0,0 +1,788 @@ +#include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +namespace { + +[[nodiscard]] float MetaCoreRuntimeUiEdge( + const MetaCoreRuntimeUiDrawVertex& a, + const MetaCoreRuntimeUiDrawVertex& b, + float x, + float y +) { + return (x - a.X) * (b.Y - a.Y) - (y - a.Y) * (b.X - a.X); +} + +[[nodiscard]] std::uint8_t MetaCoreRuntimeUiByte(float value) { + return static_cast(std::clamp(value, 0.0F, 255.0F)); +} + +[[nodiscard]] std::string MetaCoreEscapeRuntimeUiRmlText(std::string_view value) { + std::string escaped; + escaped.reserve(value.size()); + for (char c : value) { + switch (c) { + case '&': escaped += "&"; break; + case '<': escaped += "<"; break; + case '>': escaped += ">"; break; + case '"': escaped += """; break; + case '\'': escaped += "'"; break; + default: escaped.push_back(c); break; + } + } + return escaped; +} + +void MetaCoreRuntimeUiBlendPixel( + MetaCoreRuntimeUiRasterFrame& rasterFrame, + std::int32_t x, + std::int32_t y, + float red, + float green, + float blue, + float alpha +) { + if (x < 0 || y < 0 || x >= rasterFrame.Width || y >= rasterFrame.Height) { + return; + } + + const std::size_t pixelOffset = + (static_cast(y) * static_cast(rasterFrame.Width) + static_cast(x)) * 4U; + if (pixelOffset + 3U >= rasterFrame.Rgba.size()) { + return; + } + + const float sourceAlpha = std::clamp(alpha / 255.0F, 0.0F, 1.0F); + const float inverseAlpha = 1.0F - sourceAlpha; + rasterFrame.Rgba[pixelOffset + 0U] = MetaCoreRuntimeUiByte(red + static_cast(rasterFrame.Rgba[pixelOffset + 0U]) * inverseAlpha); + rasterFrame.Rgba[pixelOffset + 1U] = MetaCoreRuntimeUiByte(green + static_cast(rasterFrame.Rgba[pixelOffset + 1U]) * inverseAlpha); + rasterFrame.Rgba[pixelOffset + 2U] = MetaCoreRuntimeUiByte(blue + static_cast(rasterFrame.Rgba[pixelOffset + 2U]) * inverseAlpha); + rasterFrame.Rgba[pixelOffset + 3U] = MetaCoreRuntimeUiByte(alpha + static_cast(rasterFrame.Rgba[pixelOffset + 3U]) * inverseAlpha); +} + +void MetaCoreRuntimeUiRasterizeTriangle( + MetaCoreRuntimeUiRasterFrame& rasterFrame, + const MetaCoreRuntimeUiDrawCommand& command, + const MetaCoreRuntimeUiDrawVertex& v0, + const MetaCoreRuntimeUiDrawVertex& v1, + const MetaCoreRuntimeUiDrawVertex& v2 +) { + const float area = MetaCoreRuntimeUiEdge(v0, v1, v2.X, v2.Y); + if (std::abs(area) <= 0.0001F) { + return; + } + + std::int32_t minX = static_cast(std::floor(std::min({v0.X, v1.X, v2.X}))); + std::int32_t minY = static_cast(std::floor(std::min({v0.Y, v1.Y, v2.Y}))); + std::int32_t maxX = static_cast(std::ceil(std::max({v0.X, v1.X, v2.X}))); + std::int32_t maxY = static_cast(std::ceil(std::max({v0.Y, v1.Y, v2.Y}))); + + minX = std::clamp(minX, 0, std::max(0, rasterFrame.Width - 1)); + minY = std::clamp(minY, 0, std::max(0, rasterFrame.Height - 1)); + maxX = std::clamp(maxX, 0, std::max(0, rasterFrame.Width - 1)); + maxY = std::clamp(maxY, 0, std::max(0, rasterFrame.Height - 1)); + + if (command.ScissorEnabled) { + minX = std::max(minX, command.ScissorLeft); + minY = std::max(minY, command.ScissorTop); + maxX = std::min(maxX, command.ScissorRight); + maxY = std::min(maxY, command.ScissorBottom); + } + + if (minX > maxX || minY > maxY) { + return; + } + + const bool positiveArea = area > 0.0F; + for (std::int32_t y = minY; y <= maxY; ++y) { + for (std::int32_t x = minX; x <= maxX; ++x) { + const float sampleX = static_cast(x) + 0.5F; + const float sampleY = static_cast(y) + 0.5F; + const float w0 = MetaCoreRuntimeUiEdge(v1, v2, sampleX, sampleY); + const float w1 = MetaCoreRuntimeUiEdge(v2, v0, sampleX, sampleY); + const float w2 = MetaCoreRuntimeUiEdge(v0, v1, sampleX, sampleY); + + const bool inside = positiveArea + ? (w0 >= 0.0F && w1 >= 0.0F && w2 >= 0.0F) + : (w0 <= 0.0F && w1 <= 0.0F && w2 <= 0.0F); + if (!inside) { + continue; + } + + const float invArea = 1.0F / area; + const float b0 = w0 * invArea; + const float b1 = w1 * invArea; + const float b2 = w2 * invArea; + MetaCoreRuntimeUiBlendPixel( + rasterFrame, + x, + y, + b0 * static_cast(v0.R) + b1 * static_cast(v1.R) + b2 * static_cast(v2.R), + b0 * static_cast(v0.G) + b1 * static_cast(v1.G) + b2 * static_cast(v2.G), + b0 * static_cast(v0.B) + b1 * static_cast(v1.B) + b2 * static_cast(v2.B), + b0 * static_cast(v0.A) + b1 * static_cast(v1.A) + b2 * static_cast(v2.A) + ); + } + } +} + +void MetaCoreRuntimeUiRasterizeFrame( + const MetaCoreRuntimeUiFrame& frame, + MetaCoreRuntimeUiRasterFrame& rasterFrame, + MetaCoreRuntimeUiRenderStats& stats, + std::int32_t width, + std::int32_t height +) { + rasterFrame.Width = std::max(0, width); + rasterFrame.Height = std::max(0, height); + const std::size_t pixelCount = + static_cast(rasterFrame.Width) * static_cast(rasterFrame.Height); + rasterFrame.Rgba.assign(pixelCount * 4U, 0); + + for (const MetaCoreRuntimeUiDrawCommand& command : frame.Commands) { + const std::size_t triangleCount = command.IndexCount / 3U; + for (std::size_t triangleIndex = 0; triangleIndex < triangleCount; ++triangleIndex) { + const std::size_t i0 = command.IndexOffset + triangleIndex * 3U + 0U; + const std::size_t i1 = command.IndexOffset + triangleIndex * 3U + 1U; + const std::size_t i2 = command.IndexOffset + triangleIndex * 3U + 2U; + if (i2 >= frame.Indices.size()) { + continue; + } + + const std::uint32_t v0Index = frame.Indices[i0]; + const std::uint32_t v1Index = frame.Indices[i1]; + const std::uint32_t v2Index = frame.Indices[i2]; + if (v0Index >= frame.Vertices.size() || + v1Index >= frame.Vertices.size() || + v2Index >= frame.Vertices.size()) { + continue; + } + + MetaCoreRuntimeUiRasterizeTriangle( + rasterFrame, + command, + frame.Vertices[v0Index], + frame.Vertices[v1Index], + frame.Vertices[v2Index] + ); + } + } + + std::size_t touchedPixelCount = 0; + for (std::size_t pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) { + if (rasterFrame.Rgba[pixelIndex * 4U + 3U] > 0) { + ++touchedPixelCount; + } + } + stats.RasterPixelCount = pixelCount; + stats.RasterTouchedPixelCount = touchedPixelCount; +} + +class MetaCoreRmlSystemInterface final : public Rml::SystemInterface { +public: + double GetElapsedTime() override { + return ElapsedSeconds_; + } + + void Advance(double deltaSeconds) { + ElapsedSeconds_ += std::max(0.0, deltaSeconds); + } + + bool LogMessage(Rml::Log::Type type, const Rml::String& message) override { + LastLogType_ = type; + LastMessage_ = message; + return true; + } + + [[nodiscard]] const std::string& GetLastMessage() const { + return LastMessage_; + } + +private: + double ElapsedSeconds_ = 0.0; + Rml::Log::Type LastLogType_ = Rml::Log::LT_INFO; + std::string LastMessage_{}; +}; + +struct MetaCoreRmlMemoryFile { + std::string Data{}; + std::size_t Cursor = 0; +}; + +[[nodiscard]] std::string MetaCoreNormalizeRmlPath(std::string path) { + std::replace(path.begin(), path.end(), '\\', '/'); + while (path.find("//") != std::string::npos) { + path.erase(path.find("//"), 1); + } + return path; +} + +[[nodiscard]] std::string MetaCoreRmlBasename(const std::string& path) { + const std::string normalized = MetaCoreNormalizeRmlPath(path); + const auto slash = normalized.find_last_of('/'); + return slash == std::string::npos ? normalized : normalized.substr(slash + 1); +} + +class MetaCoreRmlMemoryFileInterface final : public Rml::FileInterface { +public: + void SetVirtualFile(std::string path, std::string data) { + if (path.empty()) { + path = "MetaCoreRuntimeUi.rcss"; + } + + const std::string normalized = MetaCoreNormalizeRmlPath(std::move(path)); + VirtualFiles_[normalized] = data; + VirtualFiles_[MetaCoreRmlBasename(normalized)] = std::move(data); + } + + void Clear() { + VirtualFiles_.clear(); + OpenFiles_.clear(); + } + + Rml::FileHandle Open(const Rml::String& path) override { + const std::string normalized = MetaCoreNormalizeRmlPath(path); + auto iterator = VirtualFiles_.find(normalized); + if (iterator == VirtualFiles_.end()) { + iterator = VirtualFiles_.find(MetaCoreRmlBasename(normalized)); + } + if (iterator == VirtualFiles_.end()) { + return 0; + } + + const Rml::FileHandle handle = NextHandle_++; + OpenFiles_.emplace(handle, MetaCoreRmlMemoryFile{iterator->second, 0}); + return handle; + } + + void Close(Rml::FileHandle file) override { + OpenFiles_.erase(file); + } + + size_t Read(void* buffer, size_t size, Rml::FileHandle file) override { + auto iterator = OpenFiles_.find(file); + if (iterator == OpenFiles_.end() || buffer == nullptr || size == 0) { + return 0; + } + + MetaCoreRmlMemoryFile& memoryFile = iterator->second; + const std::size_t remaining = memoryFile.Cursor < memoryFile.Data.size() + ? memoryFile.Data.size() - memoryFile.Cursor + : 0; + const std::size_t bytesToRead = std::min(size, remaining); + if (bytesToRead > 0) { + std::memcpy(buffer, memoryFile.Data.data() + memoryFile.Cursor, bytesToRead); + memoryFile.Cursor += bytesToRead; + } + return bytesToRead; + } + + bool Seek(Rml::FileHandle file, long offset, int origin) override { + auto iterator = OpenFiles_.find(file); + if (iterator == OpenFiles_.end()) { + return false; + } + + MetaCoreRmlMemoryFile& memoryFile = iterator->second; + long base = 0; + if (origin == SEEK_CUR) { + base = static_cast(memoryFile.Cursor); + } else if (origin == SEEK_END) { + base = static_cast(memoryFile.Data.size()); + } + + const long next = base + offset; + if (next < 0) { + return false; + } + + memoryFile.Cursor = std::min( + static_cast(next), + memoryFile.Data.size() + ); + return true; + } + + size_t Tell(Rml::FileHandle file) override { + auto iterator = OpenFiles_.find(file); + return iterator == OpenFiles_.end() ? 0 : iterator->second.Cursor; + } + +private: + Rml::FileHandle NextHandle_ = 1; + std::unordered_map VirtualFiles_{}; + std::unordered_map OpenFiles_{}; +}; + +struct MetaCoreRmlSharedRuntime { + [[nodiscard]] bool Acquire(MetaCoreRuntimeUiRenderStats& stats) { + if (ReferenceCount == 0) { + FileInterface.Clear(); + Rml::SetSystemInterface(&SystemInterface); + Rml::SetFileInterface(&FileInterface); + Rml::SetFontEngineInterface(&FontInterface); + if (!Rml::Initialise()) { + stats.LastError = SystemInterface.GetLastMessage().empty() + ? "RmlUi core initialization failed" + : "RmlUi core initialization failed: " + SystemInterface.GetLastMessage(); + return false; + } + Initialized = true; + } + + ++ReferenceCount; + stats.RmlInitialized = Initialized; + return Initialized; + } + + void Release() { + if (ReferenceCount == 0) { + return; + } + + --ReferenceCount; + if (ReferenceCount == 0 && Initialized) { + Rml::Shutdown(); + FileInterface.Clear(); + Initialized = false; + } + } + + MetaCoreRmlSystemInterface SystemInterface{}; + MetaCoreRmlMemoryFileInterface FileInterface{}; + Rml::FontEngineInterface FontInterface{}; + std::size_t ReferenceCount = 0; + bool Initialized = false; +}; + +[[nodiscard]] MetaCoreRmlSharedRuntime& MetaCoreGetRmlSharedRuntime() { + static MetaCoreRmlSharedRuntime runtime; + return runtime; +} + +class MetaCoreRmlDiagnosticRenderInterface final : public Rml::RenderInterface { +public: + MetaCoreRmlDiagnosticRenderInterface( + MetaCoreRuntimeUiRenderStats& stats, + MetaCoreRuntimeUiFrame& frame + ) + : Stats_(stats), + Frame_(frame) {} + + Rml::CompiledGeometryHandle CompileGeometry(Rml::Span vertices, Rml::Span indices) override { + const Rml::CompiledGeometryHandle handle = NextGeometryHandle_++; + MetaCoreRmlGeometryRecord record; + record.Vertices.reserve(vertices.size()); + for (const Rml::Vertex& vertex : vertices) { + record.Vertices.push_back(MetaCoreRuntimeUiDrawVertex{ + vertex.position.x, + vertex.position.y, + vertex.tex_coord.x, + vertex.tex_coord.y, + vertex.colour.red, + vertex.colour.green, + vertex.colour.blue, + vertex.colour.alpha + }); + } + record.Indices.reserve(indices.size()); + for (const int index : indices) { + if (index >= 0) { + record.Indices.push_back(static_cast(index)); + } + } + + Geometries_.emplace(handle, std::move(record)); + Stats_.CompiledGeometryCount = Geometries_.size(); + return handle; + } + + void RenderGeometry(Rml::CompiledGeometryHandle geometry, Rml::Vector2f translation, Rml::TextureHandle texture) override { + ++Stats_.RenderGeometryCalls; + const auto geometryIterator = Geometries_.find(geometry); + if (geometryIterator == Geometries_.end()) { + return; + } + + const MetaCoreRmlGeometryRecord& record = geometryIterator->second; + const std::size_t vertexOffset = Frame_.Vertices.size(); + const std::size_t indexOffset = Frame_.Indices.size(); + + Frame_.Vertices.reserve(Frame_.Vertices.size() + record.Vertices.size()); + for (MetaCoreRuntimeUiDrawVertex vertex : record.Vertices) { + vertex.X += translation.x; + vertex.Y += translation.y; + Frame_.Vertices.push_back(vertex); + } + + Frame_.Indices.reserve(Frame_.Indices.size() + record.Indices.size()); + for (const std::uint32_t index : record.Indices) { + Frame_.Indices.push_back(static_cast(vertexOffset) + index); + } + + Frame_.Commands.push_back(MetaCoreRuntimeUiDrawCommand{ + vertexOffset, + record.Vertices.size(), + indexOffset, + record.Indices.size(), + static_cast(texture), + ScissorEnabled_, + ScissorRegion_.Left(), + ScissorRegion_.Top(), + ScissorRegion_.Right(), + ScissorRegion_.Bottom() + }); + SyncFrameStats(); + } + + void ReleaseGeometry(Rml::CompiledGeometryHandle geometry) override { + Geometries_.erase(geometry); + Stats_.CompiledGeometryCount = Geometries_.size(); + } + + Rml::TextureHandle LoadTexture(Rml::Vector2i& textureDimensions, const Rml::String& source) override { + (void)source; + ++Stats_.TextureLoadRequests; + textureDimensions = Rml::Vector2i(0, 0); + return 0; + } + + Rml::TextureHandle GenerateTexture(Rml::Span source, Rml::Vector2i sourceDimensions) override { + ++Stats_.TextureGenerateRequests; + if (sourceDimensions.x <= 0 || sourceDimensions.y <= 0) { + return 0; + } + + const std::size_t width = static_cast(sourceDimensions.x); + const std::size_t height = static_cast(sourceDimensions.y); + const std::size_t expectedByteCount = width * height * 4U; + if (expectedByteCount == 0 || source.empty()) { + return 0; + } + + const Rml::TextureHandle handle = NextTextureHandle_++; + MetaCoreRmlTextureRecord record; + record.Handle = static_cast(handle); + record.Width = sourceDimensions.x; + record.Height = sourceDimensions.y; + record.Revision = NextTextureRevision_++; + record.Rgba.resize(expectedByteCount, 0); + const auto* sourceBytes = reinterpret_cast(source.data()); + const std::size_t copyByteCount = std::min(expectedByteCount, source.size()); + std::copy(sourceBytes, sourceBytes + copyByteCount, record.Rgba.begin()); + Textures_.emplace(handle, std::move(record)); + return handle; + } + + void ReleaseTexture(Rml::TextureHandle texture) override { + Textures_.erase(texture); + } + + void EnableScissorRegion(bool enable) override { + ScissorEnabled_ = enable; + } + + void SetScissorRegion(Rml::Rectanglei region) override { + ScissorRegion_ = region; + } + + void BeginFrame() { + Frame_ = MetaCoreRuntimeUiFrame{}; + SyncFrameStats(); + SyncTextureStats(); + } + + void SyncFrameStats() { + Stats_.DrawCommandCount = Frame_.Commands.size(); + Stats_.DrawVertexCount = Frame_.Vertices.size(); + Stats_.DrawIndexCount = Frame_.Indices.size(); + } + + void SyncTexturesToFrame() { + Frame_.Textures.clear(); + Frame_.Textures.reserve(Textures_.size()); + for (const auto& [handle, record] : Textures_) { + (void)handle; + if (record.Handle == 0 || record.Width <= 0 || record.Height <= 0 || record.Rgba.empty()) { + continue; + } + Frame_.Textures.push_back(MetaCoreRuntimeUiTexture{ + record.Handle, + record.Width, + record.Height, + record.Revision, + record.Rgba + }); + } + SyncTextureStats(); + } + + void SyncTextureStats() { + Stats_.TextureCount = Frame_.Textures.size(); + Stats_.TexturePixelCount = 0; + for (const MetaCoreRuntimeUiTexture& texture : Frame_.Textures) { + if (texture.Width > 0 && texture.Height > 0) { + Stats_.TexturePixelCount += static_cast(texture.Width) * static_cast(texture.Height); + } + } + } + +private: + struct MetaCoreRmlGeometryRecord { + std::vector Vertices{}; + std::vector Indices{}; + }; + + struct MetaCoreRmlTextureRecord { + std::uint64_t Handle = 0; + std::int32_t Width = 0; + std::int32_t Height = 0; + std::uint64_t Revision = 0; + std::vector Rgba{}; + }; + + MetaCoreRuntimeUiRenderStats& Stats_; + MetaCoreRuntimeUiFrame& Frame_; + Rml::CompiledGeometryHandle NextGeometryHandle_ = 1; + Rml::TextureHandle NextTextureHandle_ = 1; + std::uint64_t NextTextureRevision_ = 1; + std::unordered_map Geometries_{}; + std::unordered_map Textures_{}; + bool ScissorEnabled_ = false; + Rml::Rectanglei ScissorRegion_{}; +}; + +std::uint64_t MetaCoreNextRuntimeUiContextId() { + static std::uint64_t nextContextId = 1; + return nextContextId++; +} + +} // namespace + +struct MetaCoreRuntimeUiRenderer::MetaCoreRuntimeUiRendererImpl { + explicit MetaCoreRuntimeUiRendererImpl(MetaCoreRuntimeUiRenderStats& stats) + : RenderInterface(stats, Frame) {} + + MetaCoreCompiledRmlUiDocument Document{}; + MetaCoreRuntimeUiFrame Frame{}; + MetaCoreRuntimeUiRasterFrame RasterFrame{}; + MetaCoreRmlDiagnosticRenderInterface RenderInterface; + Rml::Context* Context = nullptr; + Rml::ElementDocument* LoadedDocument = nullptr; + std::string ContextName{}; + bool SharedRuntimeAcquired = false; +}; + +MetaCoreRuntimeUiRenderer::MetaCoreRuntimeUiRenderer() = default; + +MetaCoreRuntimeUiRenderer::~MetaCoreRuntimeUiRenderer() { + Shutdown(); +} + +bool MetaCoreRuntimeUiRenderer::Initialize() { + Shutdown(); + + Impl_ = std::make_unique(Stats_); + Impl_->SharedRuntimeAcquired = MetaCoreGetRmlSharedRuntime().Acquire(Stats_); + if (!Impl_->SharedRuntimeAcquired) { + Impl_.reset(); + return false; + } + + Impl_->ContextName = "MetaCoreRuntimeUi_" + std::to_string(MetaCoreNextRuntimeUiContextId()); + Impl_->Context = Rml::CreateContext( + Impl_->ContextName, + Rml::Vector2i(1, 1), + &Impl_->RenderInterface + ); + if (Impl_->Context == nullptr) { + Stats_.LastError = "RmlUi context creation failed"; + MetaCoreGetRmlSharedRuntime().Release(); + Impl_.reset(); + return false; + } + + Stats_.Initialized = true; + Stats_.RmlInitialized = true; + Stats_.RmlContextCreated = true; + Stats_.RmlDocumentLoaded = false; + Stats_.LastError.clear(); + return true; +} + +void MetaCoreRuntimeUiRenderer::Shutdown() { + if (Impl_ != nullptr) { + if (Impl_->Context != nullptr && Impl_->LoadedDocument != nullptr) { + Impl_->Context->UnloadDocument(Impl_->LoadedDocument); + Impl_->Context->Update(); + Impl_->LoadedDocument = nullptr; + } + + if (!Impl_->ContextName.empty()) { + (void)Rml::RemoveContext(Impl_->ContextName); + Impl_->Context = nullptr; + } + + if (Impl_->SharedRuntimeAcquired) { + Rml::ReleaseCompiledGeometry(&Impl_->RenderInterface); + Rml::ReleaseTextures(&Impl_->RenderInterface); + Rml::ReleaseRenderManagers(); + MetaCoreGetRmlSharedRuntime().Release(); + Impl_->SharedRuntimeAcquired = false; + } + } + + Impl_.reset(); + Stats_ = MetaCoreRuntimeUiRenderStats{}; +} + +bool MetaCoreRuntimeUiRenderer::LoadCompiledDocument(const MetaCoreCompiledRmlUiDocument& document) { + if (!Stats_.Initialized || Impl_ == nullptr || Impl_->Context == nullptr) { + Stats_.LastError = "Runtime UI renderer is not initialized"; + return false; + } + + if (document.Rml.empty()) { + Stats_.Loaded = false; + Stats_.RmlDocumentLoaded = false; + Stats_.RmlBytes = 0; + Stats_.RcssBytes = 0; + Stats_.LastError = "Compiled runtime UI RML is empty"; + return false; + } + + if (Impl_->LoadedDocument != nullptr) { + Impl_->Context->UnloadDocument(Impl_->LoadedDocument); + Impl_->Context->Update(); + Impl_->LoadedDocument = nullptr; + } + + Impl_->Document = document; + const std::string stylesheetHref = Impl_->Document.StylesheetHref.empty() + ? "MetaCoreRuntimeUi.rcss" + : Impl_->Document.StylesheetHref; + MetaCoreGetRmlSharedRuntime().FileInterface.SetVirtualFile(stylesheetHref, Impl_->Document.Rcss); + + Impl_->LoadedDocument = Impl_->Context->LoadDocumentFromMemory( + Impl_->Document.Rml, + "MetaCoreRuntimeUi.rml" + ); + if (Impl_->LoadedDocument == nullptr) { + Stats_.Loaded = false; + Stats_.RmlDocumentLoaded = false; + Stats_.LastError = "RmlUi failed to load compiled runtime UI document"; + return false; + } + + Impl_->LoadedDocument->Show(Rml::ModalFlag::None, Rml::FocusFlag::None); + Impl_->Context->Update(); + + Stats_.Loaded = true; + Stats_.RmlDocumentLoaded = true; + Stats_.RmlBytes = Impl_->Document.Rml.size(); + Stats_.RcssBytes = Impl_->Document.Rcss.size(); + Stats_.LastError.clear(); + return true; +} + +bool MetaCoreRuntimeUiRenderer::HasNode(std::string_view nodeId) const { + if (!Stats_.Initialized || !Stats_.Loaded || Impl_ == nullptr || Impl_->LoadedDocument == nullptr) { + return false; + } + + const std::string elementId = MetaCoreBuildRuntimeUiNodeElementId(nodeId); + return Impl_->LoadedDocument->GetElementById(elementId) != nullptr; +} + +bool MetaCoreRuntimeUiRenderer::SetNodeText(std::string_view nodeId, std::string_view text) { + if (!Stats_.Initialized || !Stats_.Loaded || Impl_ == nullptr || Impl_->LoadedDocument == nullptr) { + Stats_.LastError = "Runtime UI document is not loaded"; + return false; + } + + const std::string elementId = MetaCoreBuildRuntimeUiNodeElementId(nodeId); + Rml::Element* element = Impl_->LoadedDocument->GetElementById(elementId); + if (element == nullptr) { + Stats_.LastError = "Runtime UI node was not found: " + std::string(nodeId); + return false; + } + + element->SetInnerRML(MetaCoreEscapeRuntimeUiRmlText(text)); + if (Impl_->Context != nullptr) { + Impl_->Context->Update(); + } + Stats_.LastError.clear(); + return true; +} + +void MetaCoreRuntimeUiRenderer::Resize(std::int32_t width, std::int32_t height) { + Stats_.Width = std::max(0, width); + Stats_.Height = std::max(0, height); + if (Impl_ != nullptr && Impl_->Context != nullptr) { + Impl_->Context->SetDimensions(Rml::Vector2i( + std::max(1, width), + std::max(1, height) + )); + } +} + +void MetaCoreRuntimeUiRenderer::BeginFrame(float deltaSeconds) { + if (!Stats_.Initialized) { + return; + } + + Stats_.LastDeltaSeconds = deltaSeconds; + ++Stats_.FrameCount; + MetaCoreGetRmlSharedRuntime().SystemInterface.Advance(deltaSeconds); + + if (Impl_ != nullptr && Impl_->Context != nullptr) { + Impl_->Context->Update(); + } +} + +void MetaCoreRuntimeUiRenderer::Render() { + if (!Stats_.Initialized || !Stats_.Loaded || Impl_ == nullptr || Impl_->Context == nullptr) { + return; + } + + Impl_->RenderInterface.BeginFrame(); + if (!Impl_->Context->Render()) { + Stats_.LastError = "RmlUi context render failed"; + } + Impl_->RenderInterface.SyncTexturesToFrame(); + MetaCoreRuntimeUiRasterizeFrame( + Impl_->Frame, + Impl_->RasterFrame, + Stats_, + Stats_.Width, + Stats_.Height + ); +} + +const MetaCoreRuntimeUiRenderStats& MetaCoreRuntimeUiRenderer::GetStats() const { + return Stats_; +} + +const MetaCoreRuntimeUiFrame& MetaCoreRuntimeUiRenderer::GetLastFrame() const { + static const MetaCoreRuntimeUiFrame emptyFrame{}; + return Impl_ == nullptr ? emptyFrame : Impl_->Frame; +} + +const MetaCoreRuntimeUiRasterFrame& MetaCoreRuntimeUiRenderer::GetLastRasterFrame() const { + static const MetaCoreRuntimeUiRasterFrame emptyRasterFrame{}; + return Impl_ == nullptr ? emptyRasterFrame : Impl_->RasterFrame; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h index 768626d..9ff85d9 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h @@ -12,6 +12,7 @@ namespace MetaCore { class MetaCoreRenderDevice; class MetaCoreWindow; class MetaCoreScene; +struct MetaCoreRuntimeUiFrame; class MetaCoreEditorViewportRenderer { public: @@ -19,6 +20,7 @@ public: void Shutdown(); void SetViewportRect(const MetaCoreViewportRect& viewportRect); void RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera = false); + void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height); void RenderAll(); void SetProjectRootPath(const std::filesystem::path& projectRootPath); [[nodiscard]] uint32_t GetFilamentGLTextureId() const; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h index 6f30872..db25b2f 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h @@ -14,6 +14,7 @@ class MetaCoreWindow; class MetaCoreScene; struct MetaCoreSceneView; struct MetaCoreSceneRenderSyncSnapshot; +struct MetaCoreRuntimeUiFrame; /** * @brief Filament 场景桥接器,负责将 MetaCore 的场景数据同步到 Filament 渲染引擎中 @@ -30,6 +31,7 @@ public: void SyncScene(MetaCoreScene& scene, bool compatibilityMeshOnly = false, bool useScenePrimaryCamera = false); void SyncScene(const MetaCoreSceneRenderSyncSnapshot& snapshot, MetaCoreScene* scene = nullptr, bool compatibilityMeshOnly = false, bool useScenePrimaryCamera = false); void ApplySceneView(const MetaCoreSceneView& sceneView); + void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height); void RenderAll(); [[nodiscard]] uint32_t GetGLTextureId() const; [[nodiscard]] void* GetFilamentTexturePointer() const; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h index 67dfe20..12f3656 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreImGuiHelper.h @@ -10,9 +10,11 @@ #include #include +#include #include #include #include +#include #include struct ImDrawData; @@ -21,6 +23,8 @@ struct ImGuiContext; namespace MetaCore { +struct MetaCoreRuntimeUiFrame; + class MetaCoreImGuiHelper { public: using Callback = std::function; @@ -35,17 +39,27 @@ public: void render(float timeStepInSeconds, Callback imguiCommands); void processImGuiCommands(ImDrawData* commands, const ImGuiIO& io); + [[nodiscard]] bool processRuntimeUiFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height); void createAtlasTexture(filament::Engine* engine); filament::View* getView() const { return mView; } private: + struct RuntimeUiTextureState { + filament::Texture* Texture = nullptr; + std::int32_t Width = 0; + std::int32_t Height = 0; + std::uint64_t Revision = 0; + }; + void createBuffers(int numRequiredBuffers); void populateVertexData(size_t bufferIndex, size_t vbSizeInBytes, void* vbData, size_t ibSizeInBytes, void* ibData); void createVertexBuffer(size_t bufferIndex, size_t capacity); void createIndexBuffer(size_t bufferIndex, size_t capacity); + [[nodiscard]] filament::Texture* getWhiteTexture(); + [[nodiscard]] filament::Texture* syncRuntimeUiTexture(const MetaCoreRuntimeUiFrame& frame, std::uint64_t handle); void syncThreads(); filament::Engine* mEngine; @@ -59,6 +73,7 @@ private: utils::Entity mRenderable; utils::Entity mCameraEntity; filament::Texture* mTexture = nullptr; + filament::Texture* mWhiteTexture = nullptr; bool mHasSynced = false; ImGuiContext* mImGuiContext; filament::TextureSampler mSampler; @@ -66,6 +81,7 @@ private: bool mOwnsImGuiContext = false; std::filesystem::path mSettingsPath; std::unordered_set mImGuiTextures; + std::unordered_map mRuntimeUiTextures; }; } // namespace MetaCore diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRuntimeUiRenderer.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRuntimeUiRenderer.h new file mode 100644 index 0000000..cb6effe --- /dev/null +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreRuntimeUiRenderer.h @@ -0,0 +1,115 @@ +#pragma once + +#include "MetaCoreScene/MetaCoreUiRmlCompiler.h" + +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +struct MetaCoreRuntimeUiDrawVertex { + float X = 0.0F; + float Y = 0.0F; + float U = 0.0F; + float V = 0.0F; + std::uint8_t R = 255; + std::uint8_t G = 255; + std::uint8_t B = 255; + std::uint8_t A = 255; +}; + +struct MetaCoreRuntimeUiDrawCommand { + std::size_t VertexOffset = 0; + std::size_t VertexCount = 0; + std::size_t IndexOffset = 0; + std::size_t IndexCount = 0; + std::uint64_t TextureHandle = 0; + bool ScissorEnabled = false; + std::int32_t ScissorLeft = 0; + std::int32_t ScissorTop = 0; + std::int32_t ScissorRight = 0; + std::int32_t ScissorBottom = 0; +}; + +struct MetaCoreRuntimeUiTexture { + std::uint64_t Handle = 0; + std::int32_t Width = 0; + std::int32_t Height = 0; + std::uint64_t Revision = 0; + std::vector Rgba{}; +}; + +struct MetaCoreRuntimeUiFrame { + std::vector Vertices{}; + std::vector Indices{}; + std::vector Commands{}; + std::vector Textures{}; +}; + +struct MetaCoreRuntimeUiRasterFrame { + std::int32_t Width = 0; + std::int32_t Height = 0; + std::vector Rgba{}; +}; + +struct MetaCoreRuntimeUiRenderStats { + bool Initialized = false; + bool Loaded = false; + bool RmlInitialized = false; + bool RmlContextCreated = false; + bool RmlDocumentLoaded = false; + std::int32_t Width = 0; + std::int32_t Height = 0; + std::size_t RmlBytes = 0; + std::size_t RcssBytes = 0; + std::uint64_t FrameCount = 0; + std::uint64_t RenderGeometryCalls = 0; + std::uint64_t TextureLoadRequests = 0; + std::uint64_t TextureGenerateRequests = 0; + std::size_t CompiledGeometryCount = 0; + std::size_t DrawCommandCount = 0; + std::size_t DrawVertexCount = 0; + std::size_t DrawIndexCount = 0; + std::size_t TextureCount = 0; + std::size_t TexturePixelCount = 0; + std::size_t RasterPixelCount = 0; + std::size_t RasterTouchedPixelCount = 0; + float LastDeltaSeconds = 0.0F; + std::string LastError{}; +}; + +class MetaCoreRuntimeUiRenderer { +public: + MetaCoreRuntimeUiRenderer(); + ~MetaCoreRuntimeUiRenderer(); + + MetaCoreRuntimeUiRenderer(const MetaCoreRuntimeUiRenderer&) = delete; + MetaCoreRuntimeUiRenderer& operator=(const MetaCoreRuntimeUiRenderer&) = delete; + MetaCoreRuntimeUiRenderer(MetaCoreRuntimeUiRenderer&&) noexcept = delete; + MetaCoreRuntimeUiRenderer& operator=(MetaCoreRuntimeUiRenderer&&) noexcept = delete; + + [[nodiscard]] bool Initialize(); + void Shutdown(); + + [[nodiscard]] bool LoadCompiledDocument(const MetaCoreCompiledRmlUiDocument& document); + [[nodiscard]] bool HasNode(std::string_view nodeId) const; + [[nodiscard]] bool SetNodeText(std::string_view nodeId, std::string_view text); + void Resize(std::int32_t width, std::int32_t height); + void BeginFrame(float deltaSeconds); + void Render(); + + [[nodiscard]] const MetaCoreRuntimeUiRenderStats& GetStats() const; + [[nodiscard]] const MetaCoreRuntimeUiFrame& GetLastFrame() const; + [[nodiscard]] const MetaCoreRuntimeUiRasterFrame& GetLastRasterFrame() const; + +private: + struct MetaCoreRuntimeUiRendererImpl; + std::unique_ptr Impl_{}; + MetaCoreRuntimeUiRenderStats Stats_{}; +}; + +} // namespace MetaCore diff --git a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp index 66558e0..58efc12 100644 --- a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp +++ b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataDispatcher.cpp @@ -31,6 +31,15 @@ void MetaCoreRuntimeDataDispatcher::ApplyUpdates(const std::vectorValueType) { + MarkBindingFault(bindingDefinition.BindingId, "Update type does not match data point definition"); + continue; + } + if (update.Value.Quality == MetaCoreRuntimeDataQuality::Bad) { + MarkBindingFault(bindingDefinition.BindingId, "Update quality is bad"); + continue; + } + MetaCoreGameObject gameObject = Scene_.FindGameObject(bindingDefinition.TargetObjectId); if (!gameObject) { MarkBindingFault(bindingDefinition.BindingId, "Target object missing"); diff --git a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp index 80f8c06..72bbb17 100644 --- a/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp +++ b/Source/MetaCoreRuntimeData/Private/MetaCoreRuntimeDataProject.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace MetaCore { @@ -57,6 +58,33 @@ template return document; } +[[nodiscard]] MetaCoreRuntimeValueType MetaCoreExpectedValueTypeForBindingTarget( + MetaCoreRuntimeBindingTarget target +) { + switch (target) { + case MetaCoreRuntimeBindingTarget::MeshRendererVisible: + return MetaCoreRuntimeValueType::Bool; + case MetaCoreRuntimeBindingTarget::LightIntensity: + return MetaCoreRuntimeValueType::Double; + case MetaCoreRuntimeBindingTarget::TransformPosition: + case MetaCoreRuntimeBindingTarget::MeshRendererBaseColor: + case MetaCoreRuntimeBindingTarget::LightColor: + default: + return MetaCoreRuntimeValueType::Vec3; + } +} + +[[nodiscard]] const char* MetaCoreRuntimeValueTypeName(MetaCoreRuntimeValueType type) { + switch (type) { + case MetaCoreRuntimeValueType::Bool: return "Bool"; + case MetaCoreRuntimeValueType::Int64: return "Int64"; + case MetaCoreRuntimeValueType::Double: return "Double"; + case MetaCoreRuntimeValueType::String: return "String"; + case MetaCoreRuntimeValueType::Vec3: return "Vec3"; + } + return "Unknown"; +} + } // namespace bool MetaCoreWriteRuntimeDataSourcesDocument( @@ -104,6 +132,109 @@ std::optional MetaCoreReadRuntimeProjectDocument return MetaCoreReadBinaryDocument(path, registry); } +bool MetaCoreIsUnsafeRuntimeProjectPath(const std::filesystem::path& path) { + if (path.is_absolute()) { + return true; + } + + for (const auto& part : path.lexically_normal()) { + if (part == "..") { + return true; + } + } + return false; +} + +std::filesystem::path MetaCoreBuildRuntimeDirectoryRelativePath( + const std::filesystem::path& projectRoot, + const std::filesystem::path& runtimeDirectory +) { + std::filesystem::path relativeRuntimeDirectory = !runtimeDirectory.empty() + ? runtimeDirectory.lexically_relative(projectRoot) + : std::filesystem::path("Runtime"); + if (relativeRuntimeDirectory.empty() || + relativeRuntimeDirectory == "." || + MetaCoreIsUnsafeRuntimeProjectPath(relativeRuntimeDirectory)) { + relativeRuntimeDirectory = "Runtime"; + } + return relativeRuntimeDirectory.lexically_normal(); +} + +MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument( + const std::filesystem::path& runtimeDirectoryRelative +) { + MetaCoreRuntimeProjectDocument document; + document.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + document.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; + document.BuildProfileName = "Development"; + document.TargetPlatform = "Windows"; + document.OutputDirectory = std::filesystem::path("Build") / "Windows"; + document.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; + document.UseCookedAssets = false; + document.DataSourcesPath = runtimeDirectoryRelative / "DataSources.mcruntime"; + document.BindingsPath = runtimeDirectoryRelative / "Bindings.mcruntime"; + document.DiagnosticsPath = runtimeDirectoryRelative / "Diagnostics.mcruntimestate"; + return document; +} + +void MetaCoreApplyRuntimeProjectDefaults( + MetaCoreRuntimeProjectDocument& document, + const std::filesystem::path& runtimeDirectoryRelative +) { + const MetaCoreRuntimeProjectDocument defaults = + MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative); + if (document.StartupScenePath.empty()) { + document.StartupScenePath = defaults.StartupScenePath; + } + if (document.StartupUiPath.empty()) { + document.StartupUiPath = defaults.StartupUiPath; + } + if (document.BuildProfileName.empty()) { + document.BuildProfileName = defaults.BuildProfileName; + } + if (document.TargetPlatform.empty()) { + document.TargetPlatform = defaults.TargetPlatform; + } + if (document.OutputDirectory.empty()) { + document.OutputDirectory = defaults.OutputDirectory; + } + if (document.CookedAssetsDirectory.empty()) { + document.CookedAssetsDirectory = defaults.CookedAssetsDirectory; + } + if (document.DataSourcesPath.empty()) { + document.DataSourcesPath = defaults.DataSourcesPath; + } + if (document.BindingsPath.empty()) { + document.BindingsPath = defaults.BindingsPath; + } + if (document.DiagnosticsPath.empty()) { + document.DiagnosticsPath = defaults.DiagnosticsPath; + } +} + +std::vector MetaCoreValidateRuntimeProjectPaths( + const MetaCoreRuntimeProjectDocument& document +) { + std::vector issues; + const auto validatePath = [&](const std::filesystem::path& path, const char* label) { + if (!path.empty() && MetaCoreIsUnsafeRuntimeProjectPath(path)) { + issues.push_back(MetaCoreRuntimeConfigIssue{ + MetaCoreRuntimeConfigIssueSeverity::Error, + "RuntimeProject", + std::string(label) + " must be a project-relative path without parent traversal" + }); + } + }; + + validatePath(document.StartupScenePath, "StartupScenePath"); + validatePath(document.StartupUiPath, "StartupUiPath"); + validatePath(document.CookedAssetsDirectory, "CookedAssetsDirectory"); + validatePath(document.DataSourcesPath, "DataSourcesPath"); + validatePath(document.BindingsPath, "BindingsPath"); + validatePath(document.DiagnosticsPath, "DiagnosticsPath"); + return issues; +} + bool MetaCoreWriteRuntimeDiagnosticsSnapshot( const std::filesystem::path& path, const MetaCoreRuntimeDiagnosticsSnapshot& snapshot, @@ -164,6 +295,7 @@ std::vector MetaCoreValidateRuntimeDataDocuments( } std::unordered_set dataPointIds; + std::unordered_map dataPointValueTypes; for (const auto& dataPoint : sourcesDocument.DataPoints) { if (dataPoint.Id.empty()) { issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, "DataPoint", "存在空的 DataPointId"}); @@ -175,6 +307,7 @@ std::vector MetaCoreValidateRuntimeDataDocuments( if (dataPoint.SourceId.empty() || !sourceIds.contains(dataPoint.SourceId)) { issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, dataPoint.Id, "DataPoint 引用了不存在的 SourceId"}); } + dataPointValueTypes[dataPoint.Id] = dataPoint.ValueType; if (dataPoint.ExternalAddress.empty()) { issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Warning, dataPoint.Id, "ExternalAddress 为空"}); } @@ -192,11 +325,40 @@ std::vector MetaCoreValidateRuntimeDataDocuments( if (binding.DataPointId.empty() || !dataPointIds.contains(binding.DataPointId)) { issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "Binding 引用了不存在的 DataPointId"}); } + else if (const auto dataPointType = dataPointValueTypes.find(binding.DataPointId); + dataPointType != dataPointValueTypes.end()) { + const MetaCoreRuntimeValueType expectedType = + MetaCoreExpectedValueTypeForBindingTarget(binding.Target); + if (dataPointType->second != expectedType) { + issues.push_back({ + MetaCoreRuntimeConfigIssueSeverity::Error, + binding.BindingId, + "Binding target expects " + std::string(MetaCoreRuntimeValueTypeName(expectedType)) + + " but DataPoint is " + MetaCoreRuntimeValueTypeName(dataPointType->second) + }); + } + } if (binding.TargetObjectId == 0) { issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "Binding 缺少 TargetObjectId"}); } } + for (const auto& binding : bindingsDocument.UiBindings) { + if (binding.BindingId.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, "UiBinding", "UI BindingId is empty"}); + continue; + } + if (!bindingIds.insert(binding.BindingId).second) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "BindingId duplicate"}); + } + if (binding.DataPointId.empty() || !dataPointIds.contains(binding.DataPointId)) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "UI binding references a missing DataPointId"}); + } + if (binding.TargetNodeId.empty()) { + issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Error, binding.BindingId, "UI binding missing TargetNodeId"}); + } + } + if (sourcesDocument.Sources.empty()) { issues.push_back({MetaCoreRuntimeConfigIssueSeverity::Warning, "Runtime", "当前没有任何 Source"}); } diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h index 4c10b86..fdde12e 100644 --- a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -82,6 +83,26 @@ struct MetaCoreSceneBindingDefinition { MetaCoreRuntimeMissingDataPolicy MissingDataPolicy = MetaCoreRuntimeMissingDataPolicy::KeepLastValue; }; +MC_STRUCT() +struct MetaCoreUiBindingDefinition { + MC_GENERATED_BODY() + + MC_PROPERTY() + std::string BindingId{}; + + MC_PROPERTY() + std::string DataPointId{}; + + MC_PROPERTY() + std::string TargetNodeId{}; + + MC_PROPERTY() + MetaCoreRuntimeUiBindingTarget Target = MetaCoreRuntimeUiBindingTarget::Text; + + MC_PROPERTY() + MetaCoreRuntimeMissingDataPolicy MissingDataPolicy = MetaCoreRuntimeMissingDataPolicy::KeepLastValue; +}; + MC_STRUCT() struct MetaCoreRuntimeDataSourcesDocument { MC_GENERATED_BODY() @@ -99,6 +120,9 @@ struct MetaCoreRuntimeBindingsDocument { MC_PROPERTY() std::vector Bindings{}; + + MC_PROPERTY() + std::vector UiBindings{}; }; MC_STRUCT() @@ -108,6 +132,24 @@ struct MetaCoreRuntimeProjectDocument { MC_PROPERTY() std::filesystem::path StartupScenePath{}; + MC_PROPERTY() + std::filesystem::path StartupUiPath{}; + + MC_PROPERTY() + std::string BuildProfileName{"Development"}; + + MC_PROPERTY() + std::string TargetPlatform{"Windows"}; + + MC_PROPERTY() + std::filesystem::path OutputDirectory{}; + + MC_PROPERTY() + std::filesystem::path CookedAssetsDirectory{}; + + MC_PROPERTY() + bool UseCookedAssets = false; + MC_PROPERTY() std::filesystem::path DataSourcesPath{}; @@ -151,6 +193,26 @@ struct MetaCoreRuntimeProjectDocument { const MetaCoreTypeRegistry& registry ); +[[nodiscard]] bool MetaCoreIsUnsafeRuntimeProjectPath(const std::filesystem::path& path); + +[[nodiscard]] std::filesystem::path MetaCoreBuildRuntimeDirectoryRelativePath( + const std::filesystem::path& projectRoot, + const std::filesystem::path& runtimeDirectory +); + +[[nodiscard]] MetaCoreRuntimeProjectDocument MetaCoreBuildDefaultRuntimeProjectDocument( + const std::filesystem::path& runtimeDirectoryRelative = std::filesystem::path("Runtime") +); + +void MetaCoreApplyRuntimeProjectDefaults( + MetaCoreRuntimeProjectDocument& document, + const std::filesystem::path& runtimeDirectoryRelative = std::filesystem::path("Runtime") +); + +[[nodiscard]] std::vector MetaCoreValidateRuntimeProjectPaths( + const MetaCoreRuntimeProjectDocument& document +); + [[nodiscard]] bool MetaCoreWriteRuntimeDiagnosticsSnapshot( const std::filesystem::path& path, const MetaCoreRuntimeDiagnosticsSnapshot& snapshot, diff --git a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h index 8c72ba6..ef7e341 100644 --- a/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h +++ b/Source/MetaCoreRuntimeData/Public/MetaCoreRuntimeData/MetaCoreRuntimeDataTypes.h @@ -5,6 +5,7 @@ #include #include +#include #include @@ -44,6 +45,11 @@ enum class MetaCoreRuntimeBindingTarget : std::uint32_t { LightColor }; +MC_ENUM() +enum class MetaCoreRuntimeUiBindingTarget : std::uint32_t { + Text = 0 +}; + MC_ENUM() enum class MetaCoreRuntimeMissingDataPolicy : std::uint32_t { KeepLastValue = 0, diff --git a/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp b/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp index d79f6ba..6ddc327 100644 --- a/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreScenePackage.cpp @@ -4,6 +4,7 @@ #include "MetaCoreFoundation/MetaCoreHash.h" #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreProject.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" #include #include @@ -155,6 +156,9 @@ std::optional MetaCoreLoadStartupSceneDocument(const std: if (!startupScenePath.has_value()) { return std::nullopt; } + if (startupScenePath->extension() == ".json") { + return MetaCoreSceneSerializer::LoadSceneFromJson(*startupScenePath, MetaCoreBuildScenePackageTypeRegistry()); + } return MetaCoreReadScenePackage(*startupScenePath); } diff --git a/Source/MetaCoreScene/Private/MetaCoreUiRmlCompiler.cpp b/Source/MetaCoreScene/Private/MetaCoreUiRmlCompiler.cpp new file mode 100644 index 0000000..a12f0f6 --- /dev/null +++ b/Source/MetaCoreScene/Private/MetaCoreUiRmlCompiler.cpp @@ -0,0 +1,279 @@ +#include "MetaCoreScene/MetaCoreUiRmlCompiler.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace MetaCore { + +namespace { + +[[nodiscard]] std::string MetaCoreEscapeRml(std::string_view value) { + std::string escaped; + escaped.reserve(value.size()); + for (char c : value) { + switch (c) { + case '&': escaped += "&"; break; + case '<': escaped += "<"; break; + case '>': escaped += ">"; break; + case '"': escaped += """; break; + case '\'': escaped += "'"; break; + default: escaped.push_back(c); break; + } + } + return escaped; +} + +[[nodiscard]] std::string MetaCoreSanitizeIdentifier(std::string_view value, std::string_view fallback) { + std::string sanitized; + sanitized.reserve(value.size()); + for (char c : value) { + const auto uc = static_cast(c); + if (std::isalnum(uc) != 0) { + sanitized.push_back(static_cast(std::tolower(uc))); + } else if (c == '_' || c == '-' || c == '.') { + sanitized.push_back(c == '.' ? '-' : c); + } else { + sanitized.push_back('-'); + } + } + + while (!sanitized.empty() && sanitized.front() == '-') { + sanitized.erase(sanitized.begin()); + } + while (!sanitized.empty() && sanitized.back() == '-') { + sanitized.pop_back(); + } + if (sanitized.empty()) { + sanitized = std::string(fallback); + } + if (std::isdigit(static_cast(sanitized.front())) != 0) { + sanitized.insert(sanitized.begin(), 'n'); + } + return sanitized; +} + +[[nodiscard]] std::string MetaCoreUiNodeTypeClass(MetaCoreUiNodeType type) { + switch (type) { + case MetaCoreUiNodeType::Text: return "mcui-text"; + case MetaCoreUiNodeType::Image: return "mcui-image"; + case MetaCoreUiNodeType::Button: return "mcui-button"; + case MetaCoreUiNodeType::Panel: + default: return "mcui-panel"; + } +} + +[[nodiscard]] std::string MetaCoreUiTagName(MetaCoreUiNodeType type) { + switch (type) { + case MetaCoreUiNodeType::Text: return "p"; + case MetaCoreUiNodeType::Button: return "button"; + case MetaCoreUiNodeType::Image: + case MetaCoreUiNodeType::Panel: + default: return "div"; + } +} + +[[nodiscard]] int MetaCoreColorChannel(float value) { + const float clamped = std::clamp(value, 0.0F, 1.0F); + return static_cast(std::round(clamped * 255.0F)); +} + +[[nodiscard]] std::string MetaCoreRcssColor(const glm::vec3& color) { + std::ostringstream stream; + stream << "rgb(" + << MetaCoreColorChannel(color.r) << ", " + << MetaCoreColorChannel(color.g) << ", " + << MetaCoreColorChannel(color.b) << ")"; + return stream.str(); +} + +[[nodiscard]] std::string MetaCoreRcssTextAlign(MetaCoreUiHorizontalAlignment alignment) { + switch (alignment) { + case MetaCoreUiHorizontalAlignment::Center: return "center"; + case MetaCoreUiHorizontalAlignment::Right: return "right"; + case MetaCoreUiHorizontalAlignment::Stretch: + case MetaCoreUiHorizontalAlignment::Left: + default: return "left"; + } +} + +[[nodiscard]] bool MetaCoreIsStretch(float minAnchor, float maxAnchor, float size) { + return std::abs(minAnchor) <= 0.0001F && + std::abs(maxAnchor - 1.0F) <= 0.0001F && + std::abs(size) <= 0.0001F; +} + +void MetaCoreAppendNodeRcss( + std::ostringstream& output, + const MetaCoreUiNodeDocument& node, + std::string_view className +) { + output << "." << className << " {\n"; + output << " position: absolute;\n"; + output << " box-sizing: border-box;\n"; + if (!node.Visible) { + output << " display: none;\n"; + } + + const auto& rect = node.RectTransform; + if (MetaCoreIsStretch(rect.AnchorMin.x, rect.AnchorMax.x, rect.Size.x)) { + output << " left: 0px;\n"; + output << " right: 0px;\n"; + } else { + output << " left: " << rect.Position.x << "px;\n"; + output << " width: " << std::max(rect.Size.x, 0.0F) << "px;\n"; + } + + if (MetaCoreIsStretch(rect.AnchorMin.y, rect.AnchorMax.y, rect.Size.y)) { + output << " top: 0px;\n"; + output << " bottom: 0px;\n"; + } else { + output << " top: " << rect.Position.y << "px;\n"; + output << " height: " << std::max(rect.Size.y, 0.0F) << "px;\n"; + } + + output << " padding: " << std::max(node.Style.Padding.y, 0.0F) + << "px " << std::max(node.Style.Padding.x, 0.0F) << "px;\n"; + output << " background-color: " << MetaCoreRcssColor(node.Style.BackgroundColor) << ";\n"; + output << " color: " << MetaCoreRcssColor(node.Style.TextColor) << ";\n"; + output << " font-size: " << std::max(node.Style.FontSize, 1.0F) << "px;\n"; + output << " text-align: " << MetaCoreRcssTextAlign(node.Style.HorizontalAlignment) << ";\n"; + + if (node.Type == MetaCoreUiNodeType::Button) { + output << " border-width: 0px;\n"; + } + if (node.Type == MetaCoreUiNodeType::Image) { + output << " background-color: " << MetaCoreRcssColor(node.Style.TintColor) << ";\n"; + } + + output << "}\n\n"; +} + +void MetaCoreAppendRmlNode( + std::ostringstream& output, + const MetaCoreUiNodeDocument& node, + const std::unordered_map& nodesById, + const std::unordered_map& classById, + std::unordered_set& activeStack, + int depth +) { + if (activeStack.contains(node.Id)) { + return; + } + activeStack.insert(node.Id); + + const std::string indent(static_cast(depth) * 2U, ' '); + const std::string tagName = MetaCoreUiTagName(node.Type); + const auto classIterator = classById.find(node.Id); + const std::string className = classIterator != classById.end() ? classIterator->second : "mcui-node"; + const std::string nodeId = MetaCoreBuildRuntimeUiNodeElementId(node.Id); + + output << indent << "<" << tagName + << " id=\"" << MetaCoreEscapeRml(nodeId) << "\"" + << " class=\"mcui-node " << MetaCoreUiNodeTypeClass(node.Type) << " " << className << "\"" + << " data-metacore-id=\"" << MetaCoreEscapeRml(node.Id) << "\""; + if (node.Type == MetaCoreUiNodeType::Image && node.Style.ImageAssetGuid.IsValid()) { + output << " data-image-guid=\"" << MetaCoreEscapeRml(node.Style.ImageAssetGuid.ToString()) << "\""; + } + output << ">"; + + if (node.Type == MetaCoreUiNodeType::Text || node.Type == MetaCoreUiNodeType::Button) { + output << MetaCoreEscapeRml(node.Text); + } + + if (!node.Children.empty()) { + output << "\n"; + for (const std::string& childId : node.Children) { + const auto childIterator = nodesById.find(childId); + if (childIterator != nodesById.end()) { + MetaCoreAppendRmlNode(output, *childIterator->second, nodesById, classById, activeStack, depth + 1); + } + } + output << indent; + } + + output << "\n"; + activeStack.erase(node.Id); +} + +} // namespace + +std::string MetaCoreBuildRuntimeUiNodeElementId(std::string_view nodeId) { + return MetaCoreSanitizeIdentifier(nodeId, "node"); +} + +MetaCoreCompiledRmlUiDocument MetaCoreCompileUiDocumentToRml( + const MetaCoreUiDocument& document, + std::string_view stylesheetHref +) { + std::unordered_map nodesById; + std::unordered_map classById; + nodesById.reserve(document.Nodes.size()); + classById.reserve(document.Nodes.size()); + + for (std::size_t index = 0; index < document.Nodes.size(); ++index) { + const MetaCoreUiNodeDocument& node = document.Nodes[index]; + if (!node.Id.empty()) { + nodesById[node.Id] = &node; + classById[node.Id] = "mcui-node-" + std::to_string(index); + } + } + + MetaCoreCompiledRmlUiDocument compiled; + compiled.StylesheetHref = std::string(stylesheetHref); + + std::ostringstream rml; + rml << "\n"; + rml << " \n"; + rml << " \n"; + rml << " \n"; + rml << " \n"; + rml << "
\n"; + + std::unordered_set activeStack; + for (const std::string& rootId : document.RootNodeIds) { + const auto rootIterator = nodesById.find(rootId); + if (rootIterator != nodesById.end()) { + MetaCoreAppendRmlNode(rml, *rootIterator->second, nodesById, classById, activeStack, 3); + } + } + + rml << "
\n"; + rml << " \n"; + rml << "
\n"; + compiled.Rml = rml.str(); + + std::ostringstream rcss; + rcss << "body {\n"; + rcss << " margin: 0px;\n"; + rcss << " padding: 0px;\n"; + rcss << "}\n\n"; + rcss << ".mcui-document {\n"; + rcss << " position: relative;\n"; + rcss << " width: " << std::max(document.ReferenceWidth, 1) << "px;\n"; + rcss << " height: " << std::max(document.ReferenceHeight, 1) << "px;\n"; + rcss << "}\n\n"; + rcss << ".mcui-node {\n"; + rcss << " overflow: hidden;\n"; + rcss << "}\n\n"; + + for (const MetaCoreUiNodeDocument& node : document.Nodes) { + if (node.Id.empty()) { + continue; + } + const auto classIterator = classById.find(node.Id); + if (classIterator != classById.end()) { + MetaCoreAppendNodeRcss(rcss, node, classIterator->second); + } + } + compiled.Rcss = rcss.str(); + + return compiled; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h index 5626ec8..aa578c7 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -35,11 +35,11 @@ MC_STRUCT() struct MetaCoreTransformComponent { MC_GENERATED_BODY() - MC_PROPERTY() + MC_PROPERTY(DisplayName = "Position", Group = "Transform") glm::vec3 Position{0.0F, 0.0F, 0.0F}; - MC_PROPERTY() + MC_PROPERTY(DisplayName = "Rotation", Group = "Transform") glm::vec3 RotationEulerDegrees{0.0F, 0.0F, 0.0F}; - MC_PROPERTY() + MC_PROPERTY(DisplayName = "Scale", Group = "Transform") glm::vec3 Scale{1.0F, 1.0F, 1.0F}; }; diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreUiRmlCompiler.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreUiRmlCompiler.h new file mode 100644 index 0000000..1c11017 --- /dev/null +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreUiRmlCompiler.h @@ -0,0 +1,23 @@ +#pragma once + +#include "MetaCoreScene/MetaCoreSceneDocument.h" + +#include +#include + +namespace MetaCore { + +struct MetaCoreCompiledRmlUiDocument { + std::string Rml{}; + std::string Rcss{}; + std::string StylesheetHref{}; +}; + +[[nodiscard]] std::string MetaCoreBuildRuntimeUiNodeElementId(std::string_view nodeId); + +[[nodiscard]] MetaCoreCompiledRmlUiDocument MetaCoreCompileUiDocumentToRml( + const MetaCoreUiDocument& document, + std::string_view stylesheetHref = "MetaCoreRuntimeUi.rcss" +); + +} // namespace MetaCore diff --git a/TestProject/Assets/UI/Hud.mcui.json b/TestProject/Assets/UI/Hud.mcui.json new file mode 100644 index 0000000..30b6a45 --- /dev/null +++ b/TestProject/Assets/UI/Hud.mcui.json @@ -0,0 +1,126 @@ +{ + "Name": "RuntimeHud", + "ReferenceWidth": 1280, + "ReferenceHeight": 720, + "RootNodeIds": [ + "hud.root" + ], + "Nodes": [ + { + "Id": "hud.root", + "Name": "HUD Root", + "Type": 0, + "ParentId": "", + "Children": [ + "hud.title", + "hud.status", + "hud.button" + ], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [24.0, 24.0, 0.0], + "Size": [360.0, 164.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.05, 0.08, 0.12], + "TextColor": [1.0, 1.0, 1.0], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 16.0, + "Padding": [12.0, 12.0, 0.0], + "HorizontalAlignment": 0, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "", + "Interactable": false + }, + { + "Id": "hud.title", + "Name": "Title", + "Type": 1, + "ParentId": "hud.root", + "Children": [], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [16.0, 14.0, 0.0], + "Size": [320.0, 36.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.08, 0.11, 0.16], + "TextColor": [0.95, 0.98, 1.0], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 24.0, + "Padding": [8.0, 4.0, 0.0], + "HorizontalAlignment": 0, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "MetaCore Runtime", + "Interactable": false + }, + { + "Id": "hud.status", + "Name": "Status Strip", + "Type": 0, + "ParentId": "hud.root", + "Children": [], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [16.0, 62.0, 0.0], + "Size": [220.0, 34.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.12, 0.42, 0.82], + "TextColor": [1.0, 1.0, 1.0], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 16.0, + "Padding": [8.0, 4.0, 0.0], + "HorizontalAlignment": 0, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "", + "Interactable": false + }, + { + "Id": "hud.button", + "Name": "Action Button", + "Type": 3, + "ParentId": "hud.root", + "Children": [], + "Visible": true, + "RectTransform": { + "AnchorMin": [0.0, 0.0, 0.0], + "AnchorMax": [0.0, 0.0, 0.0], + "Pivot": [0.0, 0.0, 0.0], + "Position": [16.0, 110.0, 0.0], + "Size": [144.0, 38.0, 0.0] + }, + "Style": { + "BackgroundColor": [0.18, 0.72, 0.36], + "TextColor": [0.02, 0.04, 0.03], + "TintColor": [1.0, 1.0, 1.0], + "FontSize": 17.0, + "Padding": [10.0, 5.0, 0.0], + "HorizontalAlignment": 1, + "VerticalAlignment": 0, + "ImageAssetGuid": "", + "PreserveAspect": false + }, + "Text": "Run", + "Interactable": true + } + ] +} diff --git a/docs/designs/metacore-infernux-reference-roadmap.md b/docs/designs/metacore-infernux-reference-roadmap.md new file mode 100644 index 0000000..34e2e83 --- /dev/null +++ b/docs/designs/metacore-infernux-reference-roadmap.md @@ -0,0 +1,69 @@ +# MetaCore Infernux Reference Roadmap + +Status: active implementation reference +Updated: 2026-06-01 + +## Goal + +Use the local Infernux source tree as a Unity-like behavior reference while keeping MetaCore's native architecture intact. + +MetaCore keeps: + +- C++20 static components as the first scripting surface. +- EnTT scene storage with GameObject/Component editor semantics. +- Filament rendering. +- ImGui editor UI. +- RmlUi runtime UI. +- JSON authoring assets and Cooked runtime assets. + +Infernux is used only to understand complete engine workflows. It is not a build dependency, and its Python/Vulkan architecture is not imported into MetaCore. + +Local reference path: + +```text +D:/MetaCore/.codex/external/Infernux +``` + +## Reference Policy + +Allowed: + +- Read Infernux source before implementing a MetaCore feature. +- Extract user-visible behavior, state transitions, data flow, and acceptance tests. +- Recreate workflows in MetaCore's C++/Filament/RmlUi architecture. + +Not allowed for the mainline implementation: + +- Add Infernux as a dependency. +- Import pybind11 or the Python production layer. +- Port the Vulkan RenderGraph or RenderStack. +- Use Infernux packaging based on Nuitka/PyInstaller. +- Use ImGui as MetaCore runtime UI. + +Small MIT-licensed code snippets may only be copied after an explicit licensing review and with attribution. The default is behavior-level reimplementation. + +## Module Map + +| Milestone | Infernux reference | MetaCore implementation target | Implementation rule | +| --- | --- | --- | --- | +| M1 Component/Inspector | `components/component.py`, `components/serialized_field.py`, `engine/ui/inspector_components.py`, `engine/ui/inspector_utils.py` | `MetaCoreReflection`, `MetaCoreIComponentTypeRegistry`, editor Inspector | Replace Python descriptors with C++ reflection metadata and static registration. | +| M2 AssetDatabase/Project | C++ `AssetDatabase`, Python Project panel integration | `MetaCoreIAssetDatabaseService`, Project panel, import pipeline | Keep `.mcmeta`, GUID, and AssetRecord as MetaCore ownership model. | +| M3 Prefab | `engine/_scene_prefab.py`, prefab undo commands | `.mcprefab.json`, `MetaCoreIPrefabService`, `PrefabInstanceMetadata` | Recreate Create/Instantiate/Apply/Revert/Break behavior using MetaCore scene documents. | +| M4 Play Mode | `engine/play_mode.py`, `_play_mode_serialization.py` | `MetaCoreIPlayModeService`, runtime scene clone, C++ lifecycle | Use scene snapshots/clones; do not introduce Python runtime scripts. | +| M5 Scene View/Gizmos | `_scene_view_gizmo.py`, `gizmos/*`, C++ `EditorTools` | ImGuizmo path first, C++ `OnDrawGizmos` later | Keep ImGuizmo initially; only move to Filament-drawn gizmos after workflow gaps are proven. | +| M6 Materials | material assets, previewer, inspector material fields | Filament material assets and MeshRenderer material references | Copy behavior, not Vulkan shader/resource code. | +| M7 Runtime UI | `ui/ui_canvas.py`, `ui/ui_text.py`, `ui/ui_image.py`, `ui/ui_button.py` | RmlUi UI documents and Player rendering | Match Canvas/Text/Image/Button workflow through RmlUi. | +| M8 Build Settings | `engine/ui/build_settings_panel.py`, game builder | Build Settings document, Cook, Player package | Use MetaCore Cook/Package instead of Python app bundling. | +| M9 RuntimeData | Infernux runtime diagnostics patterns as loose reference | Existing `MetaCoreRuntimeData` | RuntimeData integrates after the engine workflow is stable. | + +## Implementation Order + +1. M1: C++ component reflection and automatic Inspector foundation. +2. M2: AssetDatabase and Project panel production workflow. +3. M3: Prefab workflow. +4. M4: Play Mode isolation and C++ lifecycle. +5. M5-M9: Scene interaction, materials, runtime UI, build pipeline, RuntimeData integration. + +## Acceptance Rule + +Before implementing each milestone, inspect the matching Infernux module and write down the behavior being mirrored. The MetaCore implementation is accepted only when the same workflow works through MetaCore-native systems. diff --git a/tests/MetaCoreRuntimeConfigToolSmoke.cmake b/tests/MetaCoreRuntimeConfigToolSmoke.cmake new file mode 100644 index 0000000..d591af6 --- /dev/null +++ b/tests/MetaCoreRuntimeConfigToolSmoke.cmake @@ -0,0 +1,104 @@ +if(NOT DEFINED METACORE_RUNTIME_CONFIG_TOOL) + message(FATAL_ERROR "METACORE_RUNTIME_CONFIG_TOOL is required") +endif() + +if(NOT DEFINED METACORE_RUNTIME_CONFIG_OUTPUT_ROOT) + message(FATAL_ERROR "METACORE_RUNTIME_CONFIG_OUTPUT_ROOT is required") +endif() + +function(metacore_expect_exists path description) + if(NOT EXISTS "${path}") + message(FATAL_ERROR "${description} does not exist: ${path}") + endif() +endfunction() + +function(metacore_expect_not_exists path description) + if(EXISTS "${path}") + message(FATAL_ERROR "${description} should not exist: ${path}") + endif() +endfunction() + +function(metacore_expect_contains path pattern description) + file(READ "${path}" content) + string(FIND "${content}" "${pattern}" pattern_index) + if(pattern_index EQUAL -1) + message(FATAL_ERROR "${description} missing '${pattern}' in ${path}") + endif() +endfunction() + +file(REMOVE_RECURSE "${METACORE_RUNTIME_CONFIG_OUTPUT_ROOT}") + +set(file_project_root "${METACORE_RUNTIME_CONFIG_OUTPUT_ROOT}/file_replay") +set(file_runtime_dir "${file_project_root}/Runtime") +execute_process( + COMMAND "${METACORE_RUNTIME_CONFIG_TOOL}" "${file_runtime_dir}" + RESULT_VARIABLE file_result + OUTPUT_VARIABLE file_stdout + ERROR_VARIABLE file_stderr +) +if(NOT file_result EQUAL 0) + message(FATAL_ERROR "MetaCoreRuntimeConfigTool file_replay failed: ${file_stderr}") +endif() +string(FIND "${file_stdout}" "mode=file_replay" file_mode_index) +if(file_mode_index EQUAL -1) + message(FATAL_ERROR "MetaCoreRuntimeConfigTool file_replay output did not report mode=file_replay: ${file_stdout}") +endif() + +metacore_expect_exists("${file_runtime_dir}/ProjectRuntime.mcruntimecfg" "Runtime project document") +metacore_expect_exists("${file_runtime_dir}/DataSources.mcruntime" "Runtime data sources document") +metacore_expect_exists("${file_runtime_dir}/Bindings.mcruntime" "Runtime bindings document") +metacore_expect_exists("${file_runtime_dir}/RuntimeReplay.mcstream" "Runtime replay stream") +metacore_expect_exists("${file_project_root}/MetaCore.project.json" "Project descriptor") +metacore_expect_exists("${file_project_root}/Scenes/Main.mcscene.json" "Startup scene") +metacore_expect_exists("${file_project_root}/Assets/UI/Hud.mcui.json" "Startup UI") +metacore_expect_contains("${file_project_root}/MetaCore.project.json" "\"runtime_directory\": \"Runtime\"" "Project descriptor") +metacore_expect_contains("${file_project_root}/MetaCore.project.json" "\"startup_scene\": \"Scenes/Main.mcscene.json\"" "Project descriptor") +metacore_expect_contains("${file_runtime_dir}/RuntimeReplay.mcstream" "runtime.status string RuntimeData replay started" "Replay stream") +metacore_expect_contains("${file_project_root}/Assets/UI/Hud.mcui.json" "runtime.status" "Startup UI") + +set(custom_project_root "${METACORE_RUNTIME_CONFIG_OUTPUT_ROOT}/custom_runtime") +set(custom_runtime_dir "${custom_project_root}/ConfigRuntime") +execute_process( + COMMAND "${METACORE_RUNTIME_CONFIG_TOOL}" "${custom_runtime_dir}" + RESULT_VARIABLE custom_result + OUTPUT_VARIABLE custom_stdout + ERROR_VARIABLE custom_stderr +) +if(NOT custom_result EQUAL 0) + message(FATAL_ERROR "MetaCoreRuntimeConfigTool custom runtime failed: ${custom_stderr}") +endif() +metacore_expect_exists("${custom_runtime_dir}/ProjectRuntime.mcruntimecfg" "Custom runtime project document") +metacore_expect_exists("${custom_runtime_dir}/DataSources.mcruntime" "Custom runtime data sources document") +metacore_expect_exists("${custom_runtime_dir}/Bindings.mcruntime" "Custom runtime bindings document") +metacore_expect_exists("${custom_runtime_dir}/RuntimeReplay.mcstream" "Custom runtime replay stream") +metacore_expect_exists("${custom_project_root}/MetaCore.project.json" "Custom project descriptor") +metacore_expect_exists("${custom_project_root}/Scenes/Main.mcscene.json" "Custom startup scene") +metacore_expect_contains("${custom_project_root}/MetaCore.project.json" "\"runtime_directory\": \"ConfigRuntime\"" "Custom project descriptor") +metacore_expect_contains("${custom_project_root}/MetaCore.project.json" "\"startup_scene\": \"Scenes/Main.mcscene.json\"" "Custom project descriptor") + +set(tcp_project_root "${METACORE_RUNTIME_CONFIG_OUTPUT_ROOT}/tcp") +set(tcp_runtime_dir "${tcp_project_root}/Runtime") +execute_process( + COMMAND "${METACORE_RUNTIME_CONFIG_TOOL}" "${tcp_runtime_dir}" "--tcp" + RESULT_VARIABLE tcp_result + OUTPUT_VARIABLE tcp_stdout + ERROR_VARIABLE tcp_stderr +) +if(NOT tcp_result EQUAL 0) + message(FATAL_ERROR "MetaCoreRuntimeConfigTool tcp failed: ${tcp_stderr}") +endif() +string(FIND "${tcp_stdout}" "mode=tcp" tcp_mode_index) +if(tcp_mode_index EQUAL -1) + message(FATAL_ERROR "MetaCoreRuntimeConfigTool tcp output did not report mode=tcp: ${tcp_stdout}") +endif() + +metacore_expect_exists("${tcp_runtime_dir}/ProjectRuntime.mcruntimecfg" "TCP runtime project document") +metacore_expect_exists("${tcp_runtime_dir}/DataSources.mcruntime" "TCP runtime data sources document") +metacore_expect_exists("${tcp_runtime_dir}/Bindings.mcruntime" "TCP runtime bindings document") +metacore_expect_not_exists("${tcp_runtime_dir}/RuntimeReplay.mcstream" "TCP runtime replay stream") +metacore_expect_exists("${tcp_project_root}/MetaCore.project.json" "TCP project descriptor") +metacore_expect_exists("${tcp_project_root}/Scenes/Main.mcscene.json" "TCP startup scene") +metacore_expect_exists("${tcp_project_root}/Assets/UI/Hud.mcui.json" "TCP startup UI") +metacore_expect_contains("${tcp_project_root}/MetaCore.project.json" "\"runtime_directory\": \"Runtime\"" "TCP project descriptor") +metacore_expect_contains("${tcp_project_root}/MetaCore.project.json" "\"startup_scene\": \"Scenes/Main.mcscene.json\"" "TCP project descriptor") +metacore_expect_contains("${tcp_project_root}/Assets/UI/Hud.mcui.json" "runtime.status" "TCP startup UI") diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 4f7ad23..d4e5c8f 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -2,6 +2,7 @@ #include "MetaCoreEditor/MetaCoreEditorAssetTypes.h" #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorModule.h" +#include "MetaCoreEditor/MetaCoreSceneInteractionService.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include "MetaCoreFoundation/MetaCoreLogService.h" @@ -10,6 +11,7 @@ #include "MetaCorePlatform/MetaCoreWindow.h" #include "MetaCoreRender/MetaCoreEditorViewportRenderer.h" #include "MetaCoreRender/MetaCoreRenderDevice.h" +#include "MetaCoreRender/MetaCoreRuntimeUiRenderer.h" #include "MetaCoreRender/MetaCoreSceneRenderSync.h" #include "MetaCoreRender/MetaCoreFilamentSceneBridge.h" #include @@ -19,11 +21,13 @@ #include "MetaCoreScene/MetaCoreScenePackage.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" +#include "MetaCoreScene/MetaCoreUiRmlCompiler.h" #include "MetaCoreScene/MetaCoreTransformUtils.h" #include "MetaCoreFoundation/MetaCoreHash.h" #include #include +#include #include #include #include @@ -31,6 +35,7 @@ #include #include #include +#include #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN @@ -198,11 +203,29 @@ void MetaCoreTestBuiltinModuleComposition() { MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 AssetDatabaseService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ImportPipelineService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 CookService"); + MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "BuildService should be registered"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ScenePersistenceService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 SelectionService"); MetaCoreExpect(moduleRegistry.ResolveService() != nullptr, "应注册 ClipboardService"); MetaCoreExpect(!moduleRegistry.GetPanelProviders().empty(), "应注册至少一个面板提供者"); + const bool hasBuildSettingsPanel = std::any_of( + moduleRegistry.GetPanelProviders().begin(), + moduleRegistry.GetPanelProviders().end(), + [](const std::unique_ptr& panelProvider) { + return panelProvider != nullptr && panelProvider->GetPanelId() == "BuildSettings"; + } + ); + MetaCoreExpect(hasBuildSettingsPanel, "Build Settings panel should be registered"); + const bool hasRuntimeDataPanel = std::any_of( + moduleRegistry.GetPanelProviders().begin(), + moduleRegistry.GetPanelProviders().end(), + [](const std::unique_ptr& panelProvider) { + return panelProvider != nullptr && panelProvider->GetPanelId() == "RuntimeData"; + } + ); + MetaCoreExpect(hasRuntimeDataPanel, "Runtime Data panel should be registered"); + editorViewsModule->Shutdown(moduleRegistry); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -241,6 +264,39 @@ void MetaCoreTestLegacyBinarySceneStartupLoadCompatibility() { MetaCoreExpect(!loadedSceneDocument->GameObjects.empty(), "startup scene 应包含对象"); MetaCoreExpect(loadedSceneDocument->GameObjects.front().Name == "Main Camera", "startup scene 首个对象应为 Main Camera"); + MetaCore::MetaCoreSceneDocument jsonSceneDocument = sceneDocument; + jsonSceneDocument.Name = "JsonMain"; + const std::filesystem::path jsonScenePath = tempProjectRoot / "Scenes" / "JsonMain.mcscene.json"; + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveSceneToJson( + jsonScenePath, + jsonSceneDocument, + MetaCore::MetaCoreBuildScenePackageTypeRegistry() + ), + "Startup scene JSON should be writable" + ); + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + MetaCoreExpect(projectFile.is_open(), "Project file should be writable for JSON startup scene"); + projectFile + << "{\n" + << " \"name\": \"SmokeProject\",\n" + << " \"scenes\": [\n" + << " \"Scenes/JsonMain.mcscene.json\"\n" + << " ],\n" + << " \"startup_scene\": \"Scenes/JsonMain.mcscene.json\",\n" + << " \"version\": \"0.1.0\"\n" + << "}\n"; + } + const auto loadedJsonSceneDocument = + MetaCore::MetaCoreLoadStartupSceneDocument(tempProjectRoot / "MetaCore.project.json"); + MetaCoreExpect(loadedJsonSceneDocument.has_value(), "Startup scene loader should read JSON scene assets"); + MetaCoreExpect(loadedJsonSceneDocument->Name == "JsonMain", "Startup scene loader should preserve JSON scene name"); + MetaCoreExpect( + loadedJsonSceneDocument->GameObjects.size() == sourceScene.GetGameObjects().size(), + "JSON startup scene object count should match" + ); + std::filesystem::remove_all(tempProjectRoot); } @@ -494,11 +550,468 @@ void MetaCoreTestComponentRegistryDescriptors() { MetaCoreExpect(static_cast(cameraDescriptor->DrawInspector), "Camera descriptor 应提供 drawer"); MetaCoreExpect(static_cast(lightDescriptor->DrawInspector), "Light descriptor 应提供 drawer"); MetaCoreExpect(static_cast(meshRendererDescriptor->DrawInspector), "MeshRenderer descriptor 应提供 drawer"); + MetaCoreExpect(transformDescriptor->Category == "Transform", "Transform descriptor should expose a component category"); + MetaCoreExpect(cameraDescriptor->Category == "Rendering", "Camera descriptor should expose a component category"); + MetaCoreExpect(transformDescriptor->ReflectedType != nullptr, "Transform descriptor should bind reflected type metadata"); + MetaCoreExpect(cameraDescriptor->ReflectedType != nullptr, "Camera descriptor should bind reflected type metadata"); + MetaCoreExpect(static_cast(transformDescriptor->MutableComponent), "Transform descriptor should expose mutable component access"); + MetaCoreExpect(static_cast(transformDescriptor->ConstComponent), "Transform descriptor should expose const component access"); + MetaCoreExpect(static_cast(cameraDescriptor->MutableComponent), "Camera descriptor should expose mutable component access"); + MetaCoreExpect(static_cast(cameraDescriptor->ConstComponent), "Camera descriptor should expose const component access"); + + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject object = scene.CreateGameObject("DescriptorAccess"); + void* transformComponent = transformDescriptor->MutableComponent(object); + MetaCoreExpect(transformComponent != nullptr, "Transform descriptor should resolve a component pointer"); + + const auto positionFieldIterator = std::find_if( + transformDescriptor->ReflectedType->Fields.begin(), + transformDescriptor->ReflectedType->Fields.end(), + [](const MetaCore::MetaCoreFieldDescriptor& field) { + return field.Name == "Position"; + } + ); + MetaCoreExpect( + positionFieldIterator != transformDescriptor->ReflectedType->Fields.end(), + "Transform descriptor should expose reflected Position field" + ); + + auto* reflectedPosition = static_cast(positionFieldIterator->MutableValue(transformComponent)); + *reflectedPosition = glm::vec3(7.0F, 8.0F, 9.0F); + MetaCoreExpectVec3Near( + object.GetComponent().Position, + glm::vec3(7.0F, 8.0F, 9.0F), + "Descriptor component pointer should work with reflected field access" + ); + MetaCoreExpect(cameraDescriptor->MutableComponent(object) == nullptr, "Camera access should be null before the component is added"); + MetaCoreExpect(cameraDescriptor->AddComponent(object), "Camera descriptor should add a camera component"); + MetaCoreExpect(cameraDescriptor->MutableComponent(object) != nullptr, "Camera access should resolve after the component is added"); + + const auto reflectionRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for static component registration"); + const MetaCore::MetaCoreStructDescriptor* modelRootTagReflection = + reflectionRegistry->GetTypeRegistry().FindStruct(); + MetaCoreExpect(modelRootTagReflection != nullptr, "ModelRootTag reflection should be available"); + + MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor scriptDescriptor{}; + scriptDescriptor.TypeId = "ModelRootTag"; + scriptDescriptor.ReflectedType = modelRootTagReflection; + scriptDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + scriptDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + scriptDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.HasComponent(); + }; + scriptDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) { + if (gameObject.HasComponent()) { + return false; + } + gameObject.AddComponent(); + return true; + }; + scriptDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) { + return false; + } + gameObject.RemoveComponent(); + return true; + }; + scriptDescriptor.ResetComponent = [](MetaCore::MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) { + return false; + } + gameObject.GetComponent() = MetaCore::MetaCoreModelRootTag{}; + return true; + }; + + MetaCoreExpect( + componentRegistry->RegisterComponentDescriptor(scriptDescriptor), + "Component registry should accept a static reflected component descriptor" + ); + MetaCoreExpect( + !componentRegistry->RegisterComponentDescriptor(scriptDescriptor), + "Component registry should reject duplicate component TypeId registration" + ); + const auto* registeredScriptDescriptor = componentRegistry->FindDescriptor("ModelRootTag"); + MetaCoreExpect(registeredScriptDescriptor != nullptr, "Registered static component should be discoverable"); + MetaCoreExpect(registeredScriptDescriptor->DisplayName == "ModelRootTag", "Registered component should default display name"); + MetaCoreExpect(registeredScriptDescriptor->Category == "Scripts", "Registered component should default category"); + MetaCoreExpect(registeredScriptDescriptor->AddComponent(object), "Registered component descriptor should add component"); + MetaCoreExpect(registeredScriptDescriptor->HasComponent(object), "Registered component descriptor should report added component"); + + const auto sourcePathFieldIterator = std::find_if( + registeredScriptDescriptor->ReflectedType->Fields.begin(), + registeredScriptDescriptor->ReflectedType->Fields.end(), + [](const MetaCore::MetaCoreFieldDescriptor& field) { + return field.Name == "SourceModelPath"; + } + ); + MetaCoreExpect( + sourcePathFieldIterator != registeredScriptDescriptor->ReflectedType->Fields.end(), + "Registered component should expose reflected SourceModelPath field" + ); + auto* modelSourcePath = static_cast( + sourcePathFieldIterator->MutableValue(registeredScriptDescriptor->MutableComponent(object)) + ); + *modelSourcePath = "Assets/Models/Test.glb"; + MetaCoreExpect( + object.GetComponent().SourceModelPath == "Assets/Models/Test.glb", + "Registered component should support reflected field mutation" + ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); } +void MetaCoreTestReflectionFieldEditorDescriptors() { + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterSceneGeneratedTypes(registry); + + const MetaCore::MetaCoreStructDescriptor* transformDescriptor = + registry.FindStruct(); + MetaCoreExpect(transformDescriptor != nullptr, "Transform reflection descriptor should exist"); + + const auto findField = [&](std::string_view name) -> const MetaCore::MetaCoreFieldDescriptor* { + const auto iterator = std::find_if( + transformDescriptor->Fields.begin(), + transformDescriptor->Fields.end(), + [&](const MetaCore::MetaCoreFieldDescriptor& field) { + return field.Name == name; + } + ); + return iterator == transformDescriptor->Fields.end() ? nullptr : &(*iterator); + }; + + const MetaCore::MetaCoreFieldDescriptor* positionField = findField("Position"); + MetaCoreExpect(positionField != nullptr, "Position field descriptor should exist"); + MetaCoreExpect(positionField->ValueKind == MetaCore::MetaCoreFieldValueKind::Vec3, "Position field should be Vec3"); + MetaCoreExpect(!positionField->TypeName.empty(), "Position field should expose a type name"); + MetaCoreExpect(static_cast(positionField->MutableValue), "Position field should expose mutable access"); + MetaCoreExpect(static_cast(positionField->ConstValue), "Position field should expose const access"); + MetaCoreExpect(positionField->Editor.DisplayName == "Position", "Position field should preserve display metadata"); + MetaCoreExpect(positionField->Editor.Group == "Transform", "Position field should preserve group metadata"); + MetaCoreExpect( + positionField->Editor.RawSpec.find("Group") != std::string::npos, + "Position field should retain MC_PROPERTY metadata spec" + ); + + MetaCore::MetaCoreTransformComponent transform; + auto* mutablePosition = static_cast(positionField->MutableValue(&transform)); + *mutablePosition = glm::vec3(4.0F, 5.0F, 6.0F); + MetaCoreExpectVec3Near(transform.Position, glm::vec3(4.0F, 5.0F, 6.0F), "MutableValue should edit the field"); + + const auto* constPosition = static_cast(positionField->ConstValue(&transform)); + MetaCoreExpectVec3Near(*constPosition, glm::vec3(4.0F, 5.0F, 6.0F), "ConstValue should read the field"); + + const MetaCore::MetaCoreEnumDescriptor* alphaModeEnum = + registry.FindEnum(); + MetaCoreExpect(alphaModeEnum != nullptr, "Alpha mode enum reflection descriptor should exist"); + MetaCoreExpect(alphaModeEnum->Values.size() == 3, "Alpha mode enum should expose generated values"); + MetaCoreExpect( + std::any_of( + alphaModeEnum->Values.begin(), + alphaModeEnum->Values.end(), + [](const MetaCore::MetaCoreEnumValueDescriptor& value) { + return value.Name == "Blend" && + value.Value == static_cast(MetaCore::MetaCoreMeshAlphaMode::Blend); + } + ), + "Alpha mode enum should expose Blend value" + ); + + const MetaCore::MetaCoreStructDescriptor* meshRendererDescriptor = + registry.FindStruct(); + MetaCoreExpect(meshRendererDescriptor != nullptr, "MeshRenderer reflection descriptor should exist"); + const auto alphaModeFieldIterator = std::find_if( + meshRendererDescriptor->Fields.begin(), + meshRendererDescriptor->Fields.end(), + [](const MetaCore::MetaCoreFieldDescriptor& field) { + return field.Name == "AlphaMode"; + } + ); + MetaCoreExpect( + alphaModeFieldIterator != meshRendererDescriptor->Fields.end(), + "MeshRenderer AlphaMode field descriptor should exist" + ); + MetaCoreExpect( + alphaModeFieldIterator->ValueKind == MetaCore::MetaCoreFieldValueKind::Enum, + "MeshRenderer AlphaMode field should be reflected as enum" + ); + MetaCoreExpect( + alphaModeFieldIterator->Size == sizeof(MetaCore::MetaCoreMeshAlphaMode), + "Reflected enum field should retain field size for Inspector writes" + ); +} + +void MetaCoreTestPlayModeLifecycleAndSceneIsolation() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto componentRegistry = moduleRegistry.ResolveService(); + const auto reflectionRegistry = moduleRegistry.ResolveService(); + const auto playModeService = moduleRegistry.ResolveService(); + MetaCoreExpect(componentRegistry != nullptr, "Component registry should be available for play mode lifecycle test"); + MetaCoreExpect(reflectionRegistry != nullptr, "Reflection registry should be available for play mode lifecycle test"); + MetaCoreExpect(playModeService != nullptr, "Play mode service should be available"); + + MetaCore::MetaCoreGameObject object = scene.CreateGameObject("LifecycleObject"); + object.GetComponent().Position = glm::vec3(1.0F, 2.0F, 3.0F); + object.AddComponent().SourceModelPath = "Assets/Models/Source.glb"; + const MetaCore::MetaCoreId objectId = object.GetId(); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + editorContext.SelectOnly(objectId); + + int startCount = 0; + int updateCount = 0; + int destroyCount = 0; + + MetaCore::MetaCoreIComponentTypeRegistry::MetaCoreComponentDescriptor lifecycleDescriptor{}; + lifecycleDescriptor.TypeId = "LifecycleModelRootTag"; + lifecycleDescriptor.DisplayName = "Lifecycle Model Root Tag"; + lifecycleDescriptor.Category = "Scripts"; + lifecycleDescriptor.ReflectedType = reflectionRegistry->GetTypeRegistry().FindStruct(); + lifecycleDescriptor.MutableComponent = [](MetaCore::MetaCoreGameObject& gameObject) -> void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + lifecycleDescriptor.ConstComponent = [](const MetaCore::MetaCoreGameObject& gameObject) -> const void* { + return gameObject.HasComponent() ? &gameObject.GetComponent() : nullptr; + }; + lifecycleDescriptor.HasComponent = [](const MetaCore::MetaCoreGameObject& gameObject) { + return gameObject.HasComponent(); + }; + lifecycleDescriptor.AddComponent = [](MetaCore::MetaCoreGameObject& gameObject) { + if (gameObject.HasComponent()) { + return false; + } + gameObject.AddComponent(); + return true; + }; + lifecycleDescriptor.RemoveComponent = [](MetaCore::MetaCoreGameObject& gameObject) { + if (!gameObject.HasComponent()) { + return false; + } + gameObject.RemoveComponent(); + return true; + }; + lifecycleDescriptor.Lifecycle.OnStart = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject& gameObject) { + ++startCount; + gameObject.GetComponent().Position.x = 10.0F; + }; + lifecycleDescriptor.Lifecycle.OnUpdate = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject& gameObject, float deltaSeconds) { + ++updateCount; + gameObject.GetComponent().Position.y += deltaSeconds; + }; + lifecycleDescriptor.Lifecycle.OnDestroy = [&](MetaCore::MetaCoreEditorContext&, MetaCore::MetaCoreGameObject&) { + ++destroyCount; + }; + + MetaCoreExpect( + componentRegistry->RegisterComponentDescriptor(std::move(lifecycleDescriptor)), + "Play mode lifecycle component descriptor should register" + ); + + MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Initial play mode state should be Edit"); + MetaCoreExpect(playModeService->EnterPlayMode(editorContext), "EnterPlayMode should succeed"); + MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Playing, "Play mode state should be Playing"); + MetaCoreExpect(startCount == 1, "OnStart should run once on enter play mode"); + MetaCoreExpectVec3Near( + scene.FindGameObject(objectId).GetComponent().Position, + glm::vec3(10.0F, 2.0F, 3.0F), + "OnStart should be able to mutate runtime scene state" + ); + + playModeService->TickPlayMode(editorContext, 0.5F); + MetaCoreExpect(startCount == 1, "OnStart should not repeat during tick"); + MetaCoreExpect(updateCount == 1, "OnUpdate should run during play tick"); + MetaCoreExpectVec3Near( + scene.FindGameObject(objectId).GetComponent().Position, + glm::vec3(10.0F, 2.5F, 3.0F), + "OnUpdate should mutate runtime scene state" + ); + + MetaCoreExpect(playModeService->PausePlayMode(editorContext), "PausePlayMode should succeed"); + playModeService->TickPlayMode(editorContext, 0.25F); + MetaCoreExpect(updateCount == 1, "OnUpdate should not run while paused"); + MetaCoreExpect(playModeService->ResumePlayMode(editorContext), "ResumePlayMode should succeed"); + playModeService->TickPlayMode(editorContext, 0.25F); + MetaCoreExpect(updateCount == 2, "OnUpdate should resume after pause"); + + MetaCoreExpect(playModeService->ExitPlayMode(editorContext), "ExitPlayMode should succeed"); + MetaCoreExpect(playModeService->GetState() == MetaCore::MetaCorePlayModeState::Edit, "Play mode state should return to Edit"); + MetaCoreExpect(destroyCount == 1, "OnDestroy should run on exit play mode"); + MetaCoreExpectVec3Near( + scene.FindGameObject(objectId).GetComponent().Position, + glm::vec3(1.0F, 2.0F, 3.0F), + "ExitPlayMode should restore edit scene snapshot" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreGameObject parent = scene.CreateGameObject("GizmoParent"); + parent.GetComponent().Position = glm::vec3(1.0F, 0.0F, 0.0F); + const MetaCore::MetaCoreId parentId = parent.GetId(); + + MetaCore::MetaCoreGameObject child = scene.CreateGameObject("GizmoChild", parentId); + child.GetComponent().Position = glm::vec3(0.0F, 2.0F, 0.0F); + const MetaCore::MetaCoreId childId = child.GetId(); + + MetaCore::MetaCoreGameObject sibling = scene.CreateGameObject("GizmoSibling"); + sibling.GetComponent().Position = glm::vec3(5.0F, 0.0F, 0.0F); + const MetaCore::MetaCoreId siblingId = sibling.GetId(); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + editorContext.SetSelection({parentId, childId, siblingId}, parentId); + + MetaCore::MetaCoreSceneInteractionService sceneInteractionService; + const glm::mat4 originalParentWorld = + MetaCore::MetaCoreBuildTransformMatrix(scene.FindGameObject(parentId).GetComponent()); + const glm::mat4 movedParentWorld = glm::translate(glm::mat4(1.0F), glm::vec3(3.0F, 0.0F, 0.0F)); + + sceneInteractionService.HandleGizmoBeginUse(editorContext, true); + MetaCoreExpect( + sceneInteractionService.ApplyWorldTransformDeltaToSelection(editorContext, parentId, originalParentWorld, movedParentWorld), + "Gizmo delta should apply to selected transform roots" + ); + sceneInteractionService.HandleGizmoEndUse(editorContext, true); + sceneInteractionService.HandleGizmoEndUse(editorContext, false); + + MetaCoreExpectVec3Near( + scene.FindGameObject(parentId).GetComponent().Position, + glm::vec3(3.0F, 0.0F, 0.0F), + "Gizmo delta should move active selected root" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(childId).GetComponent().Position, + glm::vec3(0.0F, 2.0F, 0.0F), + "Gizmo delta should not double-apply to selected children of selected roots" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(siblingId).GetComponent().Position, + glm::vec3(7.0F, 0.0F, 0.0F), + "Gizmo delta should move other selected roots by the same world delta" + ); + MetaCoreExpect(editorContext.GetCommandService().CanUndo(), "Completed gizmo drag should create an undo command"); + + MetaCoreExpect(editorContext.UndoCommand(), "Undo gizmo drag should succeed"); + MetaCoreExpectVec3Near( + scene.FindGameObject(parentId).GetComponent().Position, + glm::vec3(1.0F, 0.0F, 0.0F), + "Undo gizmo drag should restore active root" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(siblingId).GetComponent().Position, + glm::vec3(5.0F, 0.0F, 0.0F), + "Undo gizmo drag should restore other selected root" + ); + + MetaCoreExpect(editorContext.RedoCommand(), "Redo gizmo drag should succeed"); + MetaCoreExpectVec3Near( + scene.FindGameObject(parentId).GetComponent().Position, + glm::vec3(3.0F, 0.0F, 0.0F), + "Redo gizmo drag should reapply active root transform" + ); + MetaCoreExpectVec3Near( + scene.FindGameObject(siblingId).GetComponent().Position, + glm::vec3(7.0F, 0.0F, 0.0F), + "Redo gizmo drag should reapply other selected root transform" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); +} + +void MetaCoreTestGizmoSnapSettings() { + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(!editorContext.GetGizmoSnapSettings().Enabled, "Gizmo snap should be disabled by default"); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Translate) - 0.5F) < 0.0001F, + "Default translate snap step should be stable" + ); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Rotate) - 15.0F) < 0.0001F, + "Default rotate snap step should be stable" + ); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Scale) - 0.1F) < 0.0001F, + "Default scale snap step should be stable" + ); + + editorContext.SetGizmoSnapEnabled(true); + MetaCoreExpect(editorContext.GetGizmoSnapSettings().Enabled, "Gizmo snap enabled flag should be mutable"); + + MetaCore::MetaCoreGizmoSnapSettings customSnap; + customSnap.Enabled = true; + customSnap.TranslationStep = 0.25F; + customSnap.RotationStepDegrees = 30.0F; + customSnap.ScaleStep = 0.2F; + editorContext.SetGizmoSnapSettings(customSnap); + + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Translate) - 0.25F) < 0.0001F, + "Custom translate snap step should be used" + ); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Rotate) - 30.0F) < 0.0001F, + "Custom rotate snap step should be used" + ); + MetaCoreExpect( + std::abs(editorContext.GetGizmoSnapSettings().StepForOperation(MetaCore::MetaCoreGizmoOperation::Scale) - 0.2F) < 0.0001F, + "Custom scale snap step should be used" + ); +} + void MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot() { MetaCore::MetaCoreScene scene; @@ -571,8 +1084,10 @@ void MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson() { std::filesystem::temp_directory_path() / "MetaCoreSceneRoundTripProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); + std::filesystem::create_directories(tempProjectRoot / "Runtime"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); @@ -1347,8 +1862,8 @@ void MetaCoreTestDeleteModelSubChildCorrectlyClearsFromViewport() { // 在场景中删除子节点 Valve_A GameObject(在 Command 中执行,为了后续 Undo) const bool deleted = editorContext.ExecuteSnapshotCommand("删除子节点 Valve_A", [&]() { - scene.DeleteGameObjects({childId}); - return true; + const std::vector deletedIds = scene.DeleteGameObjects({childId}); + return !deletedIds.empty(); }); MetaCoreExpect(deleted, "删除子节点 Valve_A 快照命令应成功"); @@ -1462,7 +1977,7 @@ void MetaCoreTestModelReimportKeepsGeneratedAssetReferencesStable() { << "}\n"; } - MetaCoreExpect(assetDatabase->Refresh(), "修改模型源文件后应能刷新资产数据库"); + MetaCoreExpect(assetDatabase->ReimportAsset(modelRecord->Guid), "修改模型源文件后应能通过 AssetDatabase 重新导入"); const auto refreshedRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "Pump.gltf"); MetaCoreExpect(refreshedRecord.has_value(), "重导入后仍应能找到模型资源记录"); @@ -1644,6 +2159,62 @@ void MetaCoreTestPrefabWorkflow() { MetaCoreExpect(revertedRoot, "还原后应重新选中实例根"); MetaCoreExpectVec3Near(revertedRoot.GetComponent().Position, glm::vec3(8.0F, 9.0F, 10.0F), "Revert 后实例应恢复为 prefab 内容"); + const MetaCore::MetaCoreId revertedRootId = revertedRoot.GetId(); + const std::vector revertedSubtreeIds = scene.GetSubtreeObjectIds(revertedRootId); + + MetaCore::MetaCoreSceneDocument sceneRoundTripDocument; + sceneRoundTripDocument.Name = "PrefabInstanceRoundTrip"; + sceneRoundTripDocument.GameObjects = scene.CaptureSnapshot().GameObjects; + const std::filesystem::path sceneRoundTripPath = tempProjectRoot / "Scenes" / "PrefabInstanceRoundTrip.mcscene.json"; + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(sceneRoundTripPath, sceneRoundTripDocument, reflectionRegistry->GetTypeRegistry()), + "Scene save should preserve prefab instance metadata" + ); + + const auto loadedSceneRoundTrip = + MetaCore::MetaCoreSceneSerializer::LoadSceneFromJson(sceneRoundTripPath, reflectionRegistry->GetTypeRegistry()); + MetaCoreExpect(loadedSceneRoundTrip.has_value(), "Scene reload should parse prefab instance metadata"); + for (MetaCore::MetaCoreId objectId : revertedSubtreeIds) { + const auto objectIterator = std::find_if( + loadedSceneRoundTrip->GameObjects.begin(), + loadedSceneRoundTrip->GameObjects.end(), + [objectId](const MetaCore::MetaCoreGameObjectData& objectData) { + return objectData.Id == objectId; + } + ); + MetaCoreExpect(objectIterator != loadedSceneRoundTrip->GameObjects.end(), "Reloaded scene should include prefab instance object"); + MetaCoreExpect(objectIterator->PrefabInstance.has_value(), "Reloaded prefab instance object should keep metadata"); + MetaCoreExpect( + objectIterator->PrefabInstance->PrefabAssetGuid == prefabRecord->Guid, + "Reloaded prefab instance should keep source prefab guid" + ); + MetaCoreExpect( + objectIterator->PrefabInstance->PrefabInstanceRootId == revertedRootId, + "Reloaded prefab instance should keep instance root id" + ); + } + + const auto expectPrefabMetadata = [&](bool expected, const char* message) { + for (MetaCore::MetaCoreId objectId : scene.GetSubtreeObjectIds(revertedRootId)) { + MetaCore::MetaCoreGameObject object = scene.FindGameObject(objectId); + MetaCoreExpect(object, "Prefab metadata assertion object should exist"); + MetaCoreExpect( + object.HasComponent() == expected, + message + ); + } + }; + + editorContext.SelectOnly(revertedRootId); + MetaCoreExpect(prefabService->BreakSelectedPrefabInstance(editorContext), "Break should remove prefab instance metadata"); + expectPrefabMetadata(false, "Break should remove prefab metadata from the whole instance subtree"); + + MetaCoreExpect(editorContext.UndoCommand(), "Undo Break should succeed"); + expectPrefabMetadata(true, "Undo Break should restore prefab metadata"); + + MetaCoreExpect(editorContext.RedoCommand(), "Redo Break should succeed"); + expectPrefabMetadata(false, "Redo Break should remove prefab metadata again"); + coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); _putenv_s("METACORE_PROJECT_PATH", ""); @@ -1768,6 +2339,308 @@ void MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi() { std::filesystem::remove_all(tempProjectRoot); } +void MetaCoreTestBuildServiceCreatesPlayerPackageLayout() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreBuildPackageProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "Materials"); + std::filesystem::create_directories(tempProjectRoot / "Config" / "RuntimeData"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Runtime"); + std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); + std::filesystem::create_directories(tempProjectRoot / "Tools"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"BuildPackageProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"runtime_directory\": \"ConfigRuntime\",\n" + << " \"scenes\": [\"Scenes/Main.mcscene.json\"],\n" + << " \"startup_scene\": \"Scenes/Main.mcscene.json\"\n" + << "}\n"; + } + { + std::ofstream fakePlayer(tempProjectRoot / "Tools" / "MetaCorePlayer.exe", std::ios::binary | std::ios::trunc); + fakePlayer << "fake player"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + const auto buildService = moduleRegistry.ResolveService(); + const auto cookService = moduleRegistry.ResolveService(); + const auto reflectionRegistry = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "Build package test should resolve AssetDatabaseService"); + MetaCoreExpect(buildService != nullptr, "Build package test should resolve BuildService"); + MetaCoreExpect(cookService != nullptr, "Build package test should resolve CookService"); + MetaCoreExpect(reflectionRegistry != nullptr, "Build package test should resolve ReflectionRegistry"); + + const auto& registry = reflectionRegistry->GetTypeRegistry(); + const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + MetaCore::MetaCoreSceneDocument sceneDocument; + sceneDocument.Name = "Main"; + sceneDocument.GameObjects = MetaCore::MetaCoreCreateDefaultScene().CaptureSnapshot().GameObjects; + MetaCore::MetaCoreGameObjectData materialDependencyObject; + materialDependencyObject.Id = 9001; + materialDependencyObject.Name = "MaterialDependencyObject"; + materialDependencyObject.MeshRenderer = MetaCore::MetaCoreMeshRendererComponent{}; + sceneDocument.GameObjects.push_back(materialDependencyObject); + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry), + "Build package test should write startup scene" + ); + + const std::filesystem::path materialPath = std::filesystem::path("Assets") / "Materials" / "Dependency.mcmaterial.json"; + MetaCore::MetaCoreMaterialAssetDocument materialDocument; + materialDocument.AssetGuid = MetaCore::MetaCoreAssetGuid::Generate(); + materialDocument.Name = "Dependency"; + materialDocument.BaseColor = glm::vec3(0.8F, 0.3F, 0.2F); + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveMaterialToJson(tempProjectRoot / materialPath, materialDocument, registry), + "Build package test should write dependency material" + ); + + const std::filesystem::path uiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; + MetaCore::MetaCoreUiDocument uiDocument; + uiDocument.Name = "Hud"; + uiDocument.RootNodeIds.push_back("root"); + MetaCore::MetaCoreUiNodeDocument uiRoot; + uiRoot.Id = "root"; + uiRoot.Name = "Root"; + uiRoot.Type = MetaCore::MetaCoreUiNodeType::Panel; + uiDocument.Nodes.push_back(uiRoot); + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveUiToJson(tempProjectRoot / uiPath, uiDocument, registry), + "Build package test should write startup UI" + ); + + MetaCore::MetaCoreRuntimeProjectDocument runtimeProject; + runtimeProject.StartupScenePath = scenePath; + runtimeProject.StartupUiPath = uiPath; + runtimeProject.BuildProfileName = "BuildSmokeProfile"; + runtimeProject.TargetPlatform = "Windows"; + runtimeProject.OutputDirectory = std::filesystem::path("BuildSettingsOutput"); + runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; + runtimeProject.UseCookedAssets = false; + runtimeProject.DataSourcesPath = std::filesystem::path("Config") / "RuntimeData" / "DataSources.mcruntime"; + runtimeProject.BindingsPath = std::filesystem::path("Config") / "RuntimeData" / "Bindings.mcruntime"; + runtimeProject.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + const std::filesystem::path runtimeProjectPath = + std::filesystem::path("ConfigRuntime") / "ProjectRuntime.mcruntimecfg"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), + "Build package test should write runtime project" + ); + const std::filesystem::path runtimeReplayPath = + std::filesystem::path("Config") / "RuntimeData" / "BuildReplay.mcstream"; + { + std::ofstream replayFile(tempProjectRoot / runtimeReplayPath, std::ios::trunc); + replayFile << "0.00 build.status string Build package replay ready\n"; + } + MetaCore::MetaCoreRuntimeDataSourcesDocument runtimeSources; + runtimeSources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "build-source", + "file_replay", + "Build Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", runtimeReplayPath.generic_string()}}, + true, + 1000 + }); + runtimeSources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "build.status", + "build-source", + "build.status", + MetaCore::MetaCoreRuntimeValueType::String + }); + MetaCore::MetaCoreRuntimeBindingsDocument runtimeBindings; + runtimeBindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ + "binding.build.status", + "build.status", + "runtime.status", + MetaCore::MetaCoreRuntimeUiBindingTarget::Text, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(tempProjectRoot / runtimeProject.DataSourcesPath, runtimeSources, registry), + "Build package test should write runtime data sources outside Runtime" + ); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeBindingsDocument(tempProjectRoot / runtimeProject.BindingsPath, runtimeBindings, registry), + "Build package test should write runtime bindings outside Runtime" + ); + MetaCoreExpect(assetDatabase->Refresh(), "Build package test should refresh asset database"); + const auto materialRecord = assetDatabase->FindAssetByRelativePath(materialPath); + MetaCoreExpect(materialRecord.has_value(), "Build package test should register dependency material"); + bool assignedMaterialDependency = false; + for (MetaCore::MetaCoreGameObjectData& gameObject : sceneDocument.GameObjects) { + if (gameObject.MeshRenderer.has_value()) { + gameObject.MeshRenderer->MaterialAssetGuids = {materialRecord->Guid}; + assignedMaterialDependency = true; + break; + } + } + MetaCoreExpect(assignedMaterialDependency, "Build package test startup scene should have a MeshRenderer"); + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(tempProjectRoot / scenePath, sceneDocument, registry), + "Build package test should rewrite startup scene with material dependency" + ); + MetaCoreExpect(assetDatabase->Refresh(), "Build package test should refresh material dependency scene"); + + MetaCore::MetaCoreBuildPlayerPackageRequest request; + request.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + const MetaCore::MetaCoreBuildPlayerPackageResult result = buildService->BuildPlayerPackage(request); + MetaCoreExpect(result.Success, ("BuildService should create player package: " + result.Error).c_str()); + const std::filesystem::path outputRoot = tempProjectRoot / "BuildSettingsOutput" / "BuildPackageProject"; + MetaCoreExpect(result.OutputRoot == outputRoot, "BuildService should use runtime project output root"); + MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCorePlayer.exe"), "Build output should include MetaCorePlayer.exe"); + MetaCoreExpect(std::filesystem::exists(outputRoot / "MetaCore.project.json"), "Build output should include project descriptor"); + const auto packagedProjectFile = MetaCore::MetaCoreReadProjectFile(outputRoot / "MetaCore.project.json"); + MetaCoreExpect(packagedProjectFile.has_value(), "Build output project descriptor should be readable"); + MetaCoreExpect( + packagedProjectFile->RuntimeDirectory == std::filesystem::path("ConfigRuntime"), + "Build output project descriptor should preserve runtime_directory" + ); + MetaCoreExpect(std::filesystem::exists(outputRoot / scenePath), "Build output should include startup scene"); + MetaCoreExpect(std::filesystem::exists(outputRoot / uiPath), "Build output should include startup UI"); + MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeProjectPath), "Build output should include runtime project config"); + MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeProject.DataSourcesPath), "Build output should include custom runtime data sources path"); + MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeProject.BindingsPath), "Build output should include custom runtime bindings path"); + MetaCoreExpect(std::filesystem::exists(outputRoot / runtimeReplayPath), "Build output should include RuntimeData file replay dependency"); + const auto packagedLooseRuntimeProject = MetaCore::MetaCoreReadRuntimeProjectDocument( + outputRoot / runtimeProjectPath, + registry + ); + MetaCoreExpect(packagedLooseRuntimeProject.has_value(), "Build output runtime project should be readable"); + MetaCoreExpect(packagedLooseRuntimeProject->BuildProfileName == "BuildSmokeProfile", "Build output should preserve saved build profile"); + MetaCoreExpect(packagedLooseRuntimeProject->OutputDirectory == runtimeProject.OutputDirectory, "Build output should preserve saved output directory"); + MetaCoreExpect(std::filesystem::exists(outputRoot / "Library" / "Cooked" / "Windows" / "CookManifest.bin"), "Build output should include CookManifest"); + const std::filesystem::path cookedMaterialPath = cookService->GetCookedPathForAsset(materialRecord->Guid); + MetaCoreExpect(!cookedMaterialPath.empty(), "BuildService should cook material dependency"); + MetaCoreExpect(std::filesystem::exists(outputRoot / cookedMaterialPath), "Build output should include cooked material dependency"); + MetaCoreExpect(!result.CookedAssets.empty(), "Build result should record cooked assets"); + const auto materialReportIterator = std::find_if( + result.DependencyReport.begin(), + result.DependencyReport.end(), + [&](const MetaCore::MetaCoreBuildDependencyReportEntry& entry) { + return entry.AssetGuid == materialRecord->Guid; + } + ); + MetaCoreExpect(materialReportIterator != result.DependencyReport.end(), "Build result should report material dependency"); + MetaCoreExpect(materialReportIterator->Status == "Cooked", "Build material dependency report should be cooked"); + MetaCoreExpect( + materialReportIterator->Reason == "MeshRenderer.MaterialAssetGuids", + "Build material dependency report should include reference reason" + ); + MetaCoreExpect( + materialReportIterator->CookedPath == cookedMaterialPath, + "Build material dependency report should include cooked path" + ); + + MetaCore::MetaCoreBuildPlayerPackageRequest cookedOnlyRequest; + cookedOnlyRequest.PlayerExecutablePath = tempProjectRoot / "Tools" / "MetaCorePlayer.exe"; + cookedOnlyRequest.OutputDirectory = std::filesystem::path("BuildCooked"); + cookedOnlyRequest.CopyLooseProjectContent = false; + cookedOnlyRequest.UseCookedAssetsInPackage = true; + const MetaCore::MetaCoreBuildPlayerPackageResult cookedOnlyResult = + buildService->BuildPlayerPackage(cookedOnlyRequest); + MetaCoreExpect( + cookedOnlyResult.Success, + ("BuildService should create cooked-only player package: " + cookedOnlyResult.Error).c_str() + ); + const std::filesystem::path cookedOnlyOutputRoot = + tempProjectRoot / "BuildCooked" / "BuildPackageProject"; + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / "MetaCorePlayer.exe"), "Cooked-only build output should include MetaCorePlayer.exe"); + MetaCoreExpect(!std::filesystem::exists(cookedOnlyOutputRoot / scenePath), "Cooked-only build output should not copy loose startup scene"); + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProjectPath), "Cooked-only build output should include runtime project config"); + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProject.DataSourcesPath), "Cooked-only build output should include custom runtime data sources path"); + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeProject.BindingsPath), "Cooked-only build output should include custom runtime bindings path"); + MetaCoreExpect(std::filesystem::exists(cookedOnlyOutputRoot / runtimeReplayPath), "Cooked-only build output should include RuntimeData file replay dependency"); + const auto packagedRuntimeProject = MetaCore::MetaCoreReadRuntimeProjectDocument( + cookedOnlyOutputRoot / runtimeProjectPath, + registry + ); + MetaCoreExpect(packagedRuntimeProject.has_value(), "Cooked-only runtime project should be readable"); + MetaCoreExpect(packagedRuntimeProject->UseCookedAssets, "Cooked-only runtime project should enable cooked assets"); + MetaCoreExpect( + std::filesystem::exists(cookedOnlyOutputRoot / "Library" / "Cooked" / "Windows" / "CookManifest.bin"), + "Cooked-only build output should include CookManifest" + ); + MetaCoreExpect(!cookedOnlyResult.CookedAssets.empty(), "Cooked-only build result should record cooked assets"); + + runtimeProject.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), + "Build package test should rewrite unsafe runtime project" + ); + const MetaCore::MetaCoreBuildPlayerPackageResult unsafeRuntimeProjectResult = + buildService->BuildPlayerPackage(request); + MetaCoreExpect(!unsafeRuntimeProjectResult.Success, "BuildService should reject unsafe Runtime project data source paths"); + MetaCoreExpect( + unsafeRuntimeProjectResult.Error.find("Unsafe Runtime project DataSourcesPath") != std::string::npos, + "BuildService should report unsafe Runtime project path errors" + ); + + runtimeProject.DataSourcesPath = std::filesystem::path("Config") / "RuntimeData" / "DataSources.mcruntime"; + runtimeProject.StartupUiPath = std::filesystem::path("..") / "OutsideHud.mcui.json"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), + "Build package test should rewrite unsafe runtime startup UI" + ); + const MetaCore::MetaCoreBuildPlayerPackageResult unsafeStartupUiResult = + buildService->BuildPlayerPackage(request); + MetaCoreExpect(!unsafeStartupUiResult.Success, "BuildService should reject unsafe Runtime project startup UI paths"); + MetaCoreExpect( + unsafeStartupUiResult.Error.find("Unsafe Runtime project StartupUiPath") != std::string::npos, + "BuildService should report unsafe Runtime project startup UI errors" + ); + + runtimeProject.StartupUiPath = uiPath; + runtimeProject.CookedAssetsDirectory = std::filesystem::path("..") / "CookedOutside"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), + "Build package test should rewrite unsafe runtime cooked assets directory" + ); + const MetaCore::MetaCoreBuildPlayerPackageResult unsafeCookedAssetsResult = + buildService->BuildPlayerPackage(request); + MetaCoreExpect(!unsafeCookedAssetsResult.Success, "BuildService should reject unsafe Runtime project cooked assets directories"); + MetaCoreExpect( + unsafeCookedAssetsResult.Error.find("Unsafe Runtime project CookedAssetsDirectory") != std::string::npos, + "BuildService should report unsafe Runtime project cooked assets directory errors" + ); + + runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; + MetaCore::MetaCoreRuntimeDataSourcesDocument unsafeReplaySources = runtimeSources; + unsafeReplaySources.Sources.front().ConnectionSettings = { + MetaCore::MetaCoreDataSourceSetting{"file_path", (std::filesystem::path("..") / "OutsideReplay.mcstream").generic_string()} + }; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(tempProjectRoot / runtimeProject.DataSourcesPath, unsafeReplaySources, registry), + "Build package test should rewrite unsafe RuntimeData replay path" + ); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument(tempProjectRoot / runtimeProjectPath, runtimeProject, registry), + "Build package test should restore safe runtime project before unsafe replay path test" + ); + const MetaCore::MetaCoreBuildPlayerPackageResult unsafeReplayPathResult = + buildService->BuildPlayerPackage(request); + MetaCoreExpect(!unsafeReplayPathResult.Success, "BuildService should reject unsafe RuntimeData file replay paths"); + MetaCoreExpect( + unsafeReplayPathResult.Error.find("Unsafe RuntimeData file_replay file_path") != std::string::npos, + "BuildService should report unsafe RuntimeData file replay path errors" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestComponentRegistryOperations() { MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; @@ -1969,6 +2842,13 @@ void MetaCoreTestRuntimeDataBinaryDocumentIo() { MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); + bindingsDocument.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ + "binding.ui.cube.position", + "cube.position", + "runtime.status", + MetaCore::MetaCoreRuntimeUiBindingTarget::Text, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); MetaCoreExpect( MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(sourcesPath, sourcesDocument, registry), @@ -1986,6 +2866,9 @@ void MetaCoreTestRuntimeDataBinaryDocumentIo() { MetaCoreExpect(loadedSources->Sources.size() == 1, "读回的 RuntimeDataSourcesDocument 应保留 source"); MetaCoreExpect(loadedBindings->Bindings.size() == 1, "读回的 RuntimeBindingsDocument 应保留 binding"); + MetaCoreExpect(loadedBindings->UiBindings.size() == 1, "RuntimeBindingsDocument should preserve UI bindings"); + MetaCoreExpect(loadedBindings->UiBindings.front().TargetNodeId == "runtime.status", "UI binding target node id should round trip"); + std::filesystem::remove_all(tempDirectory); } @@ -1996,6 +2879,12 @@ void MetaCoreTestRuntimeProjectDocumentIo() { const std::filesystem::path tempPath = std::filesystem::temp_directory_path() / "MetaCoreRuntimeProjectDocument.mcruntimecfg"; MetaCore::MetaCoreRuntimeProjectDocument writtenDocument; writtenDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; + writtenDocument.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; + writtenDocument.BuildProfileName = "Shipping"; + writtenDocument.TargetPlatform = "Windows"; + writtenDocument.OutputDirectory = std::filesystem::path("Build") / "Windows"; + writtenDocument.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; + writtenDocument.UseCookedAssets = true; writtenDocument.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; writtenDocument.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; writtenDocument.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; @@ -2007,6 +2896,80 @@ void MetaCoreTestRuntimeProjectDocumentIo() { MetaCoreExpect(loadedDocument->StartupScenePath == writtenDocument.StartupScenePath, "StartupScenePath 应一致"); MetaCoreExpect(loadedDocument->DiagnosticsPath == writtenDocument.DiagnosticsPath, "DiagnosticsPath 应一致"); + MetaCoreExpect(loadedDocument->StartupUiPath == writtenDocument.StartupUiPath, "StartupUiPath should round trip"); + MetaCoreExpect(loadedDocument->BuildProfileName == writtenDocument.BuildProfileName, "BuildProfileName should round trip"); + MetaCoreExpect(loadedDocument->TargetPlatform == writtenDocument.TargetPlatform, "TargetPlatform should round trip"); + MetaCoreExpect(loadedDocument->OutputDirectory == writtenDocument.OutputDirectory, "OutputDirectory should round trip"); + MetaCoreExpect(loadedDocument->CookedAssetsDirectory == writtenDocument.CookedAssetsDirectory, "CookedAssetsDirectory should round trip"); + MetaCoreExpect(loadedDocument->UseCookedAssets == writtenDocument.UseCookedAssets, "UseCookedAssets should round trip"); + + const std::filesystem::path projectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeProjectDefaults"; + const std::filesystem::path runtimeRoot = projectRoot / "ConfigRuntime"; + MetaCoreExpect( + MetaCore::MetaCoreBuildRuntimeDirectoryRelativePath(projectRoot, runtimeRoot) == std::filesystem::path("ConfigRuntime"), + "Runtime directory helper should return project-relative runtime_directory" + ); + MetaCoreExpect( + MetaCore::MetaCoreBuildRuntimeDirectoryRelativePath(projectRoot, projectRoot / ".." / "OutsideRuntime") == std::filesystem::path("Runtime"), + "Runtime directory helper should fall back when runtime_directory escapes the project" + ); + + const auto defaultRuntimeProject = + MetaCore::MetaCoreBuildDefaultRuntimeProjectDocument(std::filesystem::path("ConfigRuntime")); + MetaCoreExpect( + defaultRuntimeProject.StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", + "RuntimeProject defaults should use the JSON scene asset extension" + ); + MetaCoreExpect( + defaultRuntimeProject.DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", + "RuntimeProject defaults should place DataSources under runtime_directory" + ); + MetaCoreExpect( + defaultRuntimeProject.BindingsPath == std::filesystem::path("ConfigRuntime") / "Bindings.mcruntime", + "RuntimeProject defaults should place Bindings under runtime_directory" + ); + MetaCoreExpect( + defaultRuntimeProject.DiagnosticsPath == std::filesystem::path("ConfigRuntime") / "Diagnostics.mcruntimestate", + "RuntimeProject defaults should place Diagnostics under runtime_directory" + ); + + MetaCore::MetaCoreRuntimeProjectDocument partialDocument; + partialDocument.StartupScenePath = std::filesystem::path("Scenes") / "Custom.mcscene.json"; + MetaCore::MetaCoreApplyRuntimeProjectDefaults(partialDocument, std::filesystem::path("ConfigRuntime")); + MetaCoreExpect( + partialDocument.StartupScenePath == std::filesystem::path("Scenes") / "Custom.mcscene.json", + "RuntimeProject defaults should preserve explicit startup scene paths" + ); + MetaCoreExpect( + partialDocument.DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", + "RuntimeProject default application should fill missing DataSourcesPath from runtime_directory" + ); + + MetaCoreExpect( + !MetaCore::MetaCoreIsUnsafeRuntimeProjectPath(std::filesystem::path("Runtime") / "DataSources.mcruntime"), + "Runtime project validation should accept safe project-relative paths" + ); + MetaCoreExpect( + MetaCore::MetaCoreIsUnsafeRuntimeProjectPath(std::filesystem::path("..") / "Outside.mcruntime"), + "Runtime project validation should reject parent traversal paths" + ); + MetaCore::MetaCoreRuntimeProjectDocument unsafeDocument = writtenDocument; + unsafeDocument.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; + const auto unsafeRuntimeProjectIssues = MetaCore::MetaCoreValidateRuntimeProjectPaths(unsafeDocument); + MetaCoreExpect(!unsafeRuntimeProjectIssues.empty(), "Runtime project validation should report unsafe paths"); + MetaCoreExpect( + std::any_of( + unsafeRuntimeProjectIssues.begin(), + unsafeRuntimeProjectIssues.end(), + [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "RuntimeProject" && + issue.Message.find("DataSourcesPath") != std::string::npos; + } + ), + "Runtime project validation should identify the unsafe DataSourcesPath" + ); + std::filesystem::remove(tempPath); } @@ -2142,6 +3105,97 @@ void MetaCoreTestRuntimeDataDispatcherAppliesUpdates() { MetaCoreExpect(!cube.GetComponent().Visible, "Dispatcher 应更新 MeshRenderer.Visible"); } +void MetaCoreTestRuntimeDataDispatcherRejectsInvalidUpdateDiagnostics() { + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject cube = scene.CreateGameObject("Cube"); + cube.AddComponent(); + cube.GetComponent().Visible = true; + + MetaCore::MetaCoreRuntimeDataDispatcher dispatcher(scene); + dispatcher.SetDataPointDefinitions({ + MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + "mock-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + } + }); + dispatcher.SetBindingDefinitions({ + MetaCore::MetaCoreSceneBindingDefinition{ + "binding.visible", + "cube.visible", + cube.GetId(), + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + } + }); + + dispatcher.ApplyUpdates({ + MetaCore::MetaCoreRuntimeDataUpdate{ + "cube.visible", + MetaCore::MetaCoreRuntimeDataValue{ + MetaCore::MetaCoreRuntimeValueType::Double, + false, + 0, + 1.0, + {}, + glm::vec3(0.0F, 0.0F, 0.0F), + 100, + MetaCore::MetaCoreRuntimeDataQuality::Good + }, + 1 + } + }); + MetaCoreExpect(cube.GetComponent().Visible, "Invalid RuntimeData type should not change target component"); + MetaCoreExpect(!dispatcher.GetBindingStatuses().front().Healthy, "Invalid RuntimeData type should mark binding unhealthy"); + MetaCoreExpect( + dispatcher.GetBindingStatuses().front().LastError == "Update type does not match data point definition", + "Invalid RuntimeData type should record a precise binding diagnostic" + ); + + dispatcher.ApplyUpdates({ + MetaCore::MetaCoreRuntimeDataUpdate{ + "cube.visible", + MetaCore::MetaCoreRuntimeDataValue{ + MetaCore::MetaCoreRuntimeValueType::Bool, + false, + 0, + 0.0, + {}, + glm::vec3(0.0F, 0.0F, 0.0F), + 200, + MetaCore::MetaCoreRuntimeDataQuality::Bad + }, + 2 + } + }); + MetaCoreExpect(cube.GetComponent().Visible, "Bad quality RuntimeData should not change target component"); + MetaCoreExpect( + dispatcher.GetBindingStatuses().front().LastError == "Update quality is bad", + "Bad quality RuntimeData should record a precise binding diagnostic" + ); + + dispatcher.ApplyUpdates({ + MetaCore::MetaCoreRuntimeDataUpdate{ + "cube.visible", + MetaCore::MetaCoreRuntimeDataValue{ + MetaCore::MetaCoreRuntimeValueType::Bool, + false, + 0, + 0.0, + {}, + glm::vec3(0.0F, 0.0F, 0.0F), + 300, + MetaCore::MetaCoreRuntimeDataQuality::Good + }, + 3 + } + }); + MetaCoreExpect(!cube.GetComponent().Visible, "Good RuntimeData should still apply after rejected updates"); + MetaCoreExpect(dispatcher.GetBindingStatuses().front().Healthy, "Good RuntimeData should restore binding health"); + MetaCoreExpect(dispatcher.GetBindingStatuses().front().LastError.empty(), "Good RuntimeData should clear prior binding diagnostics"); +} + void MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates() { MetaCore::MetaCoreMockRuntimeDataSourceAdapter adapter; MetaCoreExpect(adapter.Configure(MetaCore::MetaCoreDataSourceDefinition{ @@ -2338,8 +3392,10 @@ void MetaCoreTestEditorContextRuntimeDataConfigSaveLoad() { std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorConfigProject"; std::filesystem::remove_all(tempProjectRoot); std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); std::filesystem::create_directories(tempProjectRoot / "Scenes"); std::filesystem::create_directories(tempProjectRoot / "Library"); + std::filesystem::create_directories(tempProjectRoot / "Runtime"); { std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); @@ -2351,12 +3407,66 @@ void MetaCoreTestEditorContextRuntimeDataConfigSaveLoad() { << "}\n"; } + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + MetaCore::MetaCoreRuntimeProjectDocument existingRuntimeProject; + existingRuntimeProject.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + existingRuntimeProject.StartupUiPath = std::filesystem::path("Assets") / "UI" / "CustomHud.mcui.json"; + existingRuntimeProject.BuildProfileName = "PreservedProfile"; + existingRuntimeProject.TargetPlatform = "Windows"; + existingRuntimeProject.OutputDirectory = std::filesystem::path("Build") / "Preserved"; + existingRuntimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Preserved"; + existingRuntimeProject.UseCookedAssets = true; + existingRuntimeProject.DataSourcesPath = std::filesystem::path("Runtime") / "CustomSources.mcruntime"; + existingRuntimeProject.BindingsPath = std::filesystem::path("Runtime") / "CustomBindings.mcruntime"; + existingRuntimeProject.DiagnosticsPath = std::filesystem::path("Runtime") / "CustomDiagnostics.mcruntimestate"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument( + tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + existingRuntimeProject, + registry + ), + "Existing runtime project settings should be writable before editor save" + ); + MetaCore::MetaCoreRuntimeDataSourcesDocument existingSourcesDocument; + existingSourcesDocument.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "existing-source", + "file_replay", + "Existing Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/ExistingReplay.mcstream"}}, + true, + 1000 + }); + existingSourcesDocument.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "existing.status", + "existing-source", + "existing.status", + MetaCore::MetaCoreRuntimeValueType::String + }); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeDataSourcesDocument( + tempProjectRoot / existingRuntimeProject.DataSourcesPath, + existingSourcesDocument, + registry + ), + "Existing runtime data sources should be writable at the custom path" + ); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeBindingsDocument( + tempProjectRoot / existingRuntimeProject.BindingsPath, + MetaCore::MetaCoreRuntimeBindingsDocument{}, + registry + ), + "Existing runtime bindings should be writable at the custom path" + ); + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); MetaCore::MetaCoreWindow window; MetaCore::MetaCoreRenderDevice renderDevice; MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreGameObject runtimeCube = scene.CreateGameObject("RuntimeCube"); MetaCore::MetaCoreLogService logService; MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; @@ -2392,33 +3502,253 @@ void MetaCoreTestEditorContextRuntimeDataConfigSaveLoad() { bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ "binding.cube.position", "cube.position", - 3, + runtimeCube.GetId(), MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); MetaCoreExpect(editorContext.SaveRuntimeDataConfig(), "EditorContext 应能保存 Runtime 配置"); - MetaCore::MetaCoreTypeRegistry registry; - MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + const auto loadedProjectRuntime = MetaCore::MetaCoreReadRuntimeProjectDocument( + tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + registry + ); + MetaCoreExpect(loadedProjectRuntime.has_value(), "Runtime project should still be readable after RuntimeData save"); const auto loadedSources = MetaCore::MetaCoreReadRuntimeDataSourcesDocument( - tempProjectRoot / "Runtime" / "DataSources.mcruntime", + tempProjectRoot / loadedProjectRuntime->DataSourcesPath, registry ); const auto loadedBindings = MetaCore::MetaCoreReadRuntimeBindingsDocument( - tempProjectRoot / "Runtime" / "Bindings.mcruntime", - registry - ); - const auto loadedProjectRuntime = MetaCore::MetaCoreReadRuntimeProjectDocument( - tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + tempProjectRoot / loadedProjectRuntime->BindingsPath, registry ); MetaCoreExpect(loadedSources.has_value(), "保存后应能读回 Runtime data sources"); MetaCoreExpect(loadedBindings.has_value(), "保存后应能读回 Runtime bindings"); MetaCoreExpect(loadedProjectRuntime.has_value(), "保存后应能读回 Runtime project"); - MetaCoreExpect(loadedSources->Sources.size() == 1, "保存后应保留 source"); + MetaCoreExpect(loadedSources->Sources.size() == 2, "保存后应保留 source"); MetaCoreExpect(loadedBindings->Bindings.size() == 1, "保存后应保留 binding"); MetaCoreExpect(loadedProjectRuntime->StartupScenePath == std::filesystem::path("Scenes") / "Main.mcscene.json", "保存后应写入默认 startup scene 路径"); + MetaCoreExpect( + loadedProjectRuntime->StartupUiPath == existingRuntimeProject.StartupUiPath, + "RuntimeData save should preserve existing startup UI" + ); + MetaCoreExpect(loadedProjectRuntime->BuildProfileName == "PreservedProfile", "RuntimeData save should preserve build profile"); + MetaCoreExpect(loadedProjectRuntime->TargetPlatform == "Windows", "Runtime project should preserve target platform"); + MetaCoreExpect( + loadedProjectRuntime->OutputDirectory == existingRuntimeProject.OutputDirectory, + "RuntimeData save should preserve output directory" + ); + MetaCoreExpect( + loadedProjectRuntime->CookedAssetsDirectory == existingRuntimeProject.CookedAssetsDirectory, + "RuntimeData save should preserve cooked asset directory" + ); + MetaCoreExpect(loadedProjectRuntime->UseCookedAssets, "RuntimeData save should preserve cooked-assets mode"); + MetaCoreExpect( + loadedProjectRuntime->DataSourcesPath == existingRuntimeProject.DataSourcesPath, + "RuntimeData save should preserve custom data sources path" + ); + MetaCoreExpect( + loadedProjectRuntime->BindingsPath == existingRuntimeProject.BindingsPath, + "RuntimeData save should preserve custom bindings path" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestEditorContextLoadsRuntimeDiagnosticsFromProjectPath() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorDiagnosticsProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Runtime"); + std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); + std::filesystem::create_directories(tempProjectRoot / "Config" / "RuntimeData"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeDiagnosticsProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"runtime_directory\": \"ConfigRuntime\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + + MetaCore::MetaCoreRuntimeProjectDocument runtimeProject; + runtimeProject.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + runtimeProject.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; + runtimeProject.BuildProfileName = "Development"; + runtimeProject.TargetPlatform = "Windows"; + runtimeProject.OutputDirectory = std::filesystem::path("Build") / "Windows"; + runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; + runtimeProject.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; + runtimeProject.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + runtimeProject.DiagnosticsPath = std::filesystem::path("Config") / "RuntimeData" / "CustomDiagnostics.mcruntimestate"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument( + tempProjectRoot / "ConfigRuntime" / "ProjectRuntime.mcruntimecfg", + runtimeProject, + registry + ), + "Runtime project with custom runtime and diagnostics paths should be writable" + ); + + MetaCore::MetaCoreRuntimeDiagnosticsSnapshot defaultSnapshot; + defaultSnapshot.SourceStatuses.push_back(MetaCore::MetaCoreRuntimeDataSourceStatus{ + "default-diagnostics-source", + MetaCore::MetaCoreRuntimeDataSourceState::Connected, + 1, + 2, + {} + }); + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot( + tempProjectRoot / "Runtime" / "Diagnostics.mcruntimestate", + defaultSnapshot, + registry + ), + "Default runtime diagnostics should be writable" + ); + + MetaCore::MetaCoreRuntimeDiagnosticsSnapshot customSnapshot; + customSnapshot.SourceStatuses.push_back(MetaCore::MetaCoreRuntimeDataSourceStatus{ + "custom-diagnostics-source", + MetaCore::MetaCoreRuntimeDataSourceState::Degraded, + 10, + 20, + "custom fault" + }); + customSnapshot.HasFaults = true; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot( + tempProjectRoot / runtimeProject.DiagnosticsPath, + customSnapshot, + registry + ), + "Custom runtime diagnostics should be writable" + ); + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto assetDatabase = moduleRegistry.ResolveService(); + MetaCoreExpect(assetDatabase != nullptr, "AssetDatabaseService should be available for diagnostics runtime path test"); + MetaCoreExpect( + assetDatabase->GetProjectDescriptor().RuntimePath.filename() == "ConfigRuntime", + "Diagnostics runtime path test should load custom project runtime directory" + ); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto loadedSnapshot = editorContext.LoadRuntimeDiagnosticsSnapshot(); + MetaCoreExpect(loadedSnapshot.has_value(), "EditorContext should load Runtime diagnostics"); + MetaCoreExpect(loadedSnapshot->HasFaults, "EditorContext should preserve custom diagnostics fault state"); + MetaCoreExpect(loadedSnapshot->SourceStatuses.size() == 1, "EditorContext diagnostics should preserve source status count"); + MetaCoreExpect( + loadedSnapshot->SourceStatuses.front().SourceId == "custom-diagnostics-source", + "EditorContext should load diagnostics from ProjectRuntime DiagnosticsPath" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestEditorContextRuntimeDataDefaultsFollowRuntimeDirectory() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorDefaultRuntimeDirectoryProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "ConfigRuntime"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeDefaultDirectoryProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"runtime_directory\": \"ConfigRuntime\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext should load default RuntimeData config"); + MetaCoreExpect(editorContext.SaveRuntimeDataConfig(), "EditorContext should save default RuntimeData config"); + + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + const auto runtimeProject = MetaCore::MetaCoreReadRuntimeProjectDocument( + tempProjectRoot / "ConfigRuntime" / "ProjectRuntime.mcruntimecfg", + registry + ); + MetaCoreExpect(runtimeProject.has_value(), "Default RuntimeProject should be saved under custom runtime_directory"); + MetaCoreExpect( + runtimeProject->DataSourcesPath == std::filesystem::path("ConfigRuntime") / "DataSources.mcruntime", + "Default RuntimeData sources path should follow runtime_directory" + ); + MetaCoreExpect( + runtimeProject->BindingsPath == std::filesystem::path("ConfigRuntime") / "Bindings.mcruntime", + "Default RuntimeData bindings path should follow runtime_directory" + ); + MetaCoreExpect( + runtimeProject->DiagnosticsPath == std::filesystem::path("ConfigRuntime") / "Diagnostics.mcruntimestate", + "Default Runtime diagnostics path should follow runtime_directory" + ); + MetaCoreExpect( + std::filesystem::exists(tempProjectRoot / runtimeProject->DataSourcesPath), + "Default RuntimeData sources document should be written under runtime_directory" + ); + MetaCoreExpect( + std::filesystem::exists(tempProjectRoot / runtimeProject->BindingsPath), + "Default RuntimeData bindings document should be written under runtime_directory" + ); coreServicesModule->Shutdown(moduleRegistry); moduleRegistry.ShutdownServices(); @@ -2456,6 +3786,81 @@ void MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding() { ); } +void MetaCoreTestRuntimeDataConfigValidationRejectsBindingTypeMismatch() { + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "cube.visible", + "replay-source", + "cube.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + }); + + MetaCore::MetaCoreRuntimeBindingsDocument bindings; + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.invalid.type", + "cube.visible", + 1, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, bindings); + MetaCoreExpect( + std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "binding.invalid.type" && + issue.Message.find("expects Vec3") != std::string::npos && + issue.Message.find("DataPoint is Bool") != std::string::npos; + }), + "RuntimeData validation should reject DataPoint type mismatches for binding targets" + ); +} + +void MetaCoreTestRuntimeDataConfigValidationRejectsMissingUiTargetNode() { + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "runtime.status", + "replay-source", + "runtime.status", + MetaCore::MetaCoreRuntimeValueType::String + }); + + MetaCore::MetaCoreRuntimeBindingsDocument bindings; + bindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ + "binding.ui.status", + "runtime.status", + "", + MetaCore::MetaCoreRuntimeUiBindingTarget::Text, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + const auto issues = MetaCore::MetaCoreValidateRuntimeDataDocuments(sources, bindings); + MetaCoreExpect( + std::any_of(issues.begin(), issues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "binding.ui.status" && + issue.Message.find("TargetNodeId") != std::string::npos; + }), + "RuntimeData validation should reject UI bindings without target node ids" + ); +} + void MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath() { MetaCore::MetaCoreRuntimeDataSourcesDocument sources; sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ @@ -2566,6 +3971,372 @@ void MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream() { MetaCoreExpect(false, "Tcp adapter 应在轮询内收到更新"); } +void MetaCoreTestEditorContextRejectsUnsafeRuntimeProjectPaths() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorUnsafePathProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + std::filesystem::create_directories(tempProjectRoot / "Runtime"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeUnsafePathProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + MetaCore::MetaCoreTypeRegistry registry; + MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + MetaCore::MetaCoreRuntimeProjectDocument runtimeProject; + runtimeProject.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + runtimeProject.StartupUiPath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; + runtimeProject.BuildProfileName = "Development"; + runtimeProject.TargetPlatform = "Windows"; + runtimeProject.OutputDirectory = std::filesystem::path("Build") / "Windows"; + runtimeProject.CookedAssetsDirectory = std::filesystem::path("Library") / "Cooked" / "Windows"; + runtimeProject.DataSourcesPath = std::filesystem::path("..") / "OutsideDataSources.mcruntime"; + runtimeProject.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; + runtimeProject.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + MetaCoreExpect( + MetaCore::MetaCoreWriteRuntimeProjectDocument( + tempProjectRoot / "Runtime" / "ProjectRuntime.mcruntimecfg", + runtimeProject, + registry + ), + "Unsafe runtime project should be writable for validation smoke test" + ); + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect( + !editorContext.EnsureRuntimeDataConfigLoaded(), + "EditorContext should reject unsafe Runtime project data source paths" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestEditorContextRejectsUnsafeRuntimeReplayPath() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorUnsafeReplayPathProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeUnsafeReplayPathProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext should load runtime config for replay path validation"); + auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "unsafe-replay-source", + "file_replay", + "Unsafe Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", (std::filesystem::path("..") / "OutsideReplay.mcstream").generic_string()}}, + true, + 1000 + }); + + const auto validationIssues = editorContext.ValidateRuntimeDataConfig(); + MetaCoreExpect( + std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "unsafe-replay-source" && + issue.Message.find("file_replay file_path") != std::string::npos; + }), + "EditorContext Validate should reject unsafe RuntimeData replay file paths" + ); + MetaCoreExpect( + !editorContext.SaveRuntimeDataConfig(), + "EditorContext should reject saving unsafe RuntimeData replay file paths" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestEditorContextRejectsMissingRuntimeUiBindingNode() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorMissingUiBindingProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets" / "UI"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + std::filesystem::create_directories(tempProjectRoot / "Runtime"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeMissingUiBindingProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + MetaCore::MetaCoreTypeRegistry uiRegistry; + MetaCoreRegisterFoundationGeneratedTypes(uiRegistry); + MetaCoreRegisterSceneGeneratedTypes(uiRegistry); + MetaCoreRegisterEditorGeneratedTypes(uiRegistry); + + MetaCore::MetaCoreUiDocument uiDocument; + uiDocument.Name = "Hud"; + uiDocument.RootNodeIds = {"root"}; + + MetaCore::MetaCoreUiNodeDocument rootNode; + rootNode.Id = "root"; + rootNode.Name = "Root"; + rootNode.Type = MetaCore::MetaCoreUiNodeType::Panel; + rootNode.Children = {"runtime.status"}; + + MetaCore::MetaCoreUiNodeDocument statusNode; + statusNode.Id = "runtime.status"; + statusNode.Name = "Runtime Status"; + statusNode.Type = MetaCore::MetaCoreUiNodeType::Text; + statusNode.ParentId = "root"; + statusNode.Text = "Waiting"; + uiDocument.Nodes = {rootNode, statusNode}; + + MetaCoreExpect( + MetaCore::MetaCoreSceneSerializer::SaveUiToJson( + tempProjectRoot / "Assets" / "UI" / "Hud.mcui.json", + uiDocument, + uiRegistry + ), + "Startup UI should be writable for RuntimeData validation" + ); + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext should load runtime config for UI validation"); + auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); + auto& bindings = editorContext.AccessRuntimeBindingsDocument(); + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "replay-source", + "file_replay", + "Replay Source", + {MetaCore::MetaCoreDataSourceSetting{"file_path", "Runtime/RuntimeReplay.mcstream"}}, + true, + 1000 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "runtime.status", + "replay-source", + "runtime.status", + MetaCore::MetaCoreRuntimeValueType::String + }); + bindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ + "binding.ui.missing", + "runtime.status", + "missing.status", + MetaCore::MetaCoreRuntimeUiBindingTarget::Text, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + const auto validationIssues = editorContext.ValidateRuntimeDataConfig(); + MetaCoreExpect( + std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "binding.ui.missing" && + issue.Message.find("Target UI node does not exist") != std::string::npos; + }), + "EditorContext Validate should report missing Startup UI binding targets" + ); + + MetaCoreExpect( + !editorContext.SaveRuntimeDataConfig(), + "EditorContext should reject RuntimeData UI bindings that target missing Startup UI nodes" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + +void MetaCoreTestEditorContextValidatesRuntimeSceneBindingTargets() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorSceneBindingProject"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + projectFile << "{\n" + << " \"name\": \"RuntimeSceneBindingProject\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject transformOnlyObject = scene.CreateGameObject("TransformOnly"); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect(editorContext.EnsureRuntimeDataConfigLoaded(), "EditorContext should load runtime config for scene binding validation"); + auto& sources = editorContext.AccessRuntimeDataSourcesDocument(); + auto& bindings = editorContext.AccessRuntimeBindingsDocument(); + sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ + "mock-source", + "mock", + "Mock Source", + {}, + true, + 1000 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "pump.position", + "mock-source", + "pump.position", + MetaCore::MetaCoreRuntimeValueType::Vec3 + }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "pump.visible", + "mock-source", + "pump.visible", + MetaCore::MetaCoreRuntimeValueType::Bool + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.scene.missing-object", + "pump.position", + 999999, + MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ + "binding.scene.missing-mesh", + "pump.visible", + transformOnlyObject.GetId(), + MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); + + const auto validationIssues = editorContext.ValidateRuntimeDataConfig(); + MetaCoreExpect( + std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "binding.scene.missing-object" && + issue.Message.find("Target scene object does not exist") != std::string::npos; + }), + "EditorContext Validate should report missing RuntimeData scene target objects" + ); + MetaCoreExpect( + std::any_of(validationIssues.begin(), validationIssues.end(), [](const MetaCore::MetaCoreRuntimeConfigIssue& issue) { + return issue.Severity == MetaCore::MetaCoreRuntimeConfigIssueSeverity::Error && + issue.Scope == "binding.scene.missing-mesh" && + issue.Message.find("MeshRenderer") != std::string::npos; + }), + "EditorContext Validate should report RuntimeData scene target component mismatches" + ); + MetaCoreExpect( + !editorContext.SaveRuntimeDataConfig(), + "EditorContext should reject RuntimeData scene bindings that cannot resolve their target components" + ); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave() { const std::filesystem::path tempProjectRoot = std::filesystem::temp_directory_path() / "MetaCoreRuntimeEditorInvalidConfigProject"; @@ -2698,6 +4469,180 @@ void MetaCoreTestUiDocumentSerialization() { MetaCoreExpectVec3Near(roundTrip.Nodes[0].Style.BackgroundColor, glm::vec3(0.05F, 0.08F, 0.12F), "Ui 背景色应保持"); } +void MetaCoreTestUiDocumentRmlCompilation() { + MetaCore::MetaCoreUiDocument document; + document.Name = "RuntimeHud"; + document.ReferenceWidth = 1920; + document.ReferenceHeight = 1080; + document.RootNodeIds = {"root"}; + + MetaCore::MetaCoreUiNodeDocument root; + root.Id = "root"; + root.Name = "Root"; + root.Type = MetaCore::MetaCoreUiNodeType::Panel; + root.Children = {"title", "status.image", "start.button"}; + root.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); + root.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); + root.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); + root.Style.BackgroundColor = glm::vec3(0.05F, 0.08F, 0.12F); + + MetaCore::MetaCoreUiNodeDocument title; + title.Id = "title"; + title.Name = "Title"; + title.Type = MetaCore::MetaCoreUiNodeType::Text; + title.ParentId = "root"; + title.Text = "MetaCore "; + title.RectTransform.Position = glm::vec3(32.0F, 24.0F, 0.0F); + title.RectTransform.Size = glm::vec3(520.0F, 56.0F, 0.0F); + title.Style.FontSize = 28.0F; + title.Style.HorizontalAlignment = MetaCore::MetaCoreUiHorizontalAlignment::Center; + + MetaCore::MetaCoreUiNodeDocument image; + image.Id = "status.image"; + image.Name = "Status Image"; + image.Type = MetaCore::MetaCoreUiNodeType::Image; + image.ParentId = "root"; + image.RectTransform.Position = glm::vec3(32.0F, 96.0F, 0.0F); + image.RectTransform.Size = glm::vec3(96.0F, 96.0F, 0.0F); + image.Style.TintColor = glm::vec3(0.2F, 0.8F, 0.4F); + image.Style.PreserveAspect = true; + + MetaCore::MetaCoreUiNodeDocument button; + button.Id = "start.button"; + button.Name = "Start Button"; + button.Type = MetaCore::MetaCoreUiNodeType::Button; + button.ParentId = "root"; + button.Text = "Start & Run"; + button.Interactable = true; + button.RectTransform.Position = glm::vec3(32.0F, 216.0F, 0.0F); + button.RectTransform.Size = glm::vec3(180.0F, 48.0F, 0.0F); + button.Style.BackgroundColor = glm::vec3(0.12F, 0.42F, 0.82F); + + document.Nodes = {root, title, image, button}; + + const auto compiled = MetaCore::MetaCoreCompileUiDocumentToRml(document, "RuntimeHud.rcss"); + MetaCoreExpect(!compiled.Rml.empty(), "Compiled RML should not be empty"); + MetaCoreExpect(!compiled.Rcss.empty(), "Compiled RCSS should not be empty"); + MetaCoreExpect(compiled.Rml.find("href=\"RuntimeHud.rcss\"") != std::string::npos, "Compiled RML should reference stylesheet"); + MetaCoreExpect(compiled.Rml.find("MetaCore <Runtime>") != std::string::npos, "Compiled RML should escape text"); + MetaCoreExpect(compiled.Rml.find("Start & Run") != std::string::npos, "Compiled RML should escape button text"); + MetaCoreExpect(compiled.Rml.find("mcui-panel") != std::string::npos, "Compiled RML should include panel class"); + MetaCoreExpect(compiled.Rml.find("mcui-text") != std::string::npos, "Compiled RML should include text class"); + MetaCoreExpect(compiled.Rml.find("mcui-image") != std::string::npos, "Compiled RML should include image class"); + MetaCoreExpect(compiled.Rml.find("mcui-button") != std::string::npos, "Compiled RML should include button class"); + MetaCoreExpect(compiled.Rml.find("data-metacore-id=\"start.button\"") != std::string::npos, "Compiled RML should preserve source ids"); + MetaCoreExpect(compiled.Rcss.find(".mcui-node-0") != std::string::npos, "Compiled RCSS should include stable node class"); + MetaCoreExpect(compiled.Rcss.find("width: 1920px") != std::string::npos, "Compiled RCSS should include reference width"); + MetaCoreExpect(compiled.Rcss.find("text-align: center") != std::string::npos, "Compiled RCSS should include text alignment"); + MetaCoreExpect(compiled.Rcss.find("rgb(13, 20, 31)") != std::string::npos, "Compiled RCSS should convert colors"); + MetaCoreExpect(compiled.StylesheetHref == "RuntimeHud.rcss", "Compiled UI should preserve stylesheet href"); +} + +void MetaCoreTestRuntimeUiRendererLifecycle() { + MetaCore::MetaCoreRuntimeUiRenderer renderer; + + MetaCore::MetaCoreUiDocument document; + document.Name = "RuntimeRendererSmoke"; + document.ReferenceWidth = 1280; + document.ReferenceHeight = 720; + document.RootNodeIds = {"root"}; + + MetaCore::MetaCoreUiNodeDocument root; + root.Id = "root"; + root.Name = "Root"; + root.Type = MetaCore::MetaCoreUiNodeType::Panel; + root.Children = {"runtime.status"}; + root.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); + root.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); + root.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); + root.Style.BackgroundColor = glm::vec3(0.1F, 0.2F, 0.3F); + + MetaCore::MetaCoreUiNodeDocument statusText; + statusText.Id = "runtime.status"; + statusText.Name = "Runtime Status"; + statusText.Type = MetaCore::MetaCoreUiNodeType::Text; + statusText.ParentId = "root"; + statusText.Text = "RuntimeData starting"; + statusText.RectTransform.Position = glm::vec3(24.0F, 24.0F, 0.0F); + statusText.RectTransform.Size = glm::vec3(360.0F, 42.0F, 0.0F); + statusText.Style.FontSize = 18.0F; + document.Nodes = {root, statusText}; + + const auto compiledDocument = MetaCore::MetaCoreCompileUiDocumentToRml(document, "RuntimeRendererSmoke.rcss"); + + MetaCoreExpect(!renderer.LoadCompiledDocument(compiledDocument), "Runtime UI renderer should reject documents before initialization"); + MetaCoreExpect(!renderer.GetStats().LastError.empty(), "Runtime UI renderer should report initialization errors"); + + MetaCoreExpect(renderer.Initialize(), "Runtime UI renderer should initialize"); + renderer.Resize(1280, 720); + MetaCoreExpect(renderer.GetStats().Initialized, "Runtime UI renderer should report initialized state"); + MetaCoreExpect(renderer.GetStats().RmlInitialized, "Runtime UI renderer should initialize RmlUi core"); + MetaCoreExpect(renderer.GetStats().RmlContextCreated, "Runtime UI renderer should create an RmlUi context"); + MetaCoreExpect(renderer.GetStats().Width == 1280, "Runtime UI renderer should track width"); + MetaCoreExpect(renderer.GetStats().Height == 720, "Runtime UI renderer should track height"); + + MetaCoreExpect(renderer.LoadCompiledDocument(compiledDocument), "Runtime UI renderer should accept compiled UI documents"); + MetaCoreExpect(renderer.GetStats().Loaded, "Runtime UI renderer should report loaded UI state"); + MetaCoreExpect(renderer.GetStats().RmlDocumentLoaded, "Runtime UI renderer should load an RmlUi document"); + MetaCoreExpect(renderer.GetStats().RmlBytes == compiledDocument.Rml.size(), "Runtime UI renderer should track RML bytes"); + MetaCoreExpect(renderer.GetStats().RcssBytes == compiledDocument.Rcss.size(), "Runtime UI renderer should track RCSS bytes"); + MetaCoreExpect(renderer.GetStats().LastError.empty(), "Runtime UI renderer should clear load errors after success"); + MetaCoreExpect(renderer.HasNode("runtime.status"), "Runtime UI renderer should report existing MetaCore UI nodes"); + MetaCoreExpect(!renderer.HasNode("missing.status"), "Runtime UI renderer should report missing MetaCore UI nodes"); + MetaCoreExpect( + !renderer.SetNodeText("missing.status", "missing"), + "Runtime UI renderer should reject missing dynamic text nodes" + ); + MetaCoreExpect(!renderer.GetStats().LastError.empty(), "Runtime UI renderer should report missing dynamic text nodes"); + MetaCoreExpect( + renderer.SetNodeText("runtime.status", "RuntimeData & stable"), + "Runtime UI renderer should update dynamic text nodes by MetaCore UI id" + ); + MetaCoreExpect(renderer.GetStats().LastError.empty(), "Runtime UI renderer should clear dynamic text errors after success"); + + renderer.BeginFrame(1.0F / 60.0F); + renderer.Render(); + MetaCoreExpect(renderer.GetStats().FrameCount == 1, "Runtime UI renderer should count frames"); + MetaCoreExpect(renderer.GetStats().LastDeltaSeconds > 0.0F, "Runtime UI renderer should track delta time"); + MetaCoreExpect(renderer.GetStats().CompiledGeometryCount > 0, "Runtime UI renderer should receive RmlUi geometry"); + MetaCoreExpect(renderer.GetStats().RenderGeometryCalls > 0, "Runtime UI renderer should execute RmlUi render callbacks"); + MetaCoreExpect(renderer.GetStats().DrawCommandCount > 0, "Runtime UI renderer should emit draw commands"); + MetaCoreExpect(renderer.GetStats().DrawVertexCount > 0, "Runtime UI renderer should emit draw vertices"); + MetaCoreExpect(renderer.GetStats().DrawIndexCount > 0, "Runtime UI renderer should emit draw indices"); + + const MetaCore::MetaCoreRuntimeUiFrame& frame = renderer.GetLastFrame(); + MetaCoreExpect(frame.Commands.size() == renderer.GetStats().DrawCommandCount, "Runtime UI frame command count should match stats"); + MetaCoreExpect(frame.Vertices.size() == renderer.GetStats().DrawVertexCount, "Runtime UI frame vertex count should match stats"); + MetaCoreExpect(frame.Indices.size() == renderer.GetStats().DrawIndexCount, "Runtime UI frame index count should match stats"); + MetaCoreExpect(!frame.Commands.empty(), "Runtime UI frame should contain at least one command"); + MetaCoreExpect(frame.Commands.front().VertexCount > 0, "Runtime UI command should reference vertices"); + MetaCoreExpect(frame.Commands.front().IndexCount > 0, "Runtime UI command should reference indices"); + MetaCoreExpect(frame.Commands.front().VertexOffset < frame.Vertices.size(), "Runtime UI command vertex offset should be valid"); + MetaCoreExpect(frame.Commands.front().IndexOffset < frame.Indices.size(), "Runtime UI command index offset should be valid"); + MetaCoreExpect(frame.Vertices.front().A > 0, "Runtime UI vertex alpha should be preserved"); + + const MetaCore::MetaCoreRuntimeUiRasterFrame& rasterFrame = renderer.GetLastRasterFrame(); + MetaCoreExpect(rasterFrame.Width == 1280, "Runtime UI raster frame should track width"); + MetaCoreExpect(rasterFrame.Height == 720, "Runtime UI raster frame should track height"); + MetaCoreExpect(rasterFrame.Rgba.size() == static_cast(1280 * 720 * 4), "Runtime UI raster frame should allocate RGBA pixels"); + MetaCoreExpect(renderer.GetStats().RasterPixelCount == static_cast(1280 * 720), "Runtime UI raster pixel count should match dimensions"); + MetaCoreExpect(renderer.GetStats().RasterTouchedPixelCount > 0, "Runtime UI raster frame should contain touched pixels"); + MetaCoreExpect( + std::any_of( + rasterFrame.Rgba.begin(), + rasterFrame.Rgba.end(), + [](std::uint8_t value) { return value != 0; } + ), + "Runtime UI raster frame should contain non-zero pixel data" + ); + + renderer.Shutdown(); + MetaCoreExpect(!renderer.GetStats().Initialized, "Runtime UI renderer should clear initialized state on shutdown"); + MetaCoreExpect(!renderer.GetStats().Loaded, "Runtime UI renderer should clear loaded state on shutdown"); + MetaCoreExpect(renderer.GetLastFrame().Commands.empty(), "Runtime UI renderer should clear draw frame on shutdown"); + MetaCoreExpect(renderer.GetLastRasterFrame().Rgba.empty(), "Runtime UI renderer should clear raster frame on shutdown"); +} + void MetaCoreTestDecoupledSnapshotSync() { // 1. 创建默认场景并收集核心对象 ID MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); @@ -2958,6 +4903,8 @@ void MetaCoreTestP3ProductivityStage() { coreServicesModule->Startup(moduleRegistry); const auto assetDatabase = moduleRegistry.ResolveService(); + const auto assetEditingService = moduleRegistry.ResolveService(); + MetaCoreExpect(assetEditingService != nullptr, "AssetEditingService should be available"); MetaCoreExpect(assetDatabase != nullptr, "应解析到 AssetDatabaseService"); MetaCoreExpect(assetDatabase->HasProject(), "应成功打开项目"); @@ -3026,6 +4973,9 @@ void MetaCoreTestP3ProductivityStage() { // 自动刷新生成 .mcmeta 物理元文件 assetDatabase->Refresh(); const std::filesystem::path materialMetaPath = tempProjectRoot / "Assets" / "test_material.mcmaterial.json.mcmeta"; + const auto materialRecord = assetDatabase->FindAssetByRelativePath(std::filesystem::path("Assets") / "test_material.mcmaterial.json"); + MetaCoreExpect(materialRecord.has_value(), "Material asset should be registered after refresh"); + materialDoc.AssetGuid = materialRecord->Guid; MetaCoreExpect(std::filesystem::exists(materialMetaPath), "Refresh 后应生成材质的 mcmeta 元文件"); // 编辑材质属性并重新保存 @@ -3086,6 +5036,68 @@ void MetaCoreTestP3ProductivityStage() { } meshRenderer.MaterialAssetGuids[0] = materialDoc.AssetGuid; + materialDoc.BaseColor = glm::vec3(0.2F, 0.4F, 0.6F); + materialDoc.Metallic = 0.7F; + materialDoc.Roughness = 0.25F; + materialDoc.DoubleSided = false; + materialDoc.AlphaMode = MetaCore::MetaCoreMaterialAlphaMode::Blend; + materialDoc.AlphaCutoff = 0.35F; + materialDoc.EmissiveColor = glm::vec3(0.05F, 0.1F, 0.15F); + MetaCoreExpect( + assetEditingService->SaveMaterialAsset(materialDoc.AssetGuid, materialDoc), + "AssetEditingService should save standalone material assets" + ); + + const auto serviceLoadedMaterial = assetEditingService->LoadMaterialAsset(materialDoc.AssetGuid); + MetaCoreExpect(serviceLoadedMaterial.has_value(), "AssetEditingService should reload saved standalone material assets"); + MetaCoreExpect(serviceLoadedMaterial->AssetGuid == materialDoc.AssetGuid, "Saved material document guid should match AssetDatabase guid"); + MetaCoreExpectVec3Near(serviceLoadedMaterial->BaseColor, glm::vec3(0.2F, 0.4F, 0.6F), "Saved material base color should round trip"); + MetaCoreExpect(std::abs(serviceLoadedMaterial->Metallic - 0.7F) <= 0.0001F, "Saved material metallic should round trip"); + MetaCoreExpect(std::abs(serviceLoadedMaterial->Roughness - 0.25F) <= 0.0001F, "Saved material roughness should round trip"); + std::uint64_t serviceSavedHash = 0; + { + std::ifstream input(materialMetaPath); + nlohmann::json json; + input >> json; + input.close(); + if (json.contains("source_hash")) { + serviceSavedHash = json["source_hash"].get(); + } + } + MetaCoreExpect( + serviceSavedHash == MetaCore::MetaCoreHashFile(materialPath).value_or(0), + "AssetEditingService material save should update source hash" + ); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + MetaCoreExpect( + assetEditingService->ApplyMaterialAssetPreviewToScene(editorContext, materialDoc.AssetGuid), + "Standalone material asset preview should apply to scene mesh renderers" + ); + MetaCore::MetaCoreGameObject syncedObject = scene.FindGameObject(gameObject.GetId()); + MetaCoreExpect(syncedObject, "Synced mesh object should still exist"); + const auto& syncedMeshRenderer = syncedObject.GetComponent(); + MetaCoreExpect(syncedMeshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "Standalone material slot guid should stay bound"); + MetaCoreExpectVec3Near(syncedMeshRenderer.BaseColor, glm::vec3(0.2F, 0.4F, 0.6F), "Standalone material base color should sync to MeshRenderer"); + MetaCoreExpect(std::abs(syncedMeshRenderer.Metallic - 0.7F) <= 0.0001F, "Standalone material metallic should sync to MeshRenderer"); + MetaCoreExpect(std::abs(syncedMeshRenderer.Roughness - 0.25F) <= 0.0001F, "Standalone material roughness should sync to MeshRenderer"); + MetaCoreExpect(!syncedMeshRenderer.DoubleSided, "Standalone material double-sided flag should sync to MeshRenderer"); + MetaCoreExpect(syncedMeshRenderer.AlphaMode == MetaCore::MetaCoreMeshAlphaMode::Blend, "Standalone material alpha mode should sync to MeshRenderer"); + MetaCoreExpect(std::abs(syncedMeshRenderer.AlphaCutoff - 0.35F) <= 0.0001F, "Standalone material alpha cutoff should sync to MeshRenderer"); + MetaCoreExpectVec3Near(syncedMeshRenderer.EmissiveColor, glm::vec3(0.05F, 0.1F, 0.15F), "Standalone material emissive color should sync to MeshRenderer"); + MetaCoreExpect(meshRenderer.MaterialAssetGuids[0] == materialDoc.AssetGuid, "拖拽绑定后槽位 0 材质 Guid 应正确"); coreServicesModule->Shutdown(moduleRegistry); @@ -3145,6 +5157,14 @@ int main() { MetaCoreTestAssetDatabaseMovePathUpdatesProjectDescriptor(); std::cout << "[RUN] MetaCoreTestComponentRegistryDescriptors..." << std::endl; MetaCoreTestComponentRegistryDescriptors(); + std::cout << "[RUN] MetaCoreTestReflectionFieldEditorDescriptors..." << std::endl; + MetaCoreTestReflectionFieldEditorDescriptors(); + std::cout << "[RUN] MetaCoreTestPlayModeLifecycleAndSceneIsolation..." << std::endl; + MetaCoreTestPlayModeLifecycleAndSceneIsolation(); + std::cout << "[RUN] MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo..." << std::endl; + MetaCoreTestSceneGizmoMultiSelectDeltaAndUndo(); + std::cout << "[RUN] MetaCoreTestGizmoSnapSettings..." << std::endl; + MetaCoreTestGizmoSnapSettings(); std::cout << "[RUN] MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot..." << std::endl; MetaCoreTestSceneRenderSyncBuildsRenderableCameraLightSnapshot(); std::cout << "[RUN] MetaCoreTestJsonSceneSaveCurrentSceneUsesMcsceneJson..." << std::endl; @@ -3169,6 +5189,8 @@ int main() { MetaCoreTestPrefabWorkflow(); std::cout << "[RUN] MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi..." << std::endl; MetaCoreTestCookPipelineCooksJsonScenePrefabMaterialUi(); + std::cout << "[RUN] MetaCoreTestBuildServiceCreatesPlayerPackageLayout..." << std::endl; + MetaCoreTestBuildServiceCreatesPlayerPackageLayout(); std::cout << "[RUN] MetaCoreTestComponentRegistryOperations..." << std::endl; MetaCoreTestComponentRegistryOperations(); std::cout << "[RUN] MetaCoreTestRuntimeDataTypeSerialization..." << std::endl; @@ -3187,6 +5209,8 @@ int main() { MetaCoreTestRuntimeDataDispatcherConstruction(); std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherAppliesUpdates..." << std::endl; MetaCoreTestRuntimeDataDispatcherAppliesUpdates(); + std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherRejectsInvalidUpdateDiagnostics..." << std::endl; + MetaCoreTestRuntimeDataDispatcherRejectsInvalidUpdateDiagnostics(); std::cout << "[RUN] MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates..." << std::endl; MetaCoreTestMockRuntimeDataSourceAdapterEmitsUpdates(); std::cout << "[RUN] MetaCoreTestRuntimeDataDispatcherMarksStaleBindings..." << std::endl; @@ -3201,18 +5225,38 @@ int main() { MetaCoreTestRuntimeDataBinaryDocumentRejectsCorruption(); std::cout << "[RUN] MetaCoreTestEditorContextRuntimeDataConfigSaveLoad..." << std::endl; MetaCoreTestEditorContextRuntimeDataConfigSaveLoad(); + std::cout << "[RUN] MetaCoreTestEditorContextLoadsRuntimeDiagnosticsFromProjectPath..." << std::endl; + MetaCoreTestEditorContextLoadsRuntimeDiagnosticsFromProjectPath(); + std::cout << "[RUN] MetaCoreTestEditorContextRuntimeDataDefaultsFollowRuntimeDirectory..." << std::endl; + MetaCoreTestEditorContextRuntimeDataDefaultsFollowRuntimeDirectory(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsBrokenBinding(); + std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsBindingTypeMismatch..." << std::endl; + MetaCoreTestRuntimeDataConfigValidationRejectsBindingTypeMismatch(); + std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingUiTargetNode..." << std::endl; + MetaCoreTestRuntimeDataConfigValidationRejectsMissingUiTargetNode(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsMissingReplayFilePath(); std::cout << "[RUN] MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort..." << std::endl; MetaCoreTestRuntimeDataConfigValidationRejectsMissingTcpPort(); + std::cout << "[RUN] MetaCoreTestEditorContextRejectsUnsafeRuntimeProjectPaths..." << std::endl; + MetaCoreTestEditorContextRejectsUnsafeRuntimeProjectPaths(); + std::cout << "[RUN] MetaCoreTestEditorContextRejectsUnsafeRuntimeReplayPath..." << std::endl; + MetaCoreTestEditorContextRejectsUnsafeRuntimeReplayPath(); + std::cout << "[RUN] MetaCoreTestEditorContextRejectsMissingRuntimeUiBindingNode..." << std::endl; + MetaCoreTestEditorContextRejectsMissingRuntimeUiBindingNode(); + std::cout << "[RUN] MetaCoreTestEditorContextValidatesRuntimeSceneBindingTargets..." << std::endl; + MetaCoreTestEditorContextValidatesRuntimeSceneBindingTargets(); std::cout << "[RUN] MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave..." << std::endl; MetaCoreTestEditorContextRejectsInvalidRuntimeDataSave(); std::cout << "[RUN] MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream..." << std::endl; MetaCoreTestTcpRuntimeDataSourceAdapterReadsSocketStream(); std::cout << "[RUN] MetaCoreTestUiDocumentSerialization..." << std::endl; MetaCoreTestUiDocumentSerialization(); + std::cout << "[RUN] MetaCoreTestUiDocumentRmlCompilation..." << std::endl; + MetaCoreTestUiDocumentRmlCompilation(); + std::cout << "[RUN] MetaCoreTestRuntimeUiRendererLifecycle..." << std::endl; + MetaCoreTestRuntimeUiRendererLifecycle(); std::cout << "[RUN] MetaCoreTestDecoupledSnapshotSync..." << std::endl; MetaCoreTestDecoupledSnapshotSync(); diff --git a/tools/MetaCoreBuildPackageTool/main.cpp b/tools/MetaCoreBuildPackageTool/main.cpp new file mode 100644 index 0000000..0a101cd --- /dev/null +++ b/tools/MetaCoreBuildPackageTool/main.cpp @@ -0,0 +1,159 @@ +#include "MetaCoreEditor/MetaCoreBuiltinModules.h" +#include "MetaCoreEditor/MetaCoreEditorServices.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +void MetaCorePrintUsage() { + std::cerr + << "Usage: MetaCoreBuildPackageTool [options]\n" + << "Options:\n" + << " --player Path to MetaCorePlayer.exe\n" + << " --output Build output base directory\n" + << " --no-cook Package without cooking first\n" + << " --no-loose-content Do not copy loose Assets/Scenes\n" + << " --no-runtime-config Do not copy Runtime configuration\n" + << " --use-cooked-assets Enable cooked asset loading in packaged runtime config\n"; +} + +[[nodiscard]] bool MetaCoreSetProjectPathEnvironment(const std::filesystem::path& projectRoot) { +#if defined(_WIN32) + return _putenv_s("METACORE_PROJECT_PATH", projectRoot.string().c_str()) == 0; +#else + return setenv("METACORE_PROJECT_PATH", projectRoot.string().c_str(), 1) == 0; +#endif +} + +[[nodiscard]] bool MetaCoreConsumePathOption( + int& index, + int argc, + char* argv[], + std::filesystem::path& outputPath +) { + if (index + 1 >= argc) { + return false; + } + ++index; + outputPath = argv[index]; + return true; +} + +void MetaCorePrintDependencyReport( + const MetaCore::MetaCoreBuildPlayerPackageResult& result, + std::ostream& output +) { + output << "Dependencies: " << result.DependencyReport.size() << '\n'; + for (const MetaCore::MetaCoreBuildDependencyReportEntry& entry : result.DependencyReport) { + if (entry.Status == "Cooked" || entry.Status == "SkippedGeneratedSubAsset") { + continue; + } + output << " [" << entry.Status << "] " << entry.AssetGuid.ToString() + << " reason=" << entry.Reason; + if (entry.ReferencedBy.IsValid()) { + output << " referenced_by=" << entry.ReferencedBy.ToString(); + } + if (!entry.AssetType.empty()) { + output << " type=" << entry.AssetType; + } + if (!entry.AssetPath.empty()) { + output << " path=" << entry.AssetPath.generic_string(); + } + if (!entry.Message.empty()) { + output << " message=" << entry.Message; + } + output << '\n'; + } +} + +} // namespace + +int main(int argc, char* argv[]) { + if (argc < 2) { + MetaCorePrintUsage(); + return 1; + } + if (std::string_view(argv[1]) == "--help" || std::string_view(argv[1]) == "-h") { + MetaCorePrintUsage(); + return 0; + } + + const std::filesystem::path projectRoot = std::filesystem::absolute(argv[1]).lexically_normal(); + if (!std::filesystem::exists(projectRoot / "MetaCore.project.json")) { + std::cerr << "MetaCore project descriptor was not found: " + << (projectRoot / "MetaCore.project.json").string() << '\n'; + return 1; + } + + MetaCore::MetaCoreBuildPlayerPackageRequest request; + for (int index = 2; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--player") { + if (!MetaCoreConsumePathOption(index, argc, argv, request.PlayerExecutablePath)) { + std::cerr << "--player requires a path\n"; + return 1; + } + } else if (argument == "--output") { + if (!MetaCoreConsumePathOption(index, argc, argv, request.OutputDirectory)) { + std::cerr << "--output requires a directory\n"; + return 1; + } + } else if (argument == "--no-cook") { + request.CookBeforePackage = false; + } else if (argument == "--no-loose-content") { + request.CopyLooseProjectContent = false; + } else if (argument == "--no-runtime-config") { + request.CopyRuntimeConfig = false; + } else if (argument == "--use-cooked-assets") { + request.UseCookedAssetsInPackage = true; + } else if (argument == "--help" || argument == "-h") { + MetaCorePrintUsage(); + return 0; + } else { + std::cerr << "Unknown option: " << argument << '\n'; + MetaCorePrintUsage(); + return 1; + } + } + + if (!MetaCoreSetProjectPathEnvironment(projectRoot)) { + std::cerr << "Failed to set METACORE_PROJECT_PATH\n"; + return 1; + } + + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + const std::unique_ptr coreServicesModule = + MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + const auto buildService = moduleRegistry.ResolveService(); + if (buildService == nullptr) { + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + std::cerr << "BuildService is not available\n"; + return 1; + } + + const MetaCore::MetaCoreBuildPlayerPackageResult result = + buildService->BuildPlayerPackage(request); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + + if (!result.Success) { + std::cerr << "Build package failed: " << result.Error << '\n'; + MetaCorePrintDependencyReport(result, std::cerr); + return 1; + } + + std::cout << "Build package created: " << result.OutputRoot.string() << '\n' + << "Copied files: " << result.CopiedFiles.size() << '\n' + << "Cooked assets: " << result.CookedAssets.size() << '\n'; + MetaCorePrintDependencyReport(result, std::cout); + return 0; +} diff --git a/tools/MetaCoreHeaderTool/main.cpp b/tools/MetaCoreHeaderTool/main.cpp index a56f990..4c9e172 100644 --- a/tools/MetaCoreHeaderTool/main.cpp +++ b/tools/MetaCoreHeaderTool/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,6 +9,7 @@ #include #include #include +#include #include namespace { @@ -20,12 +22,18 @@ enum class MetaCoreReflectedKind { struct MetaCoreReflectedField { std::string Name{}; + std::string PropertySpec{}; +}; + +struct MetaCoreReflectedEnumValue { + std::string Name{}; }; struct MetaCoreReflectedType { MetaCoreReflectedKind Kind = MetaCoreReflectedKind::Struct; std::string Name{}; std::vector Fields{}; + std::vector EnumValues{}; }; [[nodiscard]] std::string MetaCoreTrim(std::string value) { @@ -52,6 +60,210 @@ struct MetaCoreReflectedType { return full.substr(markerIndex + marker.size()); } +[[nodiscard]] std::string MetaCoreExtractMacroArgument(std::string_view trimmed, std::string_view macroName) { + const std::size_t prefixLength = macroName.size(); + if (trimmed.size() <= prefixLength || trimmed[prefixLength] != '(') { + return {}; + } + + const std::size_t closeIndex = trimmed.rfind(')'); + if (closeIndex == std::string_view::npos || closeIndex <= prefixLength) { + return {}; + } + + return MetaCoreTrim(std::string(trimmed.substr(prefixLength + 1, closeIndex - prefixLength - 1))); +} + +[[nodiscard]] std::string MetaCoreEscapeCppString(std::string_view value) { + std::string escaped; + escaped.reserve(value.size()); + for (const char character : value) { + switch (character) { + case '\\': escaped += "\\\\"; break; + case '"': escaped += "\\\""; break; + case '\n': escaped += "\\n"; break; + case '\r': escaped += "\\r"; break; + case '\t': escaped += "\\t"; break; + default: escaped.push_back(character); break; + } + } + return escaped; +} + +[[nodiscard]] std::string MetaCoreToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; +} + +[[nodiscard]] std::string MetaCoreStripQuotes(std::string value) { + value = MetaCoreTrim(std::move(value)); + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') { + return value.substr(1, value.size() - 2); + } + return value; +} + +[[nodiscard]] bool MetaCoreIsTruthyMetadataValue(std::string value) { + value = MetaCoreToLower(MetaCoreTrim(std::move(value))); + return value.empty() || value == "true" || value == "1" || value == "yes"; +} + +[[nodiscard]] std::size_t MetaCoreFindUnquotedEquals(std::string_view value) { + bool inString = false; + bool escaping = false; + for (std::size_t index = 0; index < value.size(); ++index) { + const char character = value[index]; + if (escaping) { + escaping = false; + continue; + } + if (character == '\\') { + escaping = inString; + continue; + } + if (character == '"') { + inString = !inString; + continue; + } + if (!inString && character == '=') { + return index; + } + } + return std::string_view::npos; +} + +[[nodiscard]] std::vector MetaCoreSplitPropertySpec(std::string_view spec) { + std::vector tokens; + std::string token; + bool inString = false; + bool escaping = false; + int nestedDepth = 0; + + for (const char character : spec) { + if (escaping) { + token.push_back(character); + escaping = false; + continue; + } + if (character == '\\') { + token.push_back(character); + escaping = inString; + continue; + } + if (character == '"') { + token.push_back(character); + inString = !inString; + continue; + } + if (!inString && (character == '(' || character == '[' || character == '{')) { + ++nestedDepth; + } else if (!inString && (character == ')' || character == ']' || character == '}')) { + --nestedDepth; + } + if (!inString && nestedDepth == 0 && character == ',') { + tokens.push_back(MetaCoreTrim(token)); + token.clear(); + continue; + } + token.push_back(character); + } + + if (!MetaCoreTrim(token).empty()) { + tokens.push_back(MetaCoreTrim(token)); + } + return tokens; +} + +[[nodiscard]] std::unordered_map MetaCoreParsePropertySpec(std::string_view spec) { + std::unordered_map metadata; + for (const std::string& token : MetaCoreSplitPropertySpec(spec)) { + const std::size_t equalsIndex = MetaCoreFindUnquotedEquals(token); + if (equalsIndex == std::string_view::npos) { + metadata.emplace(MetaCoreTrim(token), "true"); + continue; + } + + metadata.insert_or_assign( + MetaCoreTrim(token.substr(0, equalsIndex)), + MetaCoreTrim(token.substr(equalsIndex + 1)) + ); + } + return metadata; +} + +[[nodiscard]] std::optional MetaCoreParseEnumValue(std::string value) { + const std::size_t commentIndex = value.find("//"); + if (commentIndex != std::string::npos) { + value = value.substr(0, commentIndex); + } + value = MetaCoreTrim(std::move(value)); + if (value.empty() || value == "{" || value == "};") { + return std::nullopt; + } + if (!value.empty() && value.back() == ',') { + value.pop_back(); + } + const std::size_t equalsIndex = MetaCoreFindUnquotedEquals(value); + if (equalsIndex != std::string::npos) { + value = value.substr(0, equalsIndex); + } + value = MetaCoreTrim(std::move(value)); + if (value.empty()) { + return std::nullopt; + } + if (!std::regex_match(value, std::regex(R"([A-Za-z_]\w*)"))) { + return std::nullopt; + } + return MetaCoreReflectedEnumValue{value}; +} + +void MetaCoreEmitStringMetadataAssignment( + std::ostringstream& output, + const std::unordered_map& metadata, + std::string_view metadataVariable, + std::string_view propertyName, + std::string_view fieldName +) { + const auto iterator = metadata.find(std::string(propertyName)); + if (iterator == metadata.end()) { + return; + } + output << " " << metadataVariable << "." << fieldName << " = \"" + << MetaCoreEscapeCppString(MetaCoreStripQuotes(iterator->second)) << "\";\n"; +} + +void MetaCoreEmitOptionalDoubleMetadataAssignment( + std::ostringstream& output, + const std::unordered_map& metadata, + std::string_view metadataVariable, + std::string_view propertyName, + std::string_view fieldName +) { + const auto iterator = metadata.find(std::string(propertyName)); + if (iterator == metadata.end()) { + return; + } + output << " " << metadataVariable << "." << fieldName << " = " + << MetaCoreStripQuotes(iterator->second) << ";\n"; +} + +void MetaCoreEmitBoolMetadataAssignment( + std::ostringstream& output, + const std::unordered_map& metadata, + std::string_view metadataVariable, + std::string_view propertyName, + std::string_view fieldName +) { + const auto iterator = metadata.find(std::string(propertyName)); + if (iterator == metadata.end()) { + return; + } + output << " " << metadataVariable << "." << fieldName << " = " + << (MetaCoreIsTruthyMetadataValue(iterator->second) ? "true" : "false") << ";\n"; +} + [[nodiscard]] std::vector MetaCoreParseHeader(const std::filesystem::path& path) { std::ifstream input(path); if (!input.is_open()) { @@ -65,6 +277,7 @@ struct MetaCoreReflectedType { std::optional pendingKind; MetaCoreReflectedType* currentType = nullptr; bool expectingField = false; + std::string pendingPropertySpec; std::string line; while (std::getline(input, line)) { @@ -87,6 +300,7 @@ struct MetaCoreReflectedType { } if (trimmed.starts_with("MC_PROPERTY")) { expectingField = true; + pendingPropertySpec = MetaCoreExtractMacroArgument(trimmed, "MC_PROPERTY"); continue; } if (trimmed.starts_with("MC_GENERATED_BODY")) { @@ -99,6 +313,7 @@ struct MetaCoreReflectedType { reflectedTypes.push_back(MetaCoreReflectedType{ pendingKind.value(), match[2].str(), + {}, {} }); currentType = &reflectedTypes.back(); @@ -114,6 +329,13 @@ struct MetaCoreReflectedType { continue; } + if (currentType != nullptr && currentType->Kind == MetaCoreReflectedKind::Enum) { + if (auto enumValue = MetaCoreParseEnumValue(trimmed); enumValue.has_value()) { + currentType->EnumValues.push_back(std::move(*enumValue)); + } + continue; + } + if (currentType == nullptr || currentType->Kind == MetaCoreReflectedKind::Enum || !expectingField) { continue; } @@ -124,9 +346,11 @@ struct MetaCoreReflectedType { } currentType->Fields.push_back(MetaCoreReflectedField{ - match[2].str() + match[2].str(), + pendingPropertySpec }); expectingField = false; + pendingPropertySpec.clear(); } return reflectedTypes; @@ -153,13 +377,42 @@ struct MetaCoreReflectedType { for (const auto& reflectedType : reflectedTypes) { if (reflectedType.Kind == MetaCoreReflectedKind::Enum) { - output << " MetaCoreRegisterGeneratedEnum<" << reflectedType.Name << ">(registry, \"" << reflectedType.Name << "\");\n"; + output << " auto " << reflectedType.Name << "Builder = MetaCoreRegisterGeneratedEnum<" + << reflectedType.Name << ">(registry, \"" << reflectedType.Name << "\");\n"; + for (const auto& enumValue : reflectedType.EnumValues) { + output << " " << reflectedType.Name << "Builder.Value(\"" + << MetaCoreEscapeCppString(enumValue.Name) << "\", " + << reflectedType.Name << "::" << enumValue.Name << ");\n"; + } continue; } output << " auto " << reflectedType.Name << "Builder = MetaCoreRegisterGeneratedStruct<" << reflectedType.Name << ">(registry, \"" << reflectedType.Name << "\");\n"; for (const auto& field : reflectedType.Fields) { + if (!field.PropertySpec.empty()) { + const std::string metadataVariable = + reflectedType.Name + field.Name + "EditorMetadata"; + const auto metadata = MetaCoreParsePropertySpec(field.PropertySpec); + output << " MetaCoreFieldEditorMetadata " << metadataVariable << "{};\n"; + output << " " << metadataVariable << ".RawSpec = \"" + << MetaCoreEscapeCppString(field.PropertySpec) << "\";\n"; + MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "DisplayName", "DisplayName"); + MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Group", "Group"); + MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Tooltip", "Tooltip"); + MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "ResourceType", "ResourceType"); + MetaCoreEmitStringMetadataAssignment(output, metadata, metadataVariable, "Resource", "ResourceType"); + MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Min", "Min"); + MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Max", "Max"); + MetaCoreEmitOptionalDoubleMetadataAssignment(output, metadata, metadataVariable, "Step", "Step"); + MetaCoreEmitBoolMetadataAssignment(output, metadata, metadataVariable, "ReadOnly", "ReadOnly"); + MetaCoreEmitBoolMetadataAssignment(output, metadata, metadataVariable, "Hidden", "Hidden"); + MetaCoreEmitBoolMetadataAssignment(output, metadata, metadataVariable, "ResourceReference", "ResourceReference"); + output << " " << reflectedType.Name << "Builder.Field<&" << reflectedType.Name << "::" + << field.Name << ">(\"" << field.Name << "\", " << metadataVariable << ");\n"; + continue; + } + output << " " << reflectedType.Name << "Builder.Field<&" << reflectedType.Name << "::" << field.Name << ">(\"" << field.Name << "\");\n"; } diff --git a/tools/MetaCoreRuntimeConfigTool/main.cpp b/tools/MetaCoreRuntimeConfigTool/main.cpp index 13a2870..1efc9b9 100644 --- a/tools/MetaCoreRuntimeConfigTool/main.cpp +++ b/tools/MetaCoreRuntimeConfigTool/main.cpp @@ -1,22 +1,148 @@ #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" +#include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreRuntimeData/MetaCoreRuntimeDataProject.h" #include "MetaCoreScene/MetaCoreScene.h" -#include "MetaCoreScene/MetaCoreScenePackage.h" +#include "MetaCoreScene/MetaCoreSceneSerializer.h" +#include #include #include #include +#include namespace { [[nodiscard]] MetaCore::MetaCoreSceneDocument MetaCoreBuildPilotSceneDocument() { MetaCore::MetaCoreSceneDocument document; document.Name = "Main"; - const MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + + MetaCore::MetaCoreScene scene; + MetaCore::MetaCoreGameObject mainCamera = scene.CreateGameObjectWithId(1, "Main Camera"); + mainCamera.AddComponent().IsPrimary = true; + mainCamera.GetComponent().Position = glm::vec3(0.0F, 2.4F, 7.0F); + mainCamera.GetComponent().RotationEulerDegrees = glm::vec3(-16.0F, 0.0F, 0.0F); + + MetaCore::MetaCoreGameObject keyLight = scene.CreateGameObjectWithId(2, "Directional Light"); + keyLight.AddComponent(); + keyLight.GetComponent().RotationEulerDegrees = glm::vec3(-45.0F, 30.0F, 0.0F); + + MetaCore::MetaCoreGameObject cube = scene.CreateGameObjectWithId(3, "Runtime Cube"); + cube.AddComponent().BaseColor = glm::vec3(0.4F, 0.6F, 0.9F); + cube.GetComponent().Position = glm::vec3(0.0F, 0.5F, 0.0F); + + MetaCore::MetaCoreGameObject valve = scene.CreateGameObjectWithId(4, "Runtime Valve"); + valve.AddComponent().BaseColor = glm::vec3(0.9F, 0.55F, 0.25F); + valve.GetComponent().Position = glm::vec3(-2.0F, 0.5F, 0.0F); + valve.GetComponent().Scale = glm::vec3(0.65F, 0.65F, 0.65F); + + MetaCore::MetaCoreGameObject tank = scene.CreateGameObjectWithId(5, "Runtime Tank"); + tank.AddComponent().BaseColor = glm::vec3(0.35F, 0.65F, 0.90F); + tank.GetComponent().Position = glm::vec3(2.0F, 0.5F, 0.0F); + tank.GetComponent().Scale = glm::vec3(1.25F, 1.25F, 1.25F); + + MetaCore::MetaCoreGameObject alarm = scene.CreateGameObjectWithId(6, "Runtime Alarm Light"); + alarm.AddComponent().Intensity = 0.0F; + alarm.GetComponent().Position = glm::vec3(0.0F, 3.0F, 0.0F); + + MetaCore::MetaCoreIdGenerator::EnsureAbove(6); document.GameObjects = scene.CaptureSnapshot().GameObjects; return document; } +[[nodiscard]] MetaCore::MetaCoreUiDocument MetaCoreBuildPilotUiDocument() { + MetaCore::MetaCoreUiDocument document; + document.Name = "RuntimeDataHud"; + document.ReferenceWidth = 1280; + document.ReferenceHeight = 720; + document.RootNodeIds = {"runtime.root"}; + + MetaCore::MetaCoreUiNodeDocument root; + root.Id = "runtime.root"; + root.Name = "Runtime Root"; + root.Type = MetaCore::MetaCoreUiNodeType::Panel; + root.Children = {"runtime.status"}; + root.RectTransform.AnchorMin = glm::vec3(0.0F, 0.0F, 0.0F); + root.RectTransform.AnchorMax = glm::vec3(1.0F, 1.0F, 0.0F); + root.RectTransform.Size = glm::vec3(0.0F, 0.0F, 0.0F); + root.Style.BackgroundColor = glm::vec3(0.0F, 0.0F, 0.0F); + + MetaCore::MetaCoreUiNodeDocument status; + status.Id = "runtime.status"; + status.Name = "Runtime Status"; + status.Type = MetaCore::MetaCoreUiNodeType::Text; + status.ParentId = "runtime.root"; + status.Text = "RuntimeData waiting"; + status.RectTransform.Position = glm::vec3(24.0F, 24.0F, 0.0F); + status.RectTransform.Size = glm::vec3(560.0F, 40.0F, 0.0F); + status.Style.FontSize = 18.0F; + status.Style.TextColor = glm::vec3(0.88F, 0.94F, 1.0F); + + document.Nodes = {root, status}; + return document; +} + +[[nodiscard]] const MetaCore::MetaCoreGameObjectData* MetaCoreFindSceneObject( + const MetaCore::MetaCoreSceneDocument& sceneDocument, + MetaCore::MetaCoreId objectId +) { + const auto iterator = std::find_if( + sceneDocument.GameObjects.begin(), + sceneDocument.GameObjects.end(), + [objectId](const MetaCore::MetaCoreGameObjectData& objectData) { + return objectData.Id == objectId; + } + ); + return iterator == sceneDocument.GameObjects.end() ? nullptr : &(*iterator); +} + +[[nodiscard]] bool MetaCoreValidatePilotRuntimeDocuments( + const MetaCore::MetaCoreSceneDocument& sceneDocument, + const MetaCore::MetaCoreUiDocument& uiDocument, + const MetaCore::MetaCoreRuntimeBindingsDocument& bindings, + std::string& error +) { + for (const MetaCore::MetaCoreSceneBindingDefinition& binding : bindings.Bindings) { + const MetaCore::MetaCoreGameObjectData* targetObject = + MetaCoreFindSceneObject(sceneDocument, binding.TargetObjectId); + if (targetObject == nullptr) { + error = "Scene binding target object does not exist: " + binding.BindingId; + return false; + } + + const bool targetHasRequiredComponent = + binding.Target == MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition || + ((binding.Target == MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible || + binding.Target == MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor) && + targetObject->MeshRenderer.has_value()) || + ((binding.Target == MetaCore::MetaCoreRuntimeBindingTarget::LightIntensity || + binding.Target == MetaCore::MetaCoreRuntimeBindingTarget::LightColor) && + targetObject->Light.has_value()); + if (!targetHasRequiredComponent) { + error = "Scene binding target object is missing the required component: " + binding.BindingId; + return false; + } + } + + for (const MetaCore::MetaCoreUiBindingDefinition& binding : bindings.UiBindings) { + if (binding.Target != MetaCore::MetaCoreRuntimeUiBindingTarget::Text) { + continue; + } + + const auto nodeIterator = std::find_if( + uiDocument.Nodes.begin(), + uiDocument.Nodes.end(), + [&](const MetaCore::MetaCoreUiNodeDocument& node) { + return node.Id == binding.TargetNodeId; + } + ); + if (nodeIterator == uiDocument.Nodes.end() || nodeIterator->Type != MetaCore::MetaCoreUiNodeType::Text) { + error = "UI binding target Text node does not exist: " + binding.BindingId; + return false; + } + } + return true; +} + } // namespace int main(int argc, char* argv[]) { @@ -28,8 +154,17 @@ int main(int argc, char* argv[]) { const bool generateTcpConfig = argc >= 3 && std::string_view(argv[2]) == "--tcp"; MetaCore::MetaCoreTypeRegistry registry; + MetaCore::MetaCoreRegisterFoundationGeneratedTypes(registry); + MetaCore::MetaCoreRegisterSceneGeneratedTypes(registry); MetaCore::MetaCoreRegisterRuntimeDataGeneratedTypes(registry); + const std::filesystem::path runtimeDirectory = std::filesystem::absolute(argv[1]).lexically_normal(); + const std::filesystem::path projectRoot = runtimeDirectory.parent_path(); + const std::filesystem::path runtimeDirectoryRelative = + MetaCore::MetaCoreBuildRuntimeDirectoryRelativePath(projectRoot, runtimeDirectory); + const std::filesystem::path sceneRelativePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; + const std::filesystem::path uiRelativePath = std::filesystem::path("Assets") / "UI" / "Hud.mcui.json"; + MetaCore::MetaCoreRuntimeDataSourcesDocument sources; sources.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{ generateTcpConfig ? "tcp-source" : "replay-source", @@ -42,7 +177,7 @@ int main(int argc, char* argv[]) { } : std::vector{ // 动态计算相对于项目根目录的播放流文件路径,替代原先硬编码的 TestProject - MetaCore::MetaCoreDataSourceSetting{"file_path", (std::filesystem::path(argv[1]).filename() / "RuntimeReplay.mcstream").generic_string()} + MetaCore::MetaCoreDataSourceSetting{"file_path", (runtimeDirectoryRelative / "RuntimeReplay.mcstream").generic_string()} }, true, 1000 @@ -83,6 +218,12 @@ int main(int argc, char* argv[]) { "alarm.intensity", MetaCore::MetaCoreRuntimeValueType::Double }); + sources.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{ + "runtime.status", + generateTcpConfig ? "tcp-source" : "replay-source", + "runtime.status", + MetaCore::MetaCoreRuntimeValueType::String + }); MetaCore::MetaCoreRuntimeBindingsDocument bindings; bindings.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{ @@ -127,10 +268,24 @@ int main(int argc, char* argv[]) { MetaCore::MetaCoreRuntimeBindingTarget::LightIntensity, MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue }); + bindings.UiBindings.push_back(MetaCore::MetaCoreUiBindingDefinition{ + "binding.runtime.status", + "runtime.status", + "runtime.status", + MetaCore::MetaCoreRuntimeUiBindingTarget::Text, + MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue + }); - const std::filesystem::path runtimeDirectory = argv[1]; - const std::filesystem::path projectRoot = runtimeDirectory.parent_path(); - const std::filesystem::path scenePath = projectRoot / "Scenes" / "Main.mcscene"; + const MetaCore::MetaCoreSceneDocument sceneDocument = MetaCoreBuildPilotSceneDocument(); + const MetaCore::MetaCoreUiDocument uiDocument = MetaCoreBuildPilotUiDocument(); + std::string validationError; + if (!MetaCoreValidatePilotRuntimeDocuments(sceneDocument, uiDocument, bindings, validationError)) { + std::cerr << "Generated runtime config is invalid: " << validationError << '\n'; + return 1; + } + + const std::filesystem::path scenePath = projectRoot / sceneRelativePath; + const std::filesystem::path uiPath = projectRoot / uiRelativePath; std::filesystem::create_directories(runtimeDirectory); if (!MetaCore::MetaCoreWriteRuntimeDataSourcesDocument(runtimeDirectory / "DataSources.mcruntime", sources, registry)) { std::cerr << "Failed to write DataSources.mcruntime\n"; @@ -140,18 +295,35 @@ int main(int argc, char* argv[]) { std::cerr << "Failed to write Bindings.mcruntime\n"; return 1; } - MetaCore::MetaCoreRuntimeProjectDocument runtimeProjectDocument; - runtimeProjectDocument.StartupScenePath = std::filesystem::path("Scenes") / "Main.mcscene"; - runtimeProjectDocument.DataSourcesPath = std::filesystem::path("Runtime") / "DataSources.mcruntime"; - runtimeProjectDocument.BindingsPath = std::filesystem::path("Runtime") / "Bindings.mcruntime"; - runtimeProjectDocument.DiagnosticsPath = std::filesystem::path("Runtime") / "Diagnostics.mcruntimestate"; + MetaCore::MetaCoreRuntimeProjectDocument runtimeProjectDocument = + MetaCore::MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative); + runtimeProjectDocument.StartupScenePath = sceneRelativePath; + runtimeProjectDocument.StartupUiPath = uiRelativePath; if (!MetaCore::MetaCoreWriteRuntimeProjectDocument(runtimeDirectory / "ProjectRuntime.mcruntimecfg", runtimeProjectDocument, registry)) { std::cerr << "Failed to write ProjectRuntime.mcruntimecfg\n"; return 1; } + + MetaCore::MetaCoreProjectFileDocument projectDocument; + projectDocument.Name = generateTcpConfig ? "MetaCoreTcpRuntimeDataPilot" : "MetaCoreRuntimeDataPilot"; + projectDocument.RuntimeDirectory = runtimeDirectoryRelative; + projectDocument.UiDirectory = std::filesystem::path("Assets") / "UI"; + projectDocument.BuildDirectory = std::filesystem::path("Build"); + projectDocument.StartupScenePath = sceneRelativePath; + projectDocument.ScenePaths = {sceneRelativePath}; + if (!MetaCore::MetaCoreWriteProjectFile(MetaCore::MetaCoreGetProjectFilePath(projectRoot), projectDocument)) { + std::cerr << "Failed to write MetaCore.project.json\n"; + return 1; + } + std::filesystem::create_directories(scenePath.parent_path()); - if (!MetaCore::MetaCoreWriteScenePackage(scenePath, MetaCoreBuildPilotSceneDocument())) { - std::cerr << "Failed to write Main.mcscene\n"; + if (!MetaCore::MetaCoreSceneSerializer::SaveSceneToJson(scenePath, sceneDocument, registry)) { + std::cerr << "Failed to write Main.mcscene.json\n"; + return 1; + } + std::filesystem::create_directories(uiPath.parent_path()); + if (!MetaCore::MetaCoreSceneSerializer::SaveUiToJson(uiPath, uiDocument, registry)) { + std::cerr << "Failed to write Hud.mcui.json\n"; return 1; } @@ -169,20 +341,25 @@ int main(int argc, char* argv[]) { << "0.00 valve.visible bool true\n" << "0.00 tank.base_color vec3 0.35 0.65 0.90\n" << "0.00 alarm.intensity double 0.0\n" + << "0.00 runtime.status string RuntimeData replay started\n" << "0.50 cube.position vec3 1.5 0.5 0.0\n" << "0.50 cube.base_color vec3 0.8 0.6 0.5\n" << "0.50 tank.base_color vec3 0.20 0.80 0.25\n" << "0.50 alarm.intensity double 2.0\n" + << "0.50 runtime.status string Replay frame 0.50s active\n" << "1.00 cube.visible bool false\n" << "1.00 valve.visible bool false\n" + << "1.00 runtime.status string Replay toggled visibility\n" << "1.50 cube.position vec3 -1.5 0.5 0.0\n" << "1.50 cube.visible bool true\n" << "1.50 valve.visible bool true\n" - << "1.50 alarm.intensity double 0.0\n"; + << "1.50 alarm.intensity double 0.0\n" + << "1.50 runtime.status string Replay restored scene\n"; } std::cout << "Runtime config generated at " << runtimeDirectory.string() << " scene=" << scenePath.string() + << " ui=" << uiPath.string() << " mode=" << (generateTcpConfig ? "tcp" : "file_replay") << '\n'; return 0; } diff --git a/tools/MetaCoreTcpSenderTool/main.cpp b/tools/MetaCoreTcpSenderTool/main.cpp index 2c60714..e95b32a 100644 --- a/tools/MetaCoreTcpSenderTool/main.cpp +++ b/tools/MetaCoreTcpSenderTool/main.cpp @@ -104,7 +104,9 @@ int main(int argc, char* argv[]) { "cube.base_color vec3 " + std::to_string(r) + " " + std::to_string(g) + " " + std::to_string(b) + "\n" + "valve.visible bool " + std::string(valveVisible ? "true" : "false") + "\n" + "tank.base_color vec3 " + std::to_string(tankR) + " " + std::to_string(tankG) + " " + std::to_string(tankB) + "\n" + - "alarm.intensity double " + std::to_string(alarmIntensity) + "\n"; + "alarm.intensity double " + std::to_string(alarmIntensity) + "\n" + + "runtime.status string TCP frame " + std::to_string(frame) + + (valveVisible ? " valve online\n" : " valve hidden\n"); const int sent = send(clientSocket, payload.c_str(), static_cast(payload.size()), 0); if (sent == SOCKET_ERROR) { diff --git a/vcpkg.json b/vcpkg.json index 3f2c6fd..280c13d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -12,6 +12,18 @@ "win32-binding", "opengl3-binding" ] + }, + { + "name": "rmlui", + "default-features": false + }, + { + "name": "qtbase", + "default-features": false, + "features": [ + "gui", + "widgets" + ] } ] }