C# Entity Scrips now working(OnInit, update, destroy) [ Broke reloading]
- added UI button to generate a .sln in the games folder - added UI for adding new component and drag n drop .cs file - added modular Core API module system
This commit is contained in:
@@ -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 <Coral/HostInstance.hpp>
|
||||
#include <Coral/GC.hpp>
|
||||
#include <Coral/NativeArray.hpp>
|
||||
#include <Coral/Attribute.hpp>
|
||||
#include <src/Scene/Components/NetScriptComponent.h>
|
||||
|
||||
|
||||
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<EngineNetAPI>()
|
||||
};
|
||||
|
||||
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<void*>(&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> 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<NetScriptComponent>())
|
||||
{
|
||||
Logger::Log("Failed to register entity .net script: Entity doesn't have a .net script component.", ".net", CRITICAL);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& component = entity.GetComponent<NetScriptComponent>();
|
||||
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<std::string> 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"(
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user