diff --git a/.gitignore b/.gitignore index 4b48de02..1922b7e2 100644 --- a/.gitignore +++ b/.gitignore @@ -857,3 +857,8 @@ bin-int imgui.ini .vscode/launch.json .directory +Nuake/dependencies/glad/bin-obj +Nuake/dependencies/glad/glad.vcxproj +Nuake/dependencies/glad/glad.vcxproj.filters +*.vcxproj +*.csproj diff --git a/Editor/src/ComponentsPanel/NetScriptPanel.cpp b/Editor/src/ComponentsPanel/NetScriptPanel.cpp new file mode 100644 index 00000000..2f0f3f49 --- /dev/null +++ b/Editor/src/ComponentsPanel/NetScriptPanel.cpp @@ -0,0 +1,107 @@ +#include "NetScriptPanel.h" +#include "../Windows/FileSystemUI.h" +#include +#include + +void NetScriptPanel::Draw(Nuake::Entity entity) +{ + if (!entity.HasComponent()) + return; + + auto& component = entity.GetComponent(); + BeginComponentTable(.NETSCRIPT, Nuake::NetScriptComponent); + { + { + ImGui::Text("Script"); + ImGui::TableNextColumn(); + + std::string path = component.ScriptPath; + ImGui::Button(path.empty() ? "Create New" : component.ScriptPath.c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0)); + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("_NetScript")) + { + char* file = (char*)payload->Data; + + std::string fullPath = std::string(file, 512); + path = Nuake::FileSystem::AbsoluteToRelative(std::move(fullPath)); + + } + ImGui::EndDragDropTarget(); + } + + component.ScriptPath = path; + + // Double click on file + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) + { + if (!component.ScriptPath.empty()) + { + Nuake::OS::OpenIn(Nuake::FileSystem::Root + "component.ScriptPath"); + } + else + { + // TODO: Turn into command (Undo/Redo) + std::string pathCreation = Nuake::FileDialog::SaveFile("*.cs"); + + if (!pathCreation.empty()) + { + if (!Nuake::String::EndsWith(pathCreation, ".cs")) + { + pathCreation += ".cs"; + } + + std::string fileName = Nuake::String::ToUpper(Nuake::FileSystem::GetFileNameFromPath(pathCreation)); + fileName = Nuake::String::RemoveWhiteSpace(fileName); + + if (!Nuake::String::IsDigit(fileName[0])) + { + Nuake::FileSystem::BeginWriteFile(pathCreation); + Nuake::FileSystem::WriteLine(NET_TEMPLATE_SCRIPT_FIRST + fileName + NET_TEMPLATE_SCRIPT_SECOND); + Nuake::FileSystem::EndWriteFile(); + + path = Nuake::FileSystem::AbsoluteToRelative(pathCreation); + Nuake::FileSystem::Scan(); + Nuake::FileSystemUI::m_CurrentDirectory = Nuake::FileSystem::RootDirectory; + } + else + { + Nuake::Logger::Log("Cannot create script files that starts with a number.", "fileSystem", Nuake::CRITICAL); + } + } + + } + } + + ImGui::TableNextColumn(); + + ComponentTableReset(component.ScriptPath, ""); + } + //ImGui::TableNextColumn(); + //{ + // ImGui::Text("Module"); + // ImGui::TableNextColumn(); + // + // // Here we create a dropdown for every modules + // auto& wrenScript = component.mWrenScript; + // if (wrenScript) + // { + // auto modules = wrenScript->GetModules(); + // + // std::vector modulesC; + // + // for (auto& m : modules) + // { + // modulesC.push_back(m.c_str()); + // } + // static int currentModule = (int)component.mModule; + // ImGui::Combo("##WrenModule", ¤tModule, &modulesC[0], modules.size()); + // component.mModule = currentModule; + // } + // + // ImGui::TableNextColumn(); + // //ComponentTableReset(component.Class, ""); + //} + } + EndComponentTable(); +} \ No newline at end of file diff --git a/Editor/src/ComponentsPanel/NetScriptPanel.h b/Editor/src/ComponentsPanel/NetScriptPanel.h new file mode 100644 index 00000000..83bd725c --- /dev/null +++ b/Editor/src/ComponentsPanel/NetScriptPanel.h @@ -0,0 +1,45 @@ +#pragma once +#include "ComponentPanel.h" + + +const std::string NET_TEMPLATE_SCRIPT_FIRST = R"(using Nuake.Net; + +namespace NuakeShowcase +{ + class )"; + +const std::string NET_TEMPLATE_SCRIPT_SECOND = R"( : Entity + { + public override void OnInit() + { + // Called once at the start of the game + } + + + public override void OnUpdate(float dt) + { + // Called every frame + } + + public override void OnFixedUpdate(float dt) + { + // Called every fixed update + } + + + public override void OnDestroy() + { + // Called at the end of the game fixed update + } + } +} +)"; + + +class NetScriptPanel : ComponentPanel { + +public: + NetScriptPanel() {} + + void Draw(Nuake::Entity entity) override; +}; \ No newline at end of file diff --git a/Editor/src/Windows/EditorInterface.cpp b/Editor/src/Windows/EditorInterface.cpp index 99586a48..1f1c4f5e 100644 --- a/Editor/src/Windows/EditorInterface.cpp +++ b/Editor/src/Windows/EditorInterface.cpp @@ -52,6 +52,7 @@ #include #include +#include namespace Nuake { @@ -2009,6 +2010,16 @@ namespace Nuake { if (ImGui::MenuItem("Duplicate selected", NULL)) {} ImGui::EndMenu(); } + if (ImGui::BeginMenu(".Net")) + { + if (ImGui::MenuItem("Generate Solution", NULL)) + { + Nuake::ScriptingEngineNet::Get().GenerateSolution(FileSystem::Root, Engine::GetProject()->Name); + Nuake::Logger::Log("Generated Solution."); + } + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Debug")) { if (ImGui::MenuItem("Show ImGui demo", NULL, m_ShowImGuiDemo)) m_ShowImGuiDemo = !m_ShowImGuiDemo; diff --git a/Editor/src/Windows/EditorSelectionPanel.cpp b/Editor/src/Windows/EditorSelectionPanel.cpp index 0ebb5e11..ce15b92e 100644 --- a/Editor/src/Windows/EditorSelectionPanel.cpp +++ b/Editor/src/Windows/EditorSelectionPanel.cpp @@ -112,6 +112,8 @@ void EditorSelectionPanel::DrawEntity(Nuake::Entity entity) mTransformPanel.Draw(entity); mLightPanel.Draw(entity); mScriptPanel.Draw(entity); + mNetScriptPanel.Draw(entity); + mAudioEmitterPanel.Draw(entity); mParticleEmitterPanel.Draw(entity); mSpritePanel.Draw(entity); mMeshPanel.Draw(entity); @@ -127,6 +129,7 @@ void EditorSelectionPanel::DrawEntity(Nuake::Entity entity) mMeshColliderPanel.Draw(entity); mCharacterControllerPanel.Draw(entity); mAudioEmitterPanel.Draw(entity); + } void EditorSelectionPanel::DrawAddComponentMenu(Nuake::Entity entity) @@ -144,6 +147,7 @@ void EditorSelectionPanel::DrawAddComponentMenu(Nuake::Entity entity) if (ImGui::BeginPopup("ComponentPopup")) { MenuItemComponent("Wren Script", WrenScriptComponent); + MenuItemComponent("C# Script", NetScriptComponent); MenuItemComponent("Camera", CameraComponent); MenuItemComponent("Light", LightComponent); ImGui::Separator(); diff --git a/Editor/src/Windows/EditorSelectionPanel.h b/Editor/src/Windows/EditorSelectionPanel.h index b380444a..a84c4325 100644 --- a/Editor/src/Windows/EditorSelectionPanel.h +++ b/Editor/src/Windows/EditorSelectionPanel.h @@ -23,6 +23,7 @@ #include "../ComponentsPanel/SkinnedModelPanel.h" #include "../ComponentsPanel/BonePanel.h" #include "../ComponentsPanel/AudioEmitterPanel.h" +#include "../ComponentsPanel/NetScriptPanel.h" class EditorSelectionPanel { @@ -30,6 +31,7 @@ private: TransformPanel mTransformPanel; LightPanel mLightPanel; ScriptPanel mScriptPanel; + NetScriptPanel mNetScriptPanel; MeshPanel mMeshPanel; SkinnedModelPanel mSkinnedModelPanel; QuakeMapPanel mQuakeMapPanel; diff --git a/Editor/src/Windows/FileSystemUI.cpp b/Editor/src/Windows/FileSystemUI.cpp index c65c0dc6..60c5e558 100644 --- a/Editor/src/Windows/FileSystemUI.cpp +++ b/Editor/src/Windows/FileSystemUI.cpp @@ -274,6 +274,10 @@ namespace Nuake { dragType = "_Script"; } + else if (fileExtension == ".cs") + { + dragType = "_NetScript"; + } else if (fileExtension == ".map") { dragType = "_Map"; diff --git a/Nuake/src/Core/FileSystem.cpp b/Nuake/src/Core/FileSystem.cpp index fe5c0f0d..648722b0 100644 --- a/Nuake/src/Core/FileSystem.cpp +++ b/Nuake/src/Core/FileSystem.cpp @@ -226,18 +226,31 @@ namespace Nuake if (!absolute) finalPath = Root + path; - std::ifstream MyReadFile(finalPath); + std::ifstream myReadFile(finalPath, std::ios::in | std::ios::binary); std::string fileContent = ""; std::string allFile = ""; + + char bom[3]; + myReadFile.read(bom, 3); + + // Check for UTF-8 BOM (EF BB BF) + if (bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) + { + myReadFile.seekg(3); + } + else + { + myReadFile.seekg(0); + } // Use a while loop together with the getline() function to read the file line by line - while (getline(MyReadFile, fileContent)) + while (getline(myReadFile, fileContent)) { allFile.append(fileContent + "\n"); } // Close the file - MyReadFile.close(); + myReadFile.close(); return allFile; } diff --git a/Nuake/src/Core/FileSystem.h b/Nuake/src/Core/FileSystem.h index 00f3d17b..4c067684 100644 --- a/Nuake/src/Core/FileSystem.h +++ b/Nuake/src/Core/FileSystem.h @@ -59,11 +59,13 @@ namespace Nuake Image, Material, Script, + NetScript, Project, Prefab, Scene, Wad, Map, + Assembly }; class File @@ -124,6 +126,16 @@ namespace Nuake return FileType::Map; } + if (ext == ".dll") + { + return FileType::Assembly; + } + + if (ext == ".cs") + { + return FileType::NetScript; + } + return FileType::Unkown; } @@ -170,6 +182,16 @@ namespace Nuake return "Map"; } + if (ext == ".map") + { + return "Assembly"; + } + + if (ext == ".cs") + { + return "C# Script"; + } + return "File"; } std::string Read() diff --git a/Nuake/src/Scene/Components/Components.h b/Nuake/src/Scene/Components/Components.h index b2962548..ba90407c 100644 --- a/Nuake/src/Scene/Components/Components.h +++ b/Nuake/src/Scene/Components/Components.h @@ -24,4 +24,5 @@ #include "BoxCollider.h" #include "AudioEmitterComponent.h" #include "WrenScriptComponent.h" +#include "NetScriptComponent.h" #include "../Entities/Entity.h" diff --git a/Nuake/src/Scene/Components/NetScriptComponent.h b/Nuake/src/Scene/Components/NetScriptComponent.h new file mode 100644 index 00000000..d99a94d9 --- /dev/null +++ b/Nuake/src/Scene/Components/NetScriptComponent.h @@ -0,0 +1,39 @@ +#pragma once +#include "src/Core/Core.h" +#include "src/Core/FileSystem.h" +#include "src/Core/Logger.h" +#include "src/Resource/Serializable.h" +#include "src/Resource/File.h" + +namespace Nuake { + + class NetScriptComponent + { + public: + std::string ScriptPath; + std::string Class; + + json Serialize() + { + BEGIN_SERIALIZE(); + SERIALIZE_VAL(ScriptPath); + SERIALIZE_VAL(Class); + END_SERIALIZE(); + } + + bool Deserialize(const json& j) + { + if (j.contains("ScriptPath")) + { + ScriptPath = j["ScriptPath"]; + } + + if (j.contains("Class")) + { + Class = j["Class"]; + } + + return true; + } + }; +} \ No newline at end of file diff --git a/Nuake/src/Scene/Entities/Entity.cpp b/Nuake/src/Scene/Entities/Entity.cpp index cc74c9fa..ebcb3224 100644 --- a/Nuake/src/Scene/Entities/Entity.cpp +++ b/Nuake/src/Scene/Entities/Entity.cpp @@ -77,6 +77,8 @@ namespace Nuake SERIALIZE_OBJECT_REF_LBL("BoneComponent", GetComponent()) if (HasComponent()) SERIALIZE_OBJECT_REF_LBL("AudioEmitterComponent", GetComponent()) + if (HasComponent()) + SERIALIZE_OBJECT_REF_LBL("NetScriptComponent", GetComponent()) END_SERIALIZE(); } @@ -108,6 +110,7 @@ namespace Nuake DESERIALIZE_COMPONENT(BoneComponent); DESERIALIZE_COMPONENT(SkinnedModelComponent); DESERIALIZE_COMPONENT(AudioEmitterComponent); + DESERIALIZE_COMPONENT(NetScriptComponent); return true; } diff --git a/Nuake/src/Scene/Systems/ScriptingSystem.cpp b/Nuake/src/Scene/Systems/ScriptingSystem.cpp index 5fec2dd2..fdc479f2 100644 --- a/Nuake/src/Scene/Systems/ScriptingSystem.cpp +++ b/Nuake/src/Scene/Systems/ScriptingSystem.cpp @@ -1,5 +1,6 @@ #include "ScriptingSystem.h" #include "src/Scene/Components/WrenScriptComponent.h" +#include "src/Scene/Components/NetScriptComponent.h" #include "src/Scene/Scene.h" #include "Engine.h" @@ -17,14 +18,12 @@ namespace Nuake { ScriptingEngine::Init(); - ScriptingEngineNet::Get().Initialize(); - Logger::Log("Initializing ScriptingSystem"); - auto entities = m_Scene->m_Registry.view(); - for (auto& e : entities) + auto wrenEntities = m_Scene->m_Registry.view(); + for (auto& e : wrenEntities) { - WrenScriptComponent& wren = entities.get(e); + WrenScriptComponent& wren = wrenEntities.get(e); if (!wren.mWrenScript) continue; @@ -41,6 +40,27 @@ namespace Nuake wren.mWrenScript->CallInit(); } + auto& scriptingEngineNet = ScriptingEngineNet::Get(); + scriptingEngineNet.Initialize(); + scriptingEngineNet.LoadProjectAssembly(Engine::GetProject()); + + auto netEntities = m_Scene->m_Registry.view(); + for (auto& e : netEntities) + { + NetScriptComponent& netScriptComponent = netEntities.get(e); + + if (netScriptComponent.ScriptPath.empty()) + continue; + + // Creates an instance of the entity script in C# + auto entity = Entity{ e, m_Scene }; + scriptingEngineNet.RegisterEntityScript(entity); + + // We can now call on init on it. + auto scriptInstance = scriptingEngineNet.GetEntityScript(entity); + scriptInstance.InvokeMethod("OnInit"); + } + return true; } @@ -57,6 +77,20 @@ namespace Nuake if (wren.mWrenScript != nullptr) wren.mWrenScript->CallUpdate(ts); } + + auto& scriptingEngineNet = ScriptingEngineNet::Get(); + auto netEntities = m_Scene->m_Registry.view(); + for (auto& e : netEntities) + { + NetScriptComponent& netScriptComponent = netEntities.get(e); + + if (netScriptComponent.ScriptPath.empty()) + continue; + + auto entity = Entity{ e, m_Scene }; + auto scriptInstance = scriptingEngineNet.GetEntityScript(entity); + scriptInstance.InvokeMethod("OnUpdate", ts.GetSeconds()); + } } @@ -73,6 +107,20 @@ namespace Nuake if (wren.mWrenScript != nullptr) wren.mWrenScript->CallFixedUpdate(ts); } + + auto& scriptingEngineNet = ScriptingEngineNet::Get(); + auto netEntities = m_Scene->m_Registry.view(); + for (auto& e : netEntities) + { + NetScriptComponent& netScriptComponent = netEntities.get(e); + + if (netScriptComponent.ScriptPath.empty()) + continue; + + auto entity = Entity{ e, m_Scene }; + auto scriptInstance = scriptingEngineNet.GetEntityScript(entity); + scriptInstance.InvokeMethod("OnFixedUpdate", ts.GetSeconds()); + } } @@ -94,6 +142,22 @@ namespace Nuake } + auto& scriptingEngineNet = ScriptingEngineNet::Get(); + auto netEntities = m_Scene->m_Registry.view(); + for (auto& e : netEntities) + { + NetScriptComponent& netScriptComponent = netEntities.get(e); + + if (netScriptComponent.ScriptPath.empty()) + continue; + + // Creates an instance of the entity script in C# + auto entity = Entity{ e, m_Scene }; + auto scriptInstance = scriptingEngineNet.GetEntityScript(entity); + scriptInstance.InvokeMethod("OnDestroy"); + } + ScriptingEngine::Close(); + ScriptingEngineNet::Get().Uninitialize(); } } diff --git a/Nuake/src/Scripting/NetModules/EngineNetAPI.cpp b/Nuake/src/Scripting/NetModules/EngineNetAPI.cpp new file mode 100644 index 00000000..247bc844 --- /dev/null +++ b/Nuake/src/Scripting/NetModules/EngineNetAPI.cpp @@ -0,0 +1,15 @@ +#include "EngineNetAPI.h" + +namespace Nuake { + + void Log(Coral::NativeString string) + { + Logger::Log(string.ToString(), ".net", VERBOSE); + } + + void EngineNetAPI::RegisterMethods() + { + RegisterMethod("LoggerLogIcall", (void*)(&Log)); + } + +} diff --git a/Nuake/src/Scripting/NetModules/EngineNetAPI.h b/Nuake/src/Scripting/NetModules/EngineNetAPI.h new file mode 100644 index 00000000..89fc0846 --- /dev/null +++ b/Nuake/src/Scripting/NetModules/EngineNetAPI.h @@ -0,0 +1,14 @@ +#pragma once +#include "NetAPIModule.h" + +namespace Nuake { + + class EngineNetAPI : public NetAPIModule + { + public: + virtual const std::string GetModuleName() const override { return "Engine"; } + + virtual void RegisterMethods() override; + + }; +} \ No newline at end of file diff --git a/Nuake/src/Scripting/NetModules/NetAPIModule.h b/Nuake/src/Scripting/NetModules/NetAPIModule.h new file mode 100644 index 00000000..b388c69b --- /dev/null +++ b/Nuake/src/Scripting/NetModules/NetAPIModule.h @@ -0,0 +1,25 @@ +#pragma once +#include "src/Core/Core.h" +#include "src/Core/Logger.h" + +#include + +namespace Nuake { + + class NetAPIModule + { + public: + virtual const std::string GetModuleName() const = 0; + virtual void RegisterMethods() = 0; + + std::unordered_map GetMethods() const { return m_Methods; } + protected: + void RegisterMethod(const std::string& name, void* methodPtr) + { + m_Methods.emplace(name, methodPtr); + } + + private: + std::unordered_map m_Methods; + }; +} \ No newline at end of file diff --git a/Nuake/src/Scripting/ScriptingEngine.cpp b/Nuake/src/Scripting/ScriptingEngine.cpp index 9b2df383..84f20df2 100644 --- a/Nuake/src/Scripting/ScriptingEngine.cpp +++ b/Nuake/src/Scripting/ScriptingEngine.cpp @@ -1,3 +1,4 @@ + #include "ScriptingEngine.h" #include "WrenScript.h" #include "src/Core/FileSystem.h" diff --git a/Nuake/src/Scripting/ScriptingEngineNet.cpp b/Nuake/src/Scripting/ScriptingEngineNet.cpp index 67d098a2..e6f1a542 100644 --- a/Nuake/src/Scripting/ScriptingEngineNet.cpp +++ b/Nuake/src/Scripting/ScriptingEngineNet.cpp @@ -2,11 +2,16 @@ #include "src/Core/Logger.h" #include "src/Core/FileSystem.h" +#include "src/Core/OS.h" +#include "src/Resource/Project.h" + +#include "NetModules/EngineNetAPI.h" #include #include #include #include +#include void ExceptionCallback(std::string_view InMessage) @@ -19,16 +24,21 @@ namespace Nuake { ScriptingEngineNet::ScriptingEngineNet() { - auto coralDir = ""; - Coral::HostSettings settings = - { - .CoralDirectory = coralDir, - .ExceptionCallback = ExceptionCallback - }; - m_HostInstance = new Coral::HostInstance(); - m_HostInstance->Initialize(settings); - + // Initialize Coral + // ---------------------------------- + + // Initialize API modules + // ---------------------------------- + m_Modules = + { + CreateRef() + }; + + for (auto& m : m_Modules) + { + m->RegisterMethods(); + } } ScriptingEngineNet::~ScriptingEngineNet() @@ -42,30 +52,241 @@ namespace Nuake return instance; } - void Log(Coral::NativeString string) - { - Logger::Log(string.ToString(), ".net", VERBOSE); - } - void ScriptingEngineNet::Initialize() { - auto loadContext = m_HostInstance->CreateAssemblyLoadContext("NuakeEngineContext"); + Coral::HostSettings settings = + { + .CoralDirectory = "", + .ExceptionCallback = ExceptionCallback + }; - auto& assembly = loadContext.LoadAssembly("NuakeNet.dll"); - assembly.AddInternalCall("Nuake.Net.Engine", "LoggerLogIcall", reinterpret_cast(&Log)); - assembly.UploadInternalCalls(); + m_HostInstance = new Coral::HostInstance(); + m_HostInstance->Initialize(settings); - auto& engineType = assembly.GetType("Nuake.Net.Engine"); - auto engineInstance = engineType.CreateInstance(); + m_LoadContext = std::move(m_HostInstance->CreateAssemblyLoadContext(m_ContextName)); - Coral::NativeString param1 = Coral::NativeString::FromUTF8("Hello from CPP");; - engineInstance.InvokeMethod("Log", std::string("Hello from CPP")); + // Load Nuake assembly DLL + const std::string absoluteAssemblyPath = FileSystem::Root + m_NetDirectory + "/" + m_EngineAssemblyName; + m_NuakeAssembly = m_LoadContext.LoadAssembly(absoluteAssemblyPath); - engineInstance.Destroy(); + // Upload internal calls for each module + // -------------------------------------------------- + for (const auto& netModule : m_Modules) + { + const std::string inClassName = m_Scope + '.' + netModule->GetModuleName(); + for (const auto& [methodName, methodPtr] : netModule->GetMethods()) + { + m_NuakeAssembly.AddInternalCall(inClassName, methodName, methodPtr); + } + } - Coral::GC::Collect(); + m_NuakeAssembly.UploadInternalCalls(); + } + + void ScriptingEngineNet::Uninitialize() + { + // We have to manually destroy every managed object we have created + for (auto& [entity, managedObject] : m_EntityToManagedObjects) + { + managedObject.Destroy(); + } + + Coral::GC::Collect(1, Coral::GCCollectionMode::Forced); Coral::GC::WaitForPendingFinalizers(); - m_HostInstance->UnloadAssemblyLoadContext(loadContext); + m_HostInstance->UnloadAssemblyLoadContext(m_LoadContext); + + m_EntityToManagedObjects.clear(); + m_GameEntityTypes.clear(); } + + void ScriptingEngineNet::LoadProjectAssembly(Ref project) + { + const std::string sanitizedProjectName = String::Sanitize(project->Name); + const std::string assemblyPath = "/bin/Debug/net7.0/" + sanitizedProjectName + ".dll"; + + if (!FileSystem::FileExists(assemblyPath)) + { + Logger::Log("Couldn't load .net assembly. Did you forget to build the .net project?", ".net", CRITICAL); + return; + } + + const std::string absoluteAssemblyPath = FileSystem::Root + assemblyPath; + m_GameAssembly = m_LoadContext.LoadAssembly(absoluteAssemblyPath); + + for (auto& type : m_GameAssembly.GetTypes()) + { + Logger::Log(std::string("Detected type: ") + std::string(type->GetName()), ".net"); + Logger::Log(std::string("Detected base type: ") + std::string(type->GetBaseType().GetName()), ".net"); + + const std::string baseTypeName = std::string(type->GetBaseType().GetName()); + if (baseTypeName == "Entity") + { + // We have found an entity script. + m_GameEntityTypes[std::string(type->GetName())] = type; + } + } + } + + void ScriptingEngineNet::RegisterEntityScript(Entity& entity) + { + if (!entity.IsValid()) + { + Logger::Log("Failed to register entity .net script: Entity not valid.", ".net", CRITICAL); + return; + } + + if (!entity.HasComponent()) + { + Logger::Log("Failed to register entity .net script: Entity doesn't have a .net script component.", ".net", CRITICAL); + return; + } + + auto& component = entity.GetComponent(); + const auto& filePath = component.ScriptPath; + if (filePath.empty()) + { + Logger::Log("Skipped .net entity script since it was empty.", ".net", VERBOSE); + return; + } + + if (!FileSystem::FileExists(filePath)) + { + Logger::Log("Skipped .net entity script: The file path doesn't exist.", ".net", WARNING); + return; + } + + // We can now scan the file and look for this pattern: class XXXXX : Entity + // Warning, this doesnt do any bound check so if there is a semicolon at the end + // of the file. IT MIGHT CRASH HERE. POTENTIALLY - I have not tested. + // ----------------------------------------------------- + std::string fileContent = FileSystem::ReadFile(filePath); + fileContent = fileContent.substr(3, fileContent.size()); + fileContent = String::RemoveWhiteSpace(fileContent); + + // Find class token + size_t classTokenPos = fileContent.find("class"); + if (classTokenPos == std::string::npos) + { + Logger::Log("Skipped .net entity script: file doesnt contain entity class.", ".net", WARNING); + return; + } + + size_t classNameStartIndex = classTokenPos + 5; // 4 letter: class + 1 for next char + + // Find semi-colon token + size_t semiColonPos = fileContent.find(":"); + if (semiColonPos == std::string::npos || semiColonPos < classTokenPos) + { + Logger::Log("Skipped .net entity script: Not class inheriting Entity was found.", ".net", WARNING); + return; + } + + size_t classNameLength = semiColonPos - classNameStartIndex; + + const std::string className = fileContent.substr(classNameStartIndex, classNameLength); + + if(m_GameEntityTypes.find(className) == m_GameEntityTypes.end()) + { + // The class name parsed in the file was not found in the game's DLL. + const std::string& msg = "Skipped .net entity script: \n Class: " + + className + " not found in " + std::string(m_GameAssembly.GetName()); + Logger::Log(msg, ".net", CRITICAL); + return; + } + + auto classInstance = m_GameEntityTypes[className]->CreateInstance(); + m_EntityToManagedObjects.emplace(entity.GetID(), classInstance); + } + + Coral::ManagedObject ScriptingEngineNet::GetEntityScript(const Entity& entity) + { + if (!HasEntityScriptInstance(entity)) + { + Logger::Log("Failed to get entity .Net script instance, doesn't exist", ".net", CRITICAL); + throw std::exception("Failed to get entity .Net script instance, doesn't exist"); + } + + return m_EntityToManagedObjects[entity.GetID()]; + } + + bool ScriptingEngineNet::HasEntityScriptInstance(const Entity& entity) + { + return m_EntityToManagedObjects.find(entity.GetID()) != m_EntityToManagedObjects.end(); + } + + void ScriptingEngineNet::GenerateSolution(const std::string& path, const std::string& projectName) + { + // Create .Net directory in projects folder. + const auto netDirectionPath = '/' + m_NetDirectory + '/'; + const auto netDir = path + netDirectionPath; + if (!FileSystem::DirectoryExists(netDirectionPath)) + { + FileSystem::MakeDirectory(netDirectionPath); + } + + // Copy engine assembly to projects folder. + const std::vector dllToCopy = + { + m_EngineAssemblyName + }; + + for (const auto& fileToCopy : dllToCopy) + { + std::filesystem::copy_file(fileToCopy, netDir + fileToCopy, std::filesystem::copy_options::overwrite_existing); + } + + // Generate premake5 templates + // ---------------------------------------- + const std::string cleanProjectName = String::Sanitize(projectName); + const std::string premakeScript = R"( +workspace ")" + cleanProjectName + R"(" + project ")" + cleanProjectName + R"(" + language "C#" + dotnetframework "net7.0" + + kind "SharedLib" + clr "Unsafe" + + -- Don't specify architecture here. (see https://github.com/premake/premake-core/issues/1758) + + files + { + "**.cs" + } + + links + { + ".net/NuakeNet" + } +)"; + + // Writting premake5 templates in project's directory + // ---------------------------------------- + FileSystem::BeginWriteFile("premake5.lua"); + FileSystem::WriteLine(premakeScript); + FileSystem::EndWriteFile(); + + // Execute premake script, generating .sln + OS::ExecuteCommand("cd " + path + " && premake5 vs2022"); + + // Open solution file in visual studio + OS::OpenIn(path + cleanProjectName + ".sln"); + + // Delete premake script + FileSystem::DeleteFileFromPath(FileSystem::Root + "/premake5.lua"); + } + + void ScriptingEngineNet::CreateEntityScript(const std::string & path, const std::string& entityName) + { + const std::string scriptTemplate = R"( +using )" + m_Scope + R"(; + +namespace NuakeShowcase +{ + class )" + entityName + R"( +} +)"; + } + } \ No newline at end of file diff --git a/Nuake/src/Scripting/ScriptingEngineNet.h b/Nuake/src/Scripting/ScriptingEngineNet.h index f05e2e0b..9ada2409 100644 --- a/Nuake/src/Scripting/ScriptingEngineNet.h +++ b/Nuake/src/Scripting/ScriptingEngineNet.h @@ -1,22 +1,44 @@ #pragma once #include "src/Core/Core.h" +#include "src/Scene/Entities/Entity.h" - - +#include "src/Scripting/NetModules/NetAPIModule.h" namespace Coral { class HostInstance; - class AssemblyLoadContext; + class Type; } -namespace Nuake -{ +#include + +namespace Nuake { + + class Project; + class ScriptingEngineNet { private: + const std::string m_Scope = "Nuake.Net"; + const std::string m_EngineAssemblyName = "NuakeNet.dll"; + const std::string m_NetDirectory = ".net"; + const std::string m_ContextName = "NuakeEngineContext"; + Coral::HostInstance* m_HostInstance; - Coral::AssemblyLoadContext* m_LoadContext; + Coral::AssemblyLoadContext m_LoadContext; + + std::unordered_map m_LoadedAssemblies; + std::vector> m_Modules; + + Coral::ManagedAssembly m_NuakeAssembly; // Nuake DLL + Coral::ManagedAssembly m_GameAssembly; // Game DLL + + // This is a map of all the entity scripts detected in the game's assembly. + // This is filled when loading the game's assembly. + + // This is a map that contains all the instances of entity scripts. + std::unordered_map m_GameEntityTypes; + std::unordered_map m_EntityToManagedObjects; ScriptingEngineNet(); ~ScriptingEngineNet(); @@ -25,5 +47,15 @@ namespace Nuake static ScriptingEngineNet& Get(); void Initialize(); + void Uninitialize(); + + void LoadProjectAssembly(Ref project); + + void RegisterEntityScript(Entity& entity); + Coral::ManagedObject GetEntityScript(const Entity& entity); + bool HasEntityScriptInstance(const Entity& entity); + + void GenerateSolution(const std::string& path, const std::string& projectName); + void CreateEntityScript(const std::string& path, const std::string& entityName); }; } \ No newline at end of file diff --git a/NuakeNet/premake5.lua b/NuakeNet/premake5.lua index dda01987..567d0b61 100644 --- a/NuakeNet/premake5.lua +++ b/NuakeNet/premake5.lua @@ -19,4 +19,9 @@ project "NuakeNet" links { "Coral.Managed" - } \ No newline at end of file + } + + postbuildcommands { + '{ECHO} Copying "%{wks.location}/NuakeNet/bin/%{cfg.buildcfg}/NuakeNet.dll" to "%{wks.location}/Editor"', + '{COPYFILE} "%{wks.location}/NuakeNet/bin/%{cfg.buildcfg}/NuakeNet.dll" "%{wks.location}/Editor"' + } diff --git a/NuakeNet/src/main.cs b/NuakeNet/src/main.cs index 9affc8f4..8b48ada0 100644 --- a/NuakeNet/src/main.cs +++ b/NuakeNet/src/main.cs @@ -13,7 +13,6 @@ namespace Nuake.Net public class Engine { internal static unsafe delegate* LoggerLogIcall; - public Engine() { } @@ -21,9 +20,9 @@ namespace Nuake.Net /// Prints a message to the console log /// /// message to be printed - public void Log(NativeString input) + public static void Log(string input) { - unsafe { LoggerLogIcall(input + " - hello from c# man"); } + unsafe { LoggerLogIcall(input); } } } @@ -64,17 +63,12 @@ namespace Nuake.Net public UInt32 ID { get; private set; } - Entity(UInt32 id) - { - ID = id; - } - public virtual void OnInit() { } public virtual void OnUpdate(float dt) { } public virtual void OnFixedUpdate(float dt) { } public virtual void OnDestroy() { } - bool HasComponent() + public bool HasComponent() { if (typeof(T) == typeof(TransformComponent)) { @@ -88,7 +82,7 @@ namespace Nuake.Net return false; } - T GetComponent() + public T GetComponent() { if (typeof(T) == typeof(TransformComponent)) {