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:
Antoine Pilote
2023-10-16 00:21:28 -04:00
parent 9807f0fa8f
commit af2546fb21
21 changed files with 677 additions and 50 deletions

View File

@@ -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"(
}
)";
}
}