SHADE_Y3/SHADE_Engine/src/Scripting/SHScriptEngine.cpp

507 lines
17 KiB
C++
Raw Normal View History

/************************************************************************************//*!
\file SHScriptEngine.cpp
\author Tng Kah Wei, kahwei.tng, 390009620
\par email: kahwei.tng\@digipen.edu
\date Sep 17, 2021
\brief Contains the implementation for ScriptEngine class.
Copyright (C) 2021 DigiPen Institute of Technology.
Reproduction or disclosure of this file or its contents without the prior written consent
of DigiPen Institute of Technology is prohibited.
*//*************************************************************************************/
// Precompiled Headers
#include <SHpch.h>
// Primary Header
#include "SHScriptEngine.h"
// Standard Library
#include <fstream> // std::fstream
#include <filesystem> // std::filesystem::canonical, std::filesystem::remove
// Project Headers
#include "Tools/SHLogger.h"
#include "Tools/SHStringUtils.h"
namespace SHADE
{
/*--------------------------------------------------------------------------------*/
/* Static Definitions */
/*--------------------------------------------------------------------------------*/
2022-09-14 20:26:31 +08:00
const std::string SHScriptEngine::DEFAULT_CSHARP_NAMESPACE = std::string("SHADE");
SHDotNetRuntime SHScriptEngine::dotNet { false };
SHScriptEngine::CsFuncPtr SHScriptEngine::csEngineInit = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csEngineLoadScripts = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csEngineUnloadScripts = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csEngineReloadScripts = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csEngineExit = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csScriptsFrameSetUp = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csScriptsExecuteFixedUpdate = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csScriptsExecuteUpdate = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csScriptsExecuteLateUpdate = nullptr;
SHScriptEngine::CsFuncPtr SHScriptEngine::csScriptsFrameCleanUp = nullptr;
SHScriptEngine::CsScriptManipFuncPtr SHScriptEngine::csScriptsAdd = nullptr;
SHScriptEngine::CsScriptBasicFuncPtr SHScriptEngine::csScriptsRemoveAll = nullptr;
SHScriptEngine::CsScriptOptionalFuncPtr SHScriptEngine::csScriptsRemoveAllImmediately = nullptr;
SHScriptEngine::CsScriptSerialiseFuncPtr SHScriptEngine::csScriptsSerialise = nullptr;
SHScriptEngine::CsScriptDeserialiseFuncPtr SHScriptEngine::csScriptDeserialise = nullptr;
SHScriptEngine::CsScriptSerialiseYamlFuncPtr SHScriptEngine::csScriptsSerialiseYaml = nullptr;
SHScriptEngine::CsScriptSerialiseYamlFuncPtr SHScriptEngine::csScriptDeserialiseYaml = nullptr;
SHScriptEngine::CsScriptEditorFuncPtr SHScriptEngine::csEditorRenderScripts = nullptr;
/*---------------------------------------------------------------------------------*/
/* Lifecycle Functions */
/*---------------------------------------------------------------------------------*/
void SHScriptEngine::Init()
{
// Do not allow initialization if already initialised
if (dotNet.IsLoaded())
{
SHLOG_ERROR("[ScriptEngine] Attempted to initialise an already loaded DotNetRuntime.");
return;
}
dotNet.Init();
// Load all the helpers
loadFunctions();
// Generate script assembly if it hasn't been before
if (!fileExists(std::string(MANAGED_SCRIPT_LIB_NAME) + ".dll"))
{
BuildScriptAssembly();
}
// Initialise the CSharp Engine
csEngineInit();
// Link events
// - Entity Destruction
/*onEntityDestroy = [this](const SHEntity& e)
{
csScriptsRemoveAll(e.GetEID());
csGOLibNotifyDestroyEntity(e.GetEID());
};
ECS::OnEntityDestroy += onEntityDestroy;*/
}
void SHScriptEngine::UnloadScriptAssembly()
{
csEngineUnloadScripts();
}
void SHScriptEngine::LoadScriptAssembly()
{
csEngineLoadScripts();
}
void SHScriptEngine::ReloadScriptAssembly()
{
csEngineReloadScripts();
}
void SHScriptEngine::ExecuteFixedUpdates()
{
csScriptsExecuteFixedUpdate();
}
void SHScriptEngine::Exit()
{
// Do not allow deinitialization if not initialised
if (!dotNet.IsLoaded())
{
SHLOG_ERROR("[ScriptEngine] Attempted to clean up an unloaded DotNetRuntime.");
return;
}
// Unlink events
/*ECS::OnEntityCreated -= onEntityCreate;
ECS::OnEntityDestroy -= onEntityDestroy;*/
// Clean up the CSharp Engine
csEngineExit();
// Shut down the CLR
dotNet.Exit();
}
/*---------------------------------------------------------------------------------*/
/* Script Manipulation Functions */
/*---------------------------------------------------------------------------------*/
bool SHScriptEngine::AddScript(const SHEntity& entity, const std::string_view& scriptName)
{
return csScriptsAdd(entity.GetEID(), scriptName.data());
}
void SHScriptEngine::RemoveAllScripts(const SHEntity& entity)
{
csScriptsRemoveAll(entity.GetEID());
}
void SHScriptEngine::RemoveAllScriptsImmediately(const SHEntity& entity, bool callOnDestroy)
{
csScriptsRemoveAllImmediately(entity.GetEID(), callOnDestroy);
}
/*---------------------------------------------------------------------------------*/
/* Script Serialisation Functions */
/*---------------------------------------------------------------------------------*/
std::string SHScriptEngine::SerialiseScripts(const SHEntity& entity)
{
// Create buffer needed to store serialised script data
constexpr int BUFFER_SIZE = 10240;
std::unique_ptr<char> buffer { new char[BUFFER_SIZE] };
std::memset(buffer.get(), 0, BUFFER_SIZE);
// Attempt to serialise the script
std::string result;
if (csScriptsSerialise(entity.GetEID(), buffer.get(), BUFFER_SIZE))
{
result = std::string(buffer.get());
}
else
{
SHLOG_ERROR("[ScriptEngine] Failed to serialise scripts as string buffer is too small!");
}
// Return an empty string since we failed to serialise
return result;
}
/*---------------------------------------------------------------------------------*/
/* Script Serialisation Functions */
/*---------------------------------------------------------------------------------*/
void SHScriptEngine::DeserialiseScript(const SHEntity& entity, const std::string& yaml)
{
csScriptDeserialise(entity.GetEID(), yaml.c_str());
}
/*---------------------------------------------------------------------------------*/
/* Script Editor Functions */
/*---------------------------------------------------------------------------------*/
void SHScriptEngine::RenderScriptsInInspector(const SHEntity& entity)
{
csEditorRenderScripts(entity.GetEID());
}
/*---------------------------------------------------------------------------------*/
/* Static Utility Functions */
/*---------------------------------------------------------------------------------*/
bool SHScriptEngine::BuildScriptAssembly(bool debug)
{
constexpr std::string_view BUILD_LOG_PATH = "../Build.log";
// Generate csproj file if it doesn't exist
static const std::filesystem::path CSPROJ_PATH = "../SHADE_Scripting.csproj";
if (!std::filesystem::exists(CSPROJ_PATH))
{
GenerateScriptsCsProjFile(CSPROJ_PATH);
}
// Prepare directory (delete useless files)
deleteFolder("net5.0");
deleteFolder("ref");
2022-09-14 20:26:31 +08:00
deleteFolder("../SHADE_Scripting");
deleteFolder("../obj");
// Attempt to build the assembly
std::ostringstream oss;
oss << "[ScriptEngine] Building " << (debug ? " debug " : "") << "Managed Script Assembly (" << MANAGED_SCRIPT_LIB_NAME << ")!";
SHLOG_INFO(oss.str());
oss.str("");
const bool BUILD_SUCCESS = execProcess
(
L"C:\\Windows\\system32\\cmd.exe",
2022-09-14 20:26:31 +08:00
L"/K \"dotnet build \"../SHADE_Scripting.csproj\" -c Debug -o \"./tmp/\" -fl -flp:LogFile=build.log;Verbosity=quiet & exit\""
) == 0;
if (BUILD_SUCCESS)
{
// Copy to built dll to the working directory and replace
std::filesystem::copy_file("./tmp/SHADE_Scripting.dll", "SHADE_Scripting.dll", std::filesystem::copy_options::overwrite_existing);
oss << "[ScriptEngine] Successfully built Managed Script Assembly (" << MANAGED_SCRIPT_LIB_NAME << ")!";
SHLOG_INFO(oss.str());
}
else
{
oss << "[ScriptEngine] Failed to build Managed Script Assembly (" << MANAGED_SCRIPT_LIB_NAME << ")!";
SHLOG_ERROR(oss.str());
}
// Clean up built files
deleteFolder("./tmp");
// Read the build log and output to the console
dumpBuildLog(BUILD_LOG_PATH);
// Delete the build log file since we no longer need it
deleteFile(BUILD_LOG_PATH);
return BUILD_SUCCESS;
}
void SHScriptEngine::GenerateScriptsCsProjFile(const std::filesystem::path& path)
{
// Sample
static std::string_view FILE_CONTENTS =
"<Project Sdk=\"Microsoft.NET.Sdk\">\n\
<PropertyGroup>\n\
<TargetFramework>net5.0</TargetFramework>\n\
<Platforms>x64</Platforms>\n\
<Configurations>Release;Debug</Configurations>\n\
</PropertyGroup>\n\
<PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Release|x64'\">\n\
<OutputPath>.\\bin_Release-x64</OutputPath>\n\
<PlatformTarget>x64</PlatformTarget>\n\
</PropertyGroup>\n\
<PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='Debug|x64'\"> \n\
<OutputPath>.\\bin_Debug-x64</OutputPath>\n\
<PlatformTarget>x64</PlatformTarget>\n\
<DefineConstants>DEBUG;TRACE</DefineConstants>\n\
<Optimize>false</Optimize>\n\
<DebugType>full</DebugType>\n\
<DebugSymbols>true</DebugSymbols>\n\
</PropertyGroup>\n\
<ItemGroup>\n\
2022-09-13 11:43:49 +08:00
<Compile Remove=\"bin\\**\" />\n\
<EmbeddedResource Remove=\"Assets\\**\" />\n\
<EmbeddedResource Remove=\"bin\\**\" />\n\
<None Remove=\"bin\\**\" />\n\
</ItemGroup>\n\
<ItemGroup>\n\
<None Remove=\".gitignore\" />\n\
<None Remove=\".gitmodules\" />\n\
</ItemGroup>\n\
<ItemGroup>\n\
2022-09-14 20:26:31 +08:00
<Reference Include=\"SHADE_Managed\">\n\
<HintPath>.\\bin\\SHADE_Managed.dll</HintPath>\n\
</Reference>\n\
</ItemGroup>\n\
</Project>";
// Attempt to create the file
std::ofstream file(path);
if (!file.is_open())
throw std::runtime_error("Unable to create CsProj file!");
// Fill the file
file << FILE_CONTENTS;
// Close
file.close();
}
/*---------------------------------------------------------------------------------*/
/* Helper Functions */
/*---------------------------------------------------------------------------------*/
void SHScriptEngine::loadFunctions()
{
std::ostringstream oss;
oss << "[ScriptEngine] Loading \"" << DEFAULT_CSHARP_LIB_NAME << "\" CLR library.";
SHLOG_INFO(oss.str());
// Load functions
csEngineInit = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".EngineInterface",
"Init"
);
csEngineLoadScripts = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".EngineInterface",
"LoadScriptAssembly"
);
csEngineUnloadScripts = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".EngineInterface",
"UnloadScriptAssembly"
);
csEngineReloadScripts = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".EngineInterface",
"ReloadScriptAssembly"
);
csEngineExit = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".EngineInterface",
"Exit"
);
csScriptsFrameSetUp = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"FrameSetUp"
);
csScriptsExecuteFixedUpdate = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"ExecuteFixedUpdate"
);
csScriptsExecuteUpdate = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"ExecuteUpdate"
);
csScriptsExecuteLateUpdate = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"ExecuteLateUpdate"
);
csScriptsFrameCleanUp = dotNet.GetFunctionPtr<CsFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"FrameCleanUp"
);
csScriptsAdd = dotNet.GetFunctionPtr<CsScriptManipFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"AddScriptViaName"
);
csScriptsRemoveAll = dotNet.GetFunctionPtr<CsScriptBasicFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"RemoveAllScripts"
);
csScriptsRemoveAllImmediately = dotNet.GetFunctionPtr<CsScriptOptionalFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"RemoveAllScriptsImmediately"
);
/*csScriptsSerialise = dotNet.GetFunctionPtr<CsScriptSerialiseFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"SerialiseScripts"
);
csScriptsSerialiseYaml = dotNet.GetFunctionPtr<CsScriptSerialiseYamlFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"SerialiseScriptsYaml"
);
csScriptDeserialise = dotNet.GetFunctionPtr<CsScriptDeserialiseFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"DeserialiseScript"
);
csScriptDeserialiseYaml = dotNet.GetFunctionPtr<CsScriptSerialiseYamlFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".ScriptStore",
"SerialiseScriptsYaml"
);
csEditorRenderScripts = dotNet.GetFunctionPtr<CsScriptEditorFuncPtr>
(
DEFAULT_CSHARP_LIB_NAME,
DEFAULT_CSHARP_NAMESPACE + ".Editor",
"RenderScriptsInInspector"
);*/
}
void SHScriptEngine::dumpBuildLog(const std::string_view& buildLogPath)
{
std::ifstream buildLog(buildLogPath);
// Fail to open
if (!buildLog.is_open())
return;
// Process line by line
std::string line;
while (std::getline(buildLog, line))
{
if (line.find("error") != line.npos)
{
SHLOG_ERROR(line);
}
else
{
SHLOG_WARNING(line);
}
}
}
void SHScriptEngine::deleteFile(const std::string_view& filePath)
{
try
{
std::filesystem::remove(std::filesystem::canonical(filePath));
}
catch (...) {} // Ignore deletion failures
}
void SHScriptEngine::deleteFolder(const std::string_view& filePath)
{
try
{
std::filesystem::remove_all(std::filesystem::canonical(filePath));
}
catch (...) {} // Ignore deletion failures
}
bool SHScriptEngine::fileExists(const std::string_view& filePath)
{
std::error_code error;
if (std::filesystem::exists(filePath, error))
{
return true;
}
return false;
}
DWORD SHScriptEngine::execProcess(const std::wstring& path, const std::wstring& args)
{
STARTUPINFOW startInfo;
PROCESS_INFORMATION procInfo;
ZeroMemory(&startInfo, sizeof(startInfo));
ZeroMemory(&procInfo, sizeof(procInfo));
startInfo.cb = sizeof(startInfo);
std::wstring argsWstr = args;
// Start Process
const auto SUCCESS = CreateProcess
(
path.data(), argsWstr.data(),
nullptr, nullptr, false, NULL, nullptr, nullptr,
&startInfo, &procInfo
);
// Error Check
if (!SUCCESS)
{
auto err = GetLastError();
std::ostringstream oss;
oss << "[ScriptEngine] Failed to launch process. Error code: " << std::hex << err
<< " (" << SHStringUtils::GetWin32ErrorMessage(err) << ")";
throw std::runtime_error(oss.str());
}
// Wait for execution to end
DWORD status;
while (true)
{
const auto SUCCESS = GetExitCodeProcess(procInfo.hProcess, &status);
if (!SUCCESS)
{
auto err = GetLastError();
std::ostringstream oss;
oss << "[ScriptEngine] Failed to query process. Error code: " << std::hex << err
<< " (" << SHStringUtils::GetWin32ErrorMessage(err) << ")";
throw std::runtime_error(oss.str());
}
// Break only if process ends
if (status != STILL_ACTIVE)
{
CloseHandle(procInfo.hProcess);
CloseHandle(procInfo.hThread);
return status;
}
}
}
}