diff --git a/Deer/src/Deer/Core/Log.cpp b/Deer/src/Deer/Core/Log.cpp index 94584ef..2b55e9d 100755 --- a/Deer/src/Deer/Core/Log.cpp +++ b/Deer/src/Deer/Core/Log.cpp @@ -6,15 +6,15 @@ namespace Deer { std::shared_ptr Log::scriptLogger; std::shared_ptr Log::EditorEngineLogger; - void Log::init() - { - spdlog::set_pattern("%^[%T] %n: %v%$"); + void Log::init() { + // spdlog::set_pattern("%^[%T] %n: %v%$"); + spdlog::set_pattern("%n: %v%$"); coreLogger = spdlog::stdout_color_mt("Core"); clientLogger = spdlog::stdout_color_mt("Client"); scriptLogger = spdlog::stdout_color_mt("Script"); EditorEngineLogger = spdlog::stdout_color_mt("UI Engine"); - + coreLogger->set_level(spdlog::level::level_enum::trace); clientLogger->set_level(spdlog::level::level_enum::trace); scriptLogger->set_level(spdlog::level::level_enum::trace); @@ -30,8 +30,7 @@ namespace Deer { spdlog::drop_all(); } - void Log::coreTrace(const char* msg) - { - //coreLogger->trace(msg); + void Log::coreTrace(const char* msg) { + // coreLogger->trace(msg); } -} +} // namespace Deer diff --git a/Deer/src/Deer/Voxels/VoxelData.cpp b/Deer/src/Deer/Voxels/VoxelData.cpp index c1fe9d6..bb57d1b 100644 --- a/Deer/src/Deer/Voxels/VoxelData.cpp +++ b/Deer/src/Deer/Voxels/VoxelData.cpp @@ -61,9 +61,6 @@ namespace Deer { voxelData.name.c_str()); continue; } - // DEER_CORE_TRACE(" {0} - {1}", - // voxel.filename().generic_string().c_str(), - // voxelData.name); uint32_t id = voxelsInfo.size(); diff --git a/Deer/src/DeerRender/Voxels/VoxelData.cpp b/Deer/src/DeerRender/Voxels/VoxelData.cpp index c4d1e6a..23ea158 100644 --- a/Deer/src/DeerRender/Voxels/VoxelData.cpp +++ b/Deer/src/DeerRender/Voxels/VoxelData.cpp @@ -56,10 +56,6 @@ namespace Deer { continue; } - // DEER_CORE_TRACE(" {0} - {1}", - // voxelAspectPath.filename().generic_string().c_str(), - // aspectDefinition.voxelName.c_str()); - voxelsAspect[voxelID].definition = aspectDefinition; } @@ -92,9 +88,6 @@ namespace Deer { texturesIDs[faceTextureString] = textureID; voxelAspect.textureFacesIDs[i] = textureID; - - // DEER_CORE_TRACE(" texture {0} - id: {1}", - // faceTextureString.c_str(), textureID); } } } diff --git a/Deer/src/Plattform/OpenGL/OpenGLShader.cpp b/Deer/src/Plattform/OpenGL/OpenGLShader.cpp index b000a64..bc0b062 100755 --- a/Deer/src/Plattform/OpenGL/OpenGLShader.cpp +++ b/Deer/src/Plattform/OpenGL/OpenGLShader.cpp @@ -209,8 +209,6 @@ namespace Deer { // Either of them. Don't leak shaders. glDeleteShader(vertexShader); - DEER_CORE_TRACE(source); - // Use the infoLog as you see fit. DEER_CORE_ERROR("Error compiling fragment shader. \n{0}", infoLog); delete[] infoLog; diff --git a/DeerStudio/headers/DeerStudio/AngelScriptEngine.h b/DeerStudio/headers/DeerStudio/AngelScriptEngine.h index ae21eac..f0e91ea 100644 --- a/DeerStudio/headers/DeerStudio/AngelScriptEngine.h +++ b/DeerStudio/headers/DeerStudio/AngelScriptEngine.h @@ -1,6 +1,8 @@ #pragma once -#include #include "DeerRender/Tools/Path.h" +#include +#include +#include class asIScriptEngine; class asIScriptModule; @@ -24,6 +26,7 @@ namespace Deer { ~Service(); Service(Service&) = delete; + Service& operator=(Service&) = delete; Service(Service&&) = default; Service& operator=(Service&&) = default; @@ -32,11 +35,11 @@ namespace Deer { void shutdown(); protected: - asITypeInfo* type= nullptr; + asITypeInfo* type = nullptr; asIScriptObject* object = nullptr; - asIScriptFunction* updateFunction= nullptr; - asIScriptFunction* initFunction= nullptr; - asIScriptFunction* shutdownFunction= nullptr; + asIScriptFunction* updateFunction = nullptr; + asIScriptFunction* initFunction = nullptr; + asIScriptFunction* shutdownFunction = nullptr; }; struct Panel : public Service { @@ -55,12 +58,33 @@ namespace Deer { asIScriptFunction* menuBarFunction; }; + struct ModuleDescription { + public: + int patchVersion = -1; + std::string moduleName; + std::string modulePath; + std::vector module_requires; + }; + + enum class ModuleState { + NotBuilt = 0, + Building = 1, + Built = 2, + DependencyFailed = 3, + CompilationFailed = 5, + ExecutionError = 6 + }; + struct Module { public: - Module(asIScriptModule*); + ModuleDescription moduleInfo; + ModuleState state = ModuleState::NotBuilt; + + Module(const ModuleDescription&); ~Module(); Module(const Module&) = delete; + Module& operator=(Module&) = delete; Module(Module&&) = default; Module& operator=(Module&&) = default; @@ -69,19 +93,22 @@ namespace Deer { void render(); void shutdown(); - inline void invalidate() { invalidated = true; } + void extractTypes(); + + inline void invalidate() { state = ModuleState::ExecutionError; } private: + asIScriptModule* angelscriptModule; + std::vector panels; std::vector services; - asIScriptModule* angelscriptModule; - bool invalidated = false; }; extern asIScriptEngine* scriptEngine; extern asIScriptContext* scriptContext; extern CScriptBuilder scriptBuilder; extern std::vector modules; + extern std::unordered_map module_id; extern asITypeInfo* panelBaseType; extern asITypeInfo* serviceBaseType; diff --git a/DeerStudio/src/DeerStudio/AngelScriptEngine/AngelScriptEngine.cpp b/DeerStudio/src/DeerStudio/AngelScriptEngine/AngelScriptEngine.cpp index 0c899a3..acf3fc2 100644 --- a/DeerStudio/src/DeerStudio/AngelScriptEngine/AngelScriptEngine.cpp +++ b/DeerStudio/src/DeerStudio/AngelScriptEngine/AngelScriptEngine.cpp @@ -21,6 +21,7 @@ namespace Deer { CScriptBuilder scriptBuilder; std::vector modules; + std::unordered_map module_id; Module* executingModule; asITypeInfo* panelBaseType; @@ -82,6 +83,7 @@ namespace Deer { } modules.clear(); + module_id.clear(); if (scriptContext) scriptContext->Release(); diff --git a/DeerStudio/src/DeerStudio/AngelScriptEngine/Module.cpp b/DeerStudio/src/DeerStudio/AngelScriptEngine/Module.cpp index f3061fd..1d83836 100644 --- a/DeerStudio/src/DeerStudio/AngelScriptEngine/Module.cpp +++ b/DeerStudio/src/DeerStudio/AngelScriptEngine/Module.cpp @@ -1,12 +1,17 @@ -#include "DeerStudio/AngelScriptEngine.h" -#include "angelscript.h" #include "DeerRender/Log.h" +#include "DeerStudio/AngelScriptEngine.h" #include "DeerStudio/AngelScriptEngine/ErrorHandle.h" +#include "angelscript.h" namespace Deer { namespace AngelScriptEngine { - Module::Module(asIScriptModule* _angelscriptModule) { - angelscriptModule = _angelscriptModule; + Module::Module(const ModuleDescription& _mi) : moduleInfo(_mi) {} + + void Module::extractTypes() { + if (state != ModuleState::Built) + return; + + angelscriptModule = AngelScriptEngine::scriptEngine->GetModule(moduleInfo.moduleName.c_str()); uint32_t typeCount = angelscriptModule->GetObjectTypeCount(); for (uint32_t typeId = 0; typeId < typeCount; typeId++) { @@ -21,10 +26,9 @@ namespace Deer { } void Module::init() { - angelscriptModule->BindAllImportedFunctions(); - - if (invalidated) + if (state != ModuleState::Built) return; + angelscriptModule->BindAllImportedFunctions(); for (Service& service : services) service.init(); @@ -34,7 +38,7 @@ namespace Deer { } void Module::update() { - if (invalidated) + if (state != ModuleState::Built) return; for (Service& service : services) @@ -45,7 +49,7 @@ namespace Deer { } void Module::render() { - if (invalidated) + if (state != ModuleState::Built) return; for (Panel& panel : panels) @@ -53,14 +57,14 @@ namespace Deer { } void Module::shutdown() { + if (state != ModuleState::Built) + return; + for (Service& service : services) service.shutdown(); for (Panel& panel : panels) panel.shutdown(); - - if (invalidated) - return; } Module::~Module() { diff --git a/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleDescriptorSerializer.h b/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleDescriptorSerializer.h new file mode 100644 index 0000000..995a4ca --- /dev/null +++ b/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleDescriptorSerializer.h @@ -0,0 +1,17 @@ +#pragma once +#include "DeerStudio/AngelScriptEngine.h" +#include +#include + +namespace Deer { + namespace AngelScriptEngine { + template + void serialize(Archive& archive, + ModuleDescription& m) { + archive(cereal::make_nvp("name", m.moduleName)); + archive(cereal::make_nvp("patch", m.patchVersion)); + archive(cereal::make_nvp("requires", m.module_requires)); + } + + } // namespace AngelScriptEngine +} // namespace Deer \ No newline at end of file diff --git a/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleLoading.cpp b/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleLoading.cpp index 251571f..62d6f99 100644 --- a/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleLoading.cpp +++ b/DeerStudio/src/DeerStudio/AngelScriptEngine/ModuleLoading.cpp @@ -1,20 +1,28 @@ #include "DeerRender/Tools/Path.h" #include "DeerStudio/AngelScriptEngine.h" #include "DeerStudio/AngelScriptEngine/ErrorHandle.h" +#include "DeerStudio/AngelScriptEngine/ModuleDescriptorSerializer.h" + #include "angelscript.h" +#include "cereal/archives/json.hpp" #include "scriptbuilder.h" #include "DeerRender/Log.h" #include +#include #include +#include #include +#include +#include namespace fs = std::filesystem; namespace Deer { namespace AngelScriptEngine { - void loadModule(const Path& path); + bool loadModule(Module& module); + void loadModuleInfo(const Path& path); } // namespace AngelScriptEngine asIScriptFunction* AngelScriptEngine::getFactory(asITypeInfo* type) { @@ -35,36 +43,116 @@ namespace Deer { return type->GetFactoryByDecl(callString.c_str()); } - void AngelScriptEngine::loadModule(const Path& path) { + bool AngelScriptEngine::loadModule(Module& module) { int err; + module.state = ModuleState::Building; - scriptBuilder.StartNewModule(scriptEngine, path.stem().string().c_str()); - if (err < 0) { - DEER_EDITOR_ENGINE_ERROR("Failed to init module for {0}", path.string().c_str()); - return; + DEER_CORE_TRACE("Loading module {}", module.moduleInfo.moduleName); + for (const std::string& dependency : module.moduleInfo.module_requires) { + if (!module_id.contains(dependency)) { + DEER_CORE_ERROR("Failed to find dependency {} for module {}", + dependency.c_str(), module.moduleInfo.moduleName); + module.state = ModuleState::DependencyFailed; + return false; + } + + Module& dependencyModule = modules[module_id[dependency]]; + if (dependencyModule.state == ModuleState::DependencyFailed || + dependencyModule.state == ModuleState::CompilationFailed) { + DEER_CORE_ERROR("Failed to load dependency {} for module {}, the dependency did not load correctly and caused a chain reaction", + dependency.c_str(), module.moduleInfo.moduleName.c_str()); + module.state = ModuleState::DependencyFailed; + return false; + } + + if (dependencyModule.state == ModuleState::Building) { + DEER_CORE_ERROR("Failed to load dependency {} for module {} due to loop", + dependency.c_str(), module.moduleInfo.moduleName.c_str()); + module.state = ModuleState::DependencyFailed; + return false; + } + + if (dependencyModule.state == ModuleState::NotBuilt) { + if (!loadModule(dependencyModule)) { + module.state = ModuleState::DependencyFailed; + return false; + } + } } - for (const auto& entry : fs::recursive_directory_iterator(path)) { + scriptBuilder.StartNewModule(scriptEngine, module.moduleInfo.moduleName.c_str()); + if (err < 0) { + DEER_EDITOR_ENGINE_ERROR("Failed to init module for {0}", module.moduleInfo.moduleName.c_str()); + module.state = ModuleState::CompilationFailed; + return false; + } + + for (const auto& entry : fs::recursive_directory_iterator(module.moduleInfo.modulePath)) { if (!entry.is_regular_file() || entry.path().extension() != ".as") continue; - DEER_CORE_TRACE("{}", entry.path().filename().string().c_str()); err = scriptBuilder.AddSectionFromFile(entry.path().string().c_str()); if (err < 0) { - DEER_EDITOR_ENGINE_ERROR("Failed loading script for module {0}\nscript: {1}", path.string().c_str(), entry.path().string().c_str()); + DEER_EDITOR_ENGINE_ERROR("Failed loading script for module {0}\nscript: {1}", module.moduleInfo.moduleName.c_str(), entry.path().string().c_str()); + module.state = ModuleState::CompilationFailed; + return false; } } err = scriptBuilder.BuildModule(); - asIScriptModule* module = scriptBuilder.GetModule(); + asIScriptModule* as_module = scriptBuilder.GetModule(); if (err < 0) { - DEER_EDITOR_ENGINE_ERROR("Failed compiling module {}\nerror: {}", path.stem().string().c_str(), - Deer::AngelScriptEngine::getAngelScriptReturnCodeString(err)); + DEER_EDITOR_ENGINE_ERROR("Failed compiling module {}\nerror: {}", module.moduleInfo.moduleName.c_str(), Deer::AngelScriptEngine::getAngelScriptReturnCodeString(err)); + module.state = ModuleState::CompilationFailed; + return false; + } + + module.state = ModuleState::Built; + return true; + } + + void AngelScriptEngine::loadModuleInfo(const Path& path) { + ModuleDescription desc; + + std::ifstream is((path / "module.json").string()); + if (!is.is_open()) { + DEER_CORE_ERROR("Cannot open module description: {}", (path / "module.json").string().c_str()); + ModuleDescription desc; + desc.moduleName = path.stem().string(); + desc.patchVersion = 1; + std::ofstream os((path / "module.json").string()); + { + cereal::JSONOutputArchive archive(os); + serialize(archive, desc); + } return; } - modules.push_back({module}); + std::string contents((std::istreambuf_iterator(is)), + std::istreambuf_iterator()); + std::stringstream ss(contents); + try { + cereal::JSONInputArchive archive(ss); + serialize(archive, desc); + + if (desc.moduleName.empty()) { + DEER_CORE_ERROR("Module description missing 'name' field: {}", path.string().c_str()); + return; + } + if (desc.patchVersion < 0) { + DEER_CORE_ERROR("Module description missing 'name' field: {}", path.string().c_str()); + return; + } + } catch (const std::exception& e) { + DEER_CORE_ERROR("Module description missing 'name' field: {}", path.string().c_str()); + return; + } + + desc.modulePath = path.string(); + + module_id[desc.moduleName] = modules.size(); + modules.push_back({desc}); } void AngelScriptEngine::loadModules() { @@ -77,14 +165,22 @@ namespace Deer { } for (const auto& module : fs::directory_iterator(path)) { - // saveAngelscriptPredefined(_dir); if (module.path().extension() == ".disabled") { - DEER_CORE_TRACE("Ignoring {}", module.path().stem().string().c_str()); + DEER_CORE_WARN("Ignoring {}", module.path().stem().string().c_str()); continue; } - DEER_CORE_TRACE("=== Loading module {} ===", module.path().stem().string().c_str()); - saveAngelscriptPredefined(module.path()); - loadModule(module.path()); + loadModuleInfo(module.path()); + } + + for (Module& module : modules) { + saveAngelscriptPredefined(module.moduleInfo.modulePath); + if (module.state == ModuleState::NotBuilt) { + loadModule(module); + } + } + + for (Module& module : modules) { + module.extractTypes(); } } } // namespace Deer \ No newline at end of file diff --git a/DeerStudio/src/DeerStudio/AngelScriptEngine/Service.cpp b/DeerStudio/src/DeerStudio/AngelScriptEngine/Service.cpp index b02ee80..757aad6 100644 --- a/DeerStudio/src/DeerStudio/AngelScriptEngine/Service.cpp +++ b/DeerStudio/src/DeerStudio/AngelScriptEngine/Service.cpp @@ -29,7 +29,7 @@ namespace Deer { if (!initFunction) return; - + AS_CHECK(scriptContext->Prepare(initFunction)); AS_CHECK(scriptContext->SetObject(object)); AS_CHECK(scriptContext->Execute()); @@ -52,7 +52,7 @@ namespace Deer { if (!shutdownFunction) { if (object) object->Release(); - + AS_CHECK(scriptContext->Unprepare()); return; } @@ -62,7 +62,7 @@ namespace Deer { AS_CHECK(scriptContext->Execute()); if (object) - object->Release(); + object->Release(); AS_CHECK(scriptContext->Unprepare()); } @@ -75,6 +75,6 @@ namespace Deer { shutdownFunction = type->GetMethodByDecl("void shutdown()"); } - Service::~Service() { } + Service::~Service() {} } // namespace AngelScriptEngine } // namespace Deer \ No newline at end of file diff --git a/DeerStudio/src/DeerStudio/EditorDataImporter/MeshData.cpp b/DeerStudio/src/DeerStudio/EditorDataImporter/MeshData.cpp index 48dba2f..01fc8dd 100644 --- a/DeerStudio/src/DeerStudio/EditorDataImporter/MeshData.cpp +++ b/DeerStudio/src/DeerStudio/EditorDataImporter/MeshData.cpp @@ -17,7 +17,6 @@ namespace Deer { Scope StorageData::deserialize() { Assimp::Importer importer; - DEER_CORE_TRACE("Size {}", size); std::string extension; const char* extension_char = nullptr; if (getMetadata().contains("dataId")) { diff --git a/DeerStudio/src/DeerStudio/EditorDataSource.cpp b/DeerStudio/src/DeerStudio/EditorDataSource.cpp index dacad72..4885c5d 100644 --- a/DeerStudio/src/DeerStudio/EditorDataSource.cpp +++ b/DeerStudio/src/DeerStudio/EditorDataSource.cpp @@ -1,5 +1,5 @@ -#include "DeerRender/Log.h" #include "DeerStudio/EditorDataSource.h" +#include "DeerRender/Log.h" #include #include @@ -8,6 +8,11 @@ namespace Deer { StorageData StorageBackend::loadData(const std::string& location) { Path path = Path("Editor") / location; + + if (!std::filesystem::exists(path)) { + return StorageData(); + } + std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file.is_open()) { DEER_CORE_ERROR("Failed to open file '{}'", location.c_str()); diff --git a/DeerStudio/src/DeerStudio/ResourceDataSource.cpp b/DeerStudio/src/DeerStudio/ResourceDataSource.cpp index 4b9ff1c..149205f 100644 --- a/DeerStudio/src/DeerStudio/ResourceDataSource.cpp +++ b/DeerStudio/src/DeerStudio/ResourceDataSource.cpp @@ -1,5 +1,5 @@ -#include "DeerRender/Log.h" #include "DeerStudio/ResourceDataSource.h" +#include "DeerRender/Log.h" #include #include @@ -55,6 +55,10 @@ namespace Deer { Path path = Path("Resources") / (location + ".meta"); StorageMetadata metadata; + if (!std::filesystem::exists(path)) { + return metadata; + } + std::ifstream file(path); if (!file.is_open()) { DEER_CORE_TRACE("Failed to open metadata file '{}' for reading", path.string().c_str()); diff --git a/DeerStudio/src/DeerStudio/StudioAPI/Entity.cpp b/DeerStudio/src/DeerStudio/StudioAPI/Entity.cpp index 73de823..cb8035c 100644 --- a/DeerStudio/src/DeerStudio/StudioAPI/Entity.cpp +++ b/DeerStudio/src/DeerStudio/StudioAPI/Entity.cpp @@ -9,7 +9,7 @@ #include -#define GET_ENV(env) ((env < 0)? Scene::environment : Deer::Resource::unsafeFromId(environmentId).getData()) +#define GET_ENV(env) ((env < 0) ? Scene::environment : Deer::Resource::unsafeFromId(environmentId).getData()) #define GET_ENTITY(env, id) GET_ENV(env).getEntity(id) #define GET_MESH_COMPONENT(env, id) \ @@ -47,7 +47,7 @@ namespace Deer { EntityStruct::EntityStruct(uint16_t _entId, int32_t _envId) : EntityHandleStruct(_entId, _envId) {} Environment* EntityHandleStruct::getEntityEnvironment() { - if (environmentId < 0) + if (environmentId < 0) return &Scene::environment; return &Resource::unsafeFromId(environmentId).getData(); } @@ -241,9 +241,8 @@ namespace Deer { EntityHandleStruct EntityStruct::createChild(std::string& name) { ASSERT_ENTITY("createChild()", return *this); - DEER_CORE_INFO("Creating child on env {}", environmentId); Environment* entityEnv = getEntityEnvironment(); - + Entity& newEnt = entityEnv->createEntity(name); Entity& me = GET_ENTITY(environmentId, entityId); diff --git a/Editor/Modules/ActiveEntity/module.json b/Editor/Modules/ActiveEntity/module.json new file mode 100644 index 0000000..8ab3307 --- /dev/null +++ b/Editor/Modules/ActiveEntity/module.json @@ -0,0 +1,5 @@ +{ + "patch": 1, + "name": "ActiveEntity", + "requires": [] +} diff --git a/Editor/Modules/EntityManipulation/module.json b/Editor/Modules/EntityManipulation/module.json new file mode 100644 index 0000000..067e357 --- /dev/null +++ b/Editor/Modules/EntityManipulation/module.json @@ -0,0 +1,5 @@ +{ + "name": "EntityManipulation", + "patch": 1, + "requires": ["ActiveEntity"] +} \ No newline at end of file diff --git a/Editor/Modules/Previewer/MeshPreview.as b/Editor/Modules/Previewer/MeshPreview.as index f736e23..f57e071 100644 --- a/Editor/Modules/Previewer/MeshPreview.as +++ b/Editor/Modules/Previewer/MeshPreview.as @@ -22,7 +22,6 @@ class RenderService : Service { shaderC.shader = Resource::loadShader("shader.glsl"); - Engine::print("CAM : " + sceneCamera.transform.position.x + " " + sceneCamera.transform.scale.x + " : " + sceneCamera.camera.aspect); sceneCamera.transform.position.z = -3; sceneCamera.transform.position.y = 1; diff --git a/Editor/Modules/Previewer/module.json b/Editor/Modules/Previewer/module.json new file mode 100644 index 0000000..7d50c9d --- /dev/null +++ b/Editor/Modules/Previewer/module.json @@ -0,0 +1,5 @@ +{ + "patch": 1, + "name": "Previewer", + "requires": [] +} diff --git a/Editor/Modules/ResourceExplorer/module.json b/Editor/Modules/ResourceExplorer/module.json new file mode 100644 index 0000000..6666d44 --- /dev/null +++ b/Editor/Modules/ResourceExplorer/module.json @@ -0,0 +1,5 @@ +{ + "patch": 1, + "name": "ResourceExplorer", + "requires": ["Previewer"] +} diff --git a/Editor/Modules/TreeExplorer/module.json b/Editor/Modules/TreeExplorer/module.json new file mode 100644 index 0000000..2d50a45 --- /dev/null +++ b/Editor/Modules/TreeExplorer/module.json @@ -0,0 +1,5 @@ +{ + "patch": 1, + "name": "TreeExplorer", + "requires": ["ActiveEntity"] +} diff --git a/Editor/Modules/Viewport/module.json b/Editor/Modules/Viewport/module.json new file mode 100644 index 0000000..26b8f5e --- /dev/null +++ b/Editor/Modules/Viewport/module.json @@ -0,0 +1,5 @@ +{ + "patch": 1, + "name": "Viewport", + "requires": ["Previewer"] +}