MetaCore/Apps/MetaCorePlayer/main.cpp

1104 lines
46 KiB
C++

#include "MetaCorePlatform/MetaCoreWindow.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include "MetaCoreFoundation/MetaCorePackage.h"
#include "MetaCoreFoundation/MetaCoreProject.h"
#include "MetaCoreRender/MetaCorePlayerRenderer.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/MetaCoreRuntimeLifecycle.h"
#include "MetaCoreScene/MetaCoreScenePackage.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreUiRmlCompiler.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <memory>
#include <optional>
#include <span>
#include <sstream>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace {
MetaCore::MetaCoreSceneView MetaCoreBuildPlayerSceneView() {
MetaCore::MetaCoreSceneView sceneView;
sceneView.CameraPosition = {0.0F, 2.5F, 6.5F};
sceneView.CameraTarget = {0.0F, 0.7F, 0.0F};
sceneView.CameraUp = {0.0F, 1.0F, 0.0F};
sceneView.VerticalFieldOfViewDegrees = 60.0F;
return sceneView;
}
struct MetaCoreRuntimeDataSourceInstance {
MetaCore::MetaCoreDataSourceDefinition Definition{};
std::unique_ptr<MetaCore::MetaCoreIRuntimeDataSourceAdapter> Adapter{};
MetaCore::MetaCoreRuntimeDataSourceState LastReportedState =
MetaCore::MetaCoreRuntimeDataSourceState::Disconnected;
};
[[nodiscard]] std::optional<std::string> 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<MetaCore::MetaCoreCookManifestDocument> 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<std::size_t>(input.tellg());
input.seekg(0, std::ios::beg);
std::vector<std::byte> buffer(size);
if (size > 0 && !input.read(reinterpret_cast<char*>(buffer.data()), static_cast<std::streamsize>(size))) {
return std::nullopt;
}
MetaCore::MetaCoreCookManifestDocument document;
if (!MetaCore::MetaCoreDeserializeFromBytes(
std::span<const std::byte>(buffer.data(), buffer.size()),
document,
registry)) {
return std::nullopt;
}
return document;
}
[[nodiscard]] std::optional<MetaCore::MetaCoreSceneDocument> 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;
}
[[nodiscard]] const MetaCore::MetaCoreCookManifestEntry* MetaCoreFindCookedManifestEntry(
const MetaCore::MetaCoreCookManifestDocument& manifest,
const MetaCore::MetaCoreAssetGuid& assetGuid
) {
if (!assetGuid.IsValid()) {
return nullptr;
}
const auto iterator = std::find_if(
manifest.Entries.begin(),
manifest.Entries.end(),
[&](const MetaCore::MetaCoreCookManifestEntry& entry) {
return entry.AssetGuid == assetGuid;
}
);
return iterator == manifest.Entries.end() ? nullptr : &*iterator;
}
[[nodiscard]] bool MetaCoreIsCookedRuntimeGltfModel(const std::filesystem::path& relativeSourcePath) {
std::string extension = relativeSourcePath.extension().generic_string();
std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return extension == ".glb" || extension == ".gltf";
}
void MetaCoreRewriteSceneGltfModelPathsToCooked(
MetaCore::MetaCoreSceneDocument& sceneDocument,
const MetaCore::MetaCoreCookManifestDocument& manifest
) {
for (MetaCore::MetaCoreGameObjectData& gameObject : sceneDocument.GameObjects) {
if (!gameObject.MeshRenderer.has_value()) {
continue;
}
if (gameObject.MeshRenderer->SourceModelPath.empty() &&
!gameObject.MeshRenderer->SourceModelAssetGuid.IsValid()) {
continue;
}
const std::filesystem::path sourceModelPath(gameObject.MeshRenderer->SourceModelPath);
const MetaCore::MetaCoreCookManifestEntry* entry = nullptr;
if (!sourceModelPath.empty() && MetaCoreIsCookedRuntimeGltfModel(sourceModelPath)) {
entry = MetaCoreFindCookedManifestEntry(manifest, sourceModelPath);
}
if (entry == nullptr && gameObject.MeshRenderer->SourceModelAssetGuid.IsValid()) {
entry = MetaCoreFindCookedManifestEntry(manifest, gameObject.MeshRenderer->SourceModelAssetGuid);
}
if (entry == nullptr || entry->CookedPath.empty()) {
continue;
}
if (!MetaCoreIsCookedRuntimeGltfModel(entry->SourcePackagePath)) {
continue;
}
const std::string cookedModelPath = entry->CookedPath.generic_string();
gameObject.MeshRenderer->SourceModelPath = cookedModelPath;
gameObject.MeshRenderer->SourceModelAssetGuid = {};
if (gameObject.ModelRootTag.has_value()) {
gameObject.ModelRootTag->SourceModelPath = cookedModelPath;
}
}
}
template <typename T>
[[nodiscard]] std::optional<T> 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 <typename T>
[[nodiscard]] std::optional<T> 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<T>(*cookedPackage, registry, expectedTypeName);
}
[[nodiscard]] MetaCore::MetaCoreRuntimeDataSourcesDocument MetaCoreBuildDefaultRuntimeDataSourcesDocument() {
MetaCore::MetaCoreRuntimeDataSourcesDocument document;
document.Sources.push_back(MetaCore::MetaCoreDataSourceDefinition{
"replay-source",
"file_replay",
"Replay Source",
{MetaCore::MetaCoreDataSourceSetting{"file_path", "TestProject/Runtime/RuntimeReplay.mcstream"}},
true,
1000
});
document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{
"cube.position",
"replay-source",
"cube.position",
MetaCore::MetaCoreRuntimeValueType::Vec3
});
document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{
"cube.visible",
"replay-source",
"cube.visible",
MetaCore::MetaCoreRuntimeValueType::Bool
});
document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{
"cube.base_color",
"replay-source",
"cube.base_color",
MetaCore::MetaCoreRuntimeValueType::Vec3
});
document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{
"valve.visible",
"replay-source",
"valve.visible",
MetaCore::MetaCoreRuntimeValueType::Bool
});
document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{
"tank.base_color",
"replay-source",
"tank.base_color",
MetaCore::MetaCoreRuntimeValueType::Vec3
});
document.DataPoints.push_back(MetaCore::MetaCoreDataPointDefinition{
"alarm.intensity",
"replay-source",
"alarm.intensity",
MetaCore::MetaCoreRuntimeValueType::Double
});
return document;
}
[[nodiscard]] MetaCore::MetaCoreRuntimeBindingsDocument MetaCoreBuildDefaultRuntimeBindingsDocument() {
MetaCore::MetaCoreRuntimeBindingsDocument document;
document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{
"binding.cube.position",
"cube.position",
3,
MetaCore::MetaCoreRuntimeBindingTarget::TransformPosition,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
});
document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{
"binding.cube.visible",
"cube.visible",
3,
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
});
document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{
"binding.cube.base_color",
"cube.base_color",
3,
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
});
document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{
"binding.valve.visible",
"valve.visible",
4,
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererVisible,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
});
document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{
"binding.tank.base_color",
"tank.base_color",
5,
MetaCore::MetaCoreRuntimeBindingTarget::MeshRendererBaseColor,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
});
document.Bindings.push_back(MetaCore::MetaCoreSceneBindingDefinition{
"binding.alarm.intensity",
"alarm.intensity",
6,
MetaCore::MetaCoreRuntimeBindingTarget::LightIntensity,
MetaCore::MetaCoreRuntimeMissingDataPolicy::KeepLastValue
});
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<std::size_t>(connectedSources))
+ " source_issues=" + std::to_string(static_cast<std::size_t>(sourceIssues))
+ " bindings=" + std::to_string(diagnostics.BindingStatuses.size())
+ " binding_issues=" + std::to_string(static_cast<std::size_t>(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<MetaCore::MetaCoreDataPointDefinition>& 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<MetaCore::MetaCoreRuntimeBindingStatus>& 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<MetaCore::MetaCoreRuntimeBindingStatus>& 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<MetaCore::MetaCoreRuntimeBindingStatus> MetaCoreBuildRuntimeUiBindingStatuses(
const std::vector<MetaCore::MetaCoreUiBindingDefinition>& uiBindings
) {
std::vector<MetaCore::MetaCoreRuntimeBindingStatus> 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<MetaCore::MetaCoreUiBindingDefinition>& 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<MetaCore::MetaCoreUiBindingDefinition>& uiBindings,
const MetaCore::MetaCoreRuntimeUiRenderer& runtimeUiRenderer,
std::vector<MetaCore::MetaCoreRuntimeBindingStatus>& 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<MetaCore::MetaCoreRuntimeDataUpdate>& updates,
const std::vector<MetaCore::MetaCoreDataPointDefinition>& dataPoints,
const std::vector<MetaCore::MetaCoreUiBindingDefinition>& uiBindings,
MetaCore::MetaCoreRuntimeUiRenderer& runtimeUiRenderer,
std::vector<MetaCore::MetaCoreRuntimeBindingStatus>& 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<MetaCore::MetaCoreRuntimeBindingStatus>& 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<MetaCore::MetaCoreRuntimeBindingStatus>& 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<MetaCore::MetaCoreRuntimeDataSourceStatus> MetaCoreCollectRuntimeSourceStatuses(
const std::vector<MetaCoreRuntimeDataSourceInstance>& runtimeSources
) {
std::vector<MetaCore::MetaCoreRuntimeDataSourceStatus> 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<MetaCore::MetaCoreRuntimeDataSourceStatus>& sourceStatuses,
const std::vector<MetaCore::MetaCoreRuntimeBindingStatus>& 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<std::string, std::string>& 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;
}
}
struct MetaCorePlayerLaunchConfig {
std::string Platform{"Windows"};
std::string GraphicsBackend{"OpenGL"};
std::string Architecture{"x64"};
std::string BuildProfile{"Development"};
};
[[nodiscard]] MetaCorePlayerLaunchConfig MetaCoreReadPlayerLaunchConfig(
const std::filesystem::path& configPath
) {
MetaCorePlayerLaunchConfig config;
std::ifstream input(configPath, std::ios::binary);
if (!input.is_open()) {
return config;
}
nlohmann::json document;
try {
input >> document;
} catch (const nlohmann::json::exception&) {
return config;
}
const auto targetIterator = document.find("target");
if (targetIterator != document.end() && targetIterator->is_object()) {
config.Platform = targetIterator->value("platform", config.Platform);
config.GraphicsBackend = targetIterator->value("graphics_backend", config.GraphicsBackend);
config.Architecture = targetIterator->value("architecture", config.Architecture);
}
const auto runtimeIterator = document.find("runtime");
if (runtimeIterator != document.end() && runtimeIterator->is_object()) {
config.BuildProfile = runtimeIterator->value("build_profile", config.BuildProfile);
}
return config;
}
[[nodiscard]] MetaCore::MetaCoreRenderBackend MetaCoreSelectRenderBackend(
const MetaCorePlayerLaunchConfig& launchConfig
) {
if (launchConfig.GraphicsBackend == "Vulkan") {
return MetaCore::MetaCoreRenderBackend::Vulkan;
}
if (launchConfig.GraphicsBackend == "WebGPU") {
return MetaCore::MetaCoreRenderBackend::WebGPU;
}
if (launchConfig.GraphicsBackend == "WebGL") {
std::cout << "MetaCorePlayer: WebGL is not available in native Player; using OpenGL backend\n";
}
return MetaCore::MetaCoreRenderBackend::OpenGL;
}
} // namespace
int main(int argc, char* argv[]) {
std::filesystem::path customProjectRoot{};
std::filesystem::path customScenePath{};
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--project") == 0 && i + 1 < argc) {
customProjectRoot = argv[i + 1];
++i;
} else if (std::strcmp(argv[i], "--scene") == 0 && i + 1 < argc) {
customScenePath = argv[i + 1];
++i;
}
}
if (argc > 0) {
std::filesystem::current_path(std::filesystem::path(argv[0]).parent_path());
}
const MetaCorePlayerLaunchConfig launchConfig =
MetaCoreReadPlayerLaunchConfig(std::filesystem::current_path() / "MetaCorePlayer.config.json");
std::cout << "MetaCorePlayer: target platform=" << launchConfig.Platform
<< " graphics=" << launchConfig.GraphicsBackend
<< " arch=" << launchConfig.Architecture
<< " config_profile=" << launchConfig.BuildProfile
<< '\n';
MetaCore::MetaCoreWindow window;
if (!window.Initialize(1280, 720, "MetaCore Player")) {
std::cerr << "MetaCorePlayer: window initialize failed\n";
return 1;
}
MetaCore::MetaCoreRenderDevice renderDevice;
MetaCore::MetaCoreRenderDeviceConfig renderConfig{};
renderConfig.Backend = MetaCoreSelectRenderBackend(launchConfig);
if (!renderDevice.Initialize(window, renderConfig)) {
std::cerr << "MetaCorePlayer: render device initialize failed\n";
return 1;
}
MetaCore::MetaCorePlayerRenderer playerRenderer;
if (!playerRenderer.Initialize(renderDevice, window)) {
std::cerr << "MetaCorePlayer: renderer initialize failed\n";
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);
} else {
const auto discovered = MetaCore::MetaCoreDiscoverProjectRoot();
if (!discovered.has_value()) {
std::cerr << "MetaCorePlayer: failed to discover project root\n";
return 1;
}
projectRoot = *discovered;
}
const std::filesystem::path projectPath = MetaCore::MetaCoreGetProjectFilePath(projectRoot);
playerRenderer.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(
runtimeDirectory / "ProjectRuntime.mcruntimecfg",
typeRegistry
);
auto runtimeProjectDocument =
loadedRuntimeProjectDocument.value_or(MetaCore::MetaCoreBuildDefaultRuntimeProjectDocument(runtimeDirectoryRelative));
MetaCore::MetaCoreApplyRuntimeProjectDefaults(runtimeProjectDocument, runtimeDirectoryRelative);
const std::vector<MetaCore::MetaCoreRuntimeConfigIssue> 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<MetaCore::MetaCoreCookManifestDocument> 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<MetaCore::MetaCoreSceneDocument> startupSceneDocument;
if (!customScenePath.empty()) {
const auto absoluteScene = customScenePath.is_absolute() ? customScenePath : (projectRoot / customScenePath);
startupSceneDocument = MetaCoreReadSceneDocumentFromPath(absoluteScene, typeRegistry);
}
if (!startupSceneDocument.has_value() &&
cookedManifest.has_value() &&
!runtimeProjectDocument.StartupScenePath.empty()) {
startupSceneDocument = MetaCoreReadCookedDocument<MetaCore::MetaCoreSceneDocument>(
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 =
MetaCoreReadSceneDocumentFromPath(projectRoot / runtimeProjectDocument.StartupScenePath, typeRegistry);
}
if (!startupSceneDocument.has_value()) {
startupSceneDocument = MetaCore::MetaCoreLoadStartupSceneDocument(projectPath);
}
MetaCore::MetaCoreScene scene;
if (startupSceneDocument.has_value()) {
if (cookedManifest.has_value()) {
MetaCoreRewriteSceneGltfModelPathsToCooked(*startupSceneDocument, *cookedManifest);
}
MetaCore::MetaCoreSceneSnapshot snapshot;
snapshot.GameObjects = startupSceneDocument->GameObjects;
scene.RestoreSnapshot(snapshot);
std::cout << "MetaCorePlayer: loaded startup scene from project "
<< projectPath.string() << '\n';
} else {
scene = MetaCore::MetaCoreCreateDefaultScene();
std::cout << "MetaCorePlayer: startup scene unavailable, using built-in default scene\n";
}
MetaCore::MetaCoreRuntimeLifecycleExecutor runtimeLifecycleExecutor;
const std::size_t runtimeComponentCount = runtimeLifecycleExecutor.RegisterDescriptors(
MetaCore::MetaCoreGetRuntimeLifecycleRegistry().GetDescriptors()
);
if (runtimeComponentCount > 0) {
std::cout << "MetaCorePlayer: registered " << runtimeComponentCount
<< " runtime lifecycle component(s)\n";
}
const auto runtimeLifecycleErrorSink = [](const MetaCore::MetaCoreRuntimeLifecycleError& error) {
std::cerr << "MetaCorePlayer: lifecycle " << error.Phase
<< " failed component=" << error.ComponentTypeId
<< " object=" << error.ObjectId
<< " error=" << error.Message << '\n';
};
runtimeLifecycleExecutor.EnterScene(scene, runtimeLifecycleErrorSink);
std::optional<MetaCore::MetaCoreUiDocument> startupUiDocument;
if (!runtimeProjectDocument.StartupUiPath.empty()) {
bool startupUiLoadedFromCooked = false;
if (cookedManifest.has_value()) {
startupUiDocument = MetaCoreReadCookedDocument<MetaCore::MetaCoreUiDocument>(
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();
const auto bindingsPath = (projectRoot / runtimeProjectDocument.BindingsPath).lexically_normal();
const auto diagnosticsPath = (projectRoot / runtimeProjectDocument.DiagnosticsPath).lexically_normal();
const auto loadedSourcesDocument = MetaCore::MetaCoreReadRuntimeDataSourcesDocument(sourcesPath, typeRegistry);
const auto loadedBindingsDocument = MetaCore::MetaCoreReadRuntimeBindingsDocument(bindingsPath, typeRegistry);
auto sourcesDocument = loadedSourcesDocument.value_or(MetaCore::MetaCoreRuntimeDataSourcesDocument{});
const auto bindingsDocument = loadedBindingsDocument.value_or(MetaCore::MetaCoreRuntimeBindingsDocument{});
if (loadedSourcesDocument.has_value() && loadedBindingsDocument.has_value()) {
std::cout << "MetaCorePlayer: loaded runtime config from binary .mcruntime documents\n";
} else {
const bool sourcesExists = std::filesystem::exists(sourcesPath);
const bool bindingsExists = std::filesystem::exists(bindingsPath);
if ((sourcesExists && !loadedSourcesDocument.has_value()) ||
(bindingsExists && !loadedBindingsDocument.has_value())) {
std::cerr << "MetaCorePlayer: runtime config exists but is unreadable or corrupted; continuing without runtime data bindings\n";
} else {
std::cout << "MetaCorePlayer: runtime config missing; continuing without runtime data bindings\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<MetaCore::MetaCoreRuntimeBindingStatus> runtimeUiBindingStatuses =
MetaCoreBuildRuntimeUiBindingStatuses(bindingsDocument.UiBindings);
MetaCoreValidateRuntimeUiBindingTargets(bindingsDocument.UiBindings, runtimeUiRenderer, runtimeUiBindingStatuses);
std::vector<MetaCoreRuntimeDataSourceInstance> 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 "
<< 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(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;
}
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="
<< 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));
}
std::uint64_t diagnosticsWriteFrame = 0;
std::unordered_map<std::string, std::string> 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();
playerRenderer.SetViewportRect(MetaCore::MetaCoreViewportRect{
0.0F,
0.0F,
static_cast<float>(windowWidth),
static_cast<float>(windowHeight)
});
runtimeUiRenderer.Resize(windowWidth, windowHeight);
runtimeUiRenderer.BeginFrame(1.0F / 60.0F);
runtimeLifecycleExecutor.TickScene(scene, 1.0F / 60.0F, runtimeLifecycleErrorSink);
if (!runtimeSources.empty()) {
std::uint64_t latestUpdateAt = 0;
for (MetaCoreRuntimeDataSourceInstance& sourceInstance : runtimeSources) {
sourceInstance.Adapter->Tick(1.0 / 60.0);
const std::vector<MetaCore::MetaCoreRuntimeDataUpdate> 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<MetaCore::MetaCoreRuntimeDataSourceStatus> 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;
}
}
}
if ((diagnosticsWriteFrame % 15ULL) == 0ULL) {
(void)MetaCore::MetaCoreWriteRuntimeDiagnosticsSnapshot(diagnosticsPath, diagnostics, typeRegistry);
}
++diagnosticsWriteFrame;
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<int>(sourceInstance.Adapter->GetStatus().State)
<< " error=" << sourceInstance.Adapter->GetStatus().LastError << '\n';
sourceInstance.LastReportedState = sourceInstance.Adapter->GetStatus().State;
}
}
MetaCoreLogRuntimeBindingIssues(diagnostics, reportedBindingIssues);
playerRenderer.RenderScene(scene, MetaCoreBuildPlayerSceneView());
runtimeUiRenderer.Render();
playerRenderer.SetRuntimeUiOverlayFrame(runtimeUiRenderer.GetLastFrame(), windowWidth, windowHeight);
playerRenderer.RenderAll();
renderDevice.RenderFrame();
renderDevice.PresentFrame();
window.EndFrame();
}
runtimeLifecycleExecutor.ExitScene(scene, runtimeLifecycleErrorSink);
runtimeUiRenderer.Shutdown();
playerRenderer.Shutdown();
renderDevice.Shutdown();
window.Shutdown();
return 0;
}