Files
FlaxEngine/Source/Engine/Scripting/Scripting.cpp

1131 lines
36 KiB
C++

// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
#include "BinaryModule.h"
#include "Scripting.h"
#include "ScriptingType.h"
#include "FlaxEngine.Gen.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Threading/ThreadLocal.h"
#include "Engine/Threading/IRunnable.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Debug/DebugLog.h"
#if USE_EDITOR
#include "Engine/Level/Level.h"
#include "Editor/Scripting/ScriptsBuilder.h"
#endif
#include "ManagedCLR/MAssembly.h"
#include "ManagedCLR/MClass.h"
#include "ManagedCLR/MMethod.h"
#include "ManagedCLR/MDomain.h"
#include "ManagedCLR/MCore.h"
#include "ManagedCLR/MException.h"
#include "Internal/StdTypesContainer.h"
#include "Engine/Core/ObjectsRemovalService.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Core/Types/Stopwatch.h"
#include "Engine/Content/Asset.h"
#include "Engine/Content/Content.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Engine/Time.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Profiler/ProfilerCPU.h"
extern void registerFlaxEngineInternalCalls();
class ScriptingService : public EngineService
{
public:
ScriptingService()
: EngineService(TEXT("Scripting"), -20)
{
}
bool Init() override;
void Update() override;
void LateUpdate() override;
void FixedUpdate() override;
void LateFixedUpdate() override;
void Draw() override;
void BeforeExit() override;
void Dispose() override;
};
namespace
{
MDomain* _rootDomain = nullptr;
MDomain* _scriptsDomain = nullptr;
CriticalSection _objectsLocker;
#define USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING 0
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
struct ScriptingObjectData
{
ScriptingObject* Ptr;
StringAnsi TypeName;
ScriptingObjectData()
{
Ptr = nullptr;
}
ScriptingObjectData(ScriptingObject* ptr)
{
Ptr = ptr;
if (ptr && ptr->GetTypeHandle() && ptr->GetTypeHandle().TypeIndex < ptr->GetTypeHandle().Module->Types.Count())
TypeName = ptr->GetType().Fullname;
}
ScriptingObject* operator->() const
{
return Ptr;
}
explicit operator ScriptingObject*()
{
return Ptr;
}
operator ScriptingObject*() const
{
return Ptr;
}
};
Dictionary<Guid, ScriptingObjectData> _objectsDictionary(1024 * 16);
#else
Dictionary<Guid, ScriptingObject*> _objectsDictionary(1024 * 16);
#endif
bool _isEngineAssemblyLoaded = false;
bool _hasGameModulesLoaded = false;
MMethod* _method_Update = nullptr;
MMethod* _method_LateUpdate = nullptr;
MMethod* _method_FixedUpdate = nullptr;
MMethod* _method_LateFixedUpdate = nullptr;
MMethod* _method_Draw = nullptr;
MMethod* _method_Exit = nullptr;
Dictionary<StringAnsi, BinaryModule*, InlinedAllocation<64>> _nonNativeModules;
#if USE_EDITOR
bool LastBinariesLoadTriggeredCompilation = false;
#endif
void ReleaseObjects(bool gameOnly)
{
// Flush objects already enqueued objects to delete
ObjectsRemovalService::Flush();
// Give GC a try to cleanup old user objects and the other mess
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Destroy objects from game assemblies (eg. not released objects that might crash if persist in memory after reload)
const auto flaxModule = GetBinaryModuleFlaxEngine();
_objectsLocker.Lock();
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
auto obj = i->Value;
if (gameOnly && obj->GetTypeHandle().Module == flaxModule)
continue;
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[OnScriptingDispose] obj = 0x{0:x}, {1}", (uint64)obj.Ptr, String(obj.TypeName));
#endif
obj->OnScriptingDispose();
}
_objectsLocker.Unlock();
// Release assets sourced from game assemblies
Array<Asset*> assets = Content::GetAssets();
for (auto asset : assets)
{
if (asset->GetTypeHandle().Module == flaxModule)
{
continue;
}
asset->DeleteObject();
}
ObjectsRemovalService::Flush();
}
}
Delegate<BinaryModule*> Scripting::BinaryModuleLoaded;
Action Scripting::ScriptsLoaded;
Action Scripting::ScriptsUnload;
Action Scripting::ScriptsReloading;
Action Scripting::ScriptsReloaded;
ThreadLocal<Scripting::IdsMappingTable*, PLATFORM_THREADS_LIMIT> Scripting::ObjectsLookupIdMapping;
ScriptingService ScriptingServiceInstance;
bool initFlaxEngine();
// Assembly events
void onEngineLoaded(MAssembly* assembly);
void onEngineUnloading(MAssembly* assembly);
bool ScriptingService::Init()
{
Stopwatch stopwatch;
// Initialize managed runtime
if (MCore::LoadEngine())
{
LOG(Fatal, "C# runtime initialization failed.");
return true;
}
// Cache root domain
_rootDomain = MCore::GetRootDomain();
#if USE_SCRIPTING_SINGLE_DOMAIN
// Use single root domain
auto domain = _rootDomain;
#else
// Create Mono domain for scripts
auto domain = MCore::CreateDomain("Scripts Domain");
#endif
domain->SetCurrentDomain(true);
_scriptsDomain = domain;
// Add internal calls
registerFlaxEngineInternalCalls();
// Load assemblies
if (Scripting::Load())
{
LOG(Fatal, "Scripting Engine initialization failed.");
return true;
}
stopwatch.Stop();
LOG(Info, "Scripting Engine initializated! (time: {0}ms)", stopwatch.GetMilliseconds());
return false;
}
#if COMPILE_WITHOUT_CSHARP
#define INVOKE_EVENT(name)
#else
#define INVOKE_EVENT(name) \
if (!_isEngineAssemblyLoaded) return; \
if (_method_##name == nullptr) \
{ \
MClass* mclass = Scripting::GetStaticClass(); \
if (mclass == nullptr) \
{ \
LOG(Fatal, "Missing Scripting class."); \
return; \
} \
_method_##name = mclass->GetMethod("Internal_" #name, 0); \
if (_method_##name == nullptr) \
{ \
LOG(Fatal, "Missing Scripting method " #name "."); \
return; \
} \
} \
MObject* exception = nullptr; \
_method_##name->Invoke(nullptr, nullptr, &exception); \
DebugLog::LogException(exception)
#endif
void ScriptingService::Update()
{
PROFILE_CPU_NAMED("Scripting::Update");
INVOKE_EVENT(Update);
#ifdef USE_NETCORE
// Force GC to run in background periodically to avoid large blocking collections causing hitches
if (Time::Update.TicksCount % 60 == 0)
{
MCore::GC::Collect(MCore::GC::MaxGeneration(), MGCCollectionMode::Forced, false, false);
}
#endif
}
void ScriptingService::LateUpdate()
{
PROFILE_CPU_NAMED("Scripting::LateUpdate");
INVOKE_EVENT(LateUpdate);
}
void ScriptingService::FixedUpdate()
{
PROFILE_CPU_NAMED("Scripting::FixedUpdate");
INVOKE_EVENT(FixedUpdate);
}
void ScriptingService::LateFixedUpdate()
{
PROFILE_CPU_NAMED("Scripting::LateFixedUpdate");
INVOKE_EVENT(LateFixedUpdate);
}
void ScriptingService::Draw()
{
PROFILE_CPU_NAMED("Scripting::Draw");
INVOKE_EVENT(Draw);
}
void ScriptingService::BeforeExit()
{
PROFILE_CPU_NAMED("Scripting::BeforeExit");
INVOKE_EVENT(Exit);
}
MDomain* Scripting::GetRootDomain()
{
return _rootDomain;
}
MDomain* Scripting::GetScriptsDomain()
{
return _scriptsDomain;
}
void Scripting::ProcessBuildInfoPath(String& path, const String& projectFolderPath)
{
if (path.IsEmpty())
return;
if (path.StartsWith(TEXT("$(EnginePath)")))
path = Globals::StartupFolder / path.Substring(14);
else if (path.StartsWith(TEXT("$(ProjectPath)")))
path = projectFolderPath / path.Substring(14);
else if (FileSystem::IsRelative(path))
path = projectFolderPath / path;
}
bool Scripting::LoadBinaryModules(const String& path, const String& projectFolderPath)
{
PROFILE_CPU_NAMED("LoadBinaryModules");
LOG(Info, "Loading binary modules from build info file {0}", path);
// Read file contents
Array<byte> fileData;
if (File::ReadAllBytes(path, fileData))
{
LOG(Error, "Failed to read file contents.");
return true;
}
// Parse Json data
rapidjson_flax::Document document;
{
PROFILE_CPU_NAMED("Json.Parse");
document.Parse((char*)fileData.Get(), fileData.Count());
}
if (document.HasParseError())
{
LOG(Error, "Failed to file contents.");
return true;
}
// TODO: validate Name, Platform, Architecture, Configuration from file
// Load all references
auto referencesMember = document.FindMember("References");
if (referencesMember != document.MemberEnd() && referencesMember->value.IsArray())
{
auto& referencesArray = referencesMember->value;
for (rapidjson::SizeType i = 0; i < referencesArray.Size(); i++)
{
auto& reference = referencesArray[i];
String referenceProjectPath = JsonTools::GetString(reference, "ProjectPath", String::Empty);
if (referenceProjectPath == TEXT("$(EnginePath)/Flax.flaxproj"))
continue; // Skip reference to engine
String referencePath = JsonTools::GetString(reference, "Path", String::Empty);
if (referenceProjectPath.IsEmpty() || referencePath.IsEmpty())
{
LOG(Error, "Empty reference.");
return true;
}
ProcessBuildInfoPath(referenceProjectPath, projectFolderPath);
ProcessBuildInfoPath(referencePath, projectFolderPath);
String referenceProjectFolderPath = StringUtils::GetDirectoryName(referenceProjectPath);
if (LoadBinaryModules(referencePath, referenceProjectFolderPath))
{
LOG(Error, "Failed to load reference.");
return true;
}
}
}
// Load all binary modules
auto binaryModulesMember = document.FindMember("BinaryModules");
if (binaryModulesMember != document.MemberEnd() && binaryModulesMember->value.IsArray())
{
auto& binaryModulesArray = binaryModulesMember->value;
for (rapidjson::SizeType i = 0; i < binaryModulesArray.Size(); i++)
{
auto& binaryModule = binaryModulesArray[i];
const auto nameMember = binaryModule.FindMember("Name");
if (nameMember == binaryModule.MemberEnd())
{
LOG(Error, "Failed to process file.");
return true;
}
String name = nameMember->value.GetText();
StringAnsi nameAnsi(nameMember->value.GetString(), nameMember->value.GetStringLength());
String nativePath = JsonTools::GetString(binaryModule, "NativePath", String::Empty);
String managedPath = JsonTools::GetString(binaryModule, "ManagedPath", String::Empty);
ProcessBuildInfoPath(nativePath, projectFolderPath);
ProcessBuildInfoPath(managedPath, projectFolderPath);
LOG(Info, "Loading binary module {0}", name);
// Check if that module has been already registered
BinaryModule* module = BinaryModule::GetModule(nameAnsi);
if (!module)
_nonNativeModules.TryGet(nameAnsi, module);
if (!module)
{
// C++
if (nativePath.HasChars())
{
// Check if this module has been statically linked
auto& staticallyLinkedBinaryModules = StaticallyLinkedBinaryModuleInitializer::GetStaticallyLinkedBinaryModules();
for (auto getter : staticallyLinkedBinaryModules)
{
module = getter();
if (module && module->GetName() == nameAnsi)
break;
module = nullptr;
}
if (!module)
{
// Load library
Stopwatch stopwatch;
#if PLATFORM_ANDROID || PLATFORM_MAC
// On some platforms all native binaries are side-by-side with the app in a different folder
if (!FileSystem::FileExists(nativePath))
{
nativePath = String(StringUtils::GetDirectoryName(Platform::GetExecutableFilePath())) / StringUtils::GetFileName(nativePath);
}
#elif PLATFORM_IOS
// iOS uses Frameworks folder with native binaries
if (!FileSystem::FileExists(nativePath))
{
nativePath = Globals::ProjectFolder / TEXT("Frameworks") / StringUtils::GetFileName(nativePath);
}
#endif
auto library = Platform::LoadLibrary(nativePath.Get());
if (!library)
{
LOG(Error, "Failed to load library '{0}' for binary module {1}.", nativePath, name);
return true;
}
char getBinaryFuncName[512];
StringAnsiView getBinaryFuncNamePrefix("GetBinaryModule");
ASSERT(getBinaryFuncNamePrefix.Length() + nameAnsi.Length() < ARRAY_COUNT(getBinaryFuncName));
Platform::MemoryCopy(getBinaryFuncName, getBinaryFuncNamePrefix.Get(), getBinaryFuncNamePrefix.Length());
Platform::MemoryCopy(getBinaryFuncName + getBinaryFuncNamePrefix.Length(), nameAnsi.Get(), nameAnsi.Length());
*(getBinaryFuncName + getBinaryFuncNamePrefix.Length() + nameAnsi.Length()) = '\0';
const auto getBinaryFunc = (GetBinaryModuleFunc)Platform::GetProcAddress(library, getBinaryFuncName);
if (!getBinaryFunc)
{
Platform::FreeLibrary(library);
LOG(Error, "Failed to setup library '{0}' for binary module {1}.", nativePath, name);
return true;
}
stopwatch.Stop();
LOG(Info, "Module {0} loaded in {1}ms", name, stopwatch.GetMilliseconds());
// Get the binary module
module = getBinaryFunc();
if (!module)
{
Platform::FreeLibrary(library);
LOG(Error, "Failed to get binary module {0}.", name);
return true;
}
((NativeBinaryModule*)module)->Library = library;
}
}
else
{
// Create module if native library is not used
module = New<ManagedBinaryModule>(nameAnsi);
_nonNativeModules.Add(nameAnsi, module);
}
}
#if !COMPILE_WITHOUT_CSHARP
// C#
if (managedPath.HasChars() && !((ManagedBinaryModule*)module)->Assembly->IsLoaded())
{
if (((ManagedBinaryModule*)module)->Assembly->Load(managedPath, nativePath))
{
LOG(Error, "Failed to load C# assembly '{0}' for binary module {1}.", managedPath, name);
return true;
}
}
#endif
BinaryModuleLoaded(module);
}
}
return false;
}
bool Scripting::Load()
{
PROFILE_CPU();
// Note: this action can be called from main thread (due to Mono problems with assemblies actions from other threads)
ASSERT(IsInMainThread());
ScopeLock lock(BinaryModule::Locker);
#if USE_CSHARP
// Load C# core assembly
ManagedBinaryModule* corlib = GetBinaryModuleCorlib();
if (corlib->Assembly->LoadCorlib())
{
LOG(Error, "Failed to load corlib C# assembly.");
return true;
}
// Initialize C# corelib types
{
const auto& corlibClasses = corlib->Assembly->GetClasses();
bool gotAll = true;
#define CACHE_CORLIB_CLASS(var, name) gotAll &= corlibClasses.TryGet(StringAnsiView(name), MCore::TypeCache::var)
CACHE_CORLIB_CLASS(Void, "System.Void");
CACHE_CORLIB_CLASS(Object, "System.Object");
CACHE_CORLIB_CLASS(Byte, "System.Byte");
CACHE_CORLIB_CLASS(Boolean, "System.Boolean");
CACHE_CORLIB_CLASS(SByte, "System.SByte");
CACHE_CORLIB_CLASS(Char, "System.Char");
CACHE_CORLIB_CLASS(Int16, "System.Int16");
CACHE_CORLIB_CLASS(UInt16, "System.UInt16");
CACHE_CORLIB_CLASS(Int32, "System.Int32");
CACHE_CORLIB_CLASS(UInt32, "System.UInt32");
CACHE_CORLIB_CLASS(Int64, "System.Int64");
CACHE_CORLIB_CLASS(UInt64, "System.UInt64");
CACHE_CORLIB_CLASS(IntPtr, "System.IntPtr");
CACHE_CORLIB_CLASS(UIntPtr, "System.UIntPtr");
CACHE_CORLIB_CLASS(Single, "System.Single");
CACHE_CORLIB_CLASS(Double, "System.Double");
CACHE_CORLIB_CLASS(String, "System.String");
#undef CACHE_CORLIB_CLASS
if (!gotAll)
{
LOG(Error, "Failed to load corlib C# assembly.");
for (const auto& e : corlibClasses)
LOG(Info, "Class: {0}", String(e.Value->GetFullName()));
return true;
}
}
#endif
// Load FlaxEngine
const String flaxEnginePath = Globals::BinariesFolder / TEXT("FlaxEngine.CSharp.dll");
auto* flaxEngineModule = (NativeBinaryModule*)GetBinaryModuleFlaxEngine();
if (!flaxEngineModule->Assembly->IsLoaded())
{
if (flaxEngineModule->Assembly->Load(flaxEnginePath))
{
LOG(Error, "Failed to load FlaxEngine C# assembly.");
return true;
}
onEngineLoaded(flaxEngineModule->Assembly);
// Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types)
// TODO: add support for automatic typedef aliases setup for scripting module to properly lookup type from the alias typename
#if USE_LARGE_WORLDS
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double2"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double3"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double4"];
#else
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float2"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float3"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float4"];
#endif
#if USE_CSHARP
flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector2")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"];
flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector3")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"];
flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector4")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"];
#endif
}
#if USE_EDITOR
// Skip loading game modules in Editor on startup - Editor loads them later during splash screen (eg. after first compilation)
static bool SkipFirstLoad = true;
if (SkipFirstLoad)
{
SkipFirstLoad = false;
return false;
}
// Flax.Build outputs the <target>.Build.json with binary modules to use for game scripting
const Char *target, *platform, *architecture, *configuration;
ScriptsBuilder::GetBinariesConfiguration(target, platform, architecture, configuration);
if (StringUtils::Length(target) == 0)
{
LOG(Info, "Missing EditorTarget in project. Not using game script modules.");
_hasGameModulesLoaded = true;
return false;
}
const String targetBuildInfo = Globals::ProjectFolder / TEXT("Binaries") / target / platform / architecture / configuration / target + TEXT(".Build.json");
// Call compilation if game target build info is missing
if (!FileSystem::FileExists(targetBuildInfo))
{
LOG(Info, "Missing target build info ({0})", targetBuildInfo);
if (LastBinariesLoadTriggeredCompilation)
return false;
LastBinariesLoadTriggeredCompilation = true;
ScriptsBuilder::Compile();
return false;
}
#else
const String targetBuildInfo = Globals::BinariesFolder / TEXT("Game.Build.json");
#endif
// Load game binary modules
if (LoadBinaryModules(targetBuildInfo, Globals::ProjectFolder))
{
LOG(Error, "Failed to load Game assemblies.");
return true;
}
_hasGameModulesLoaded = true;
// End
ScriptsLoaded();
return false;
}
void Scripting::Release()
{
PROFILE_CPU();
// Note: this action can be called from main thread (due to Mono problems with assemblies actions from other threads)
ASSERT(IsInMainThread());
// Fire event
ScriptsUnload();
// Release managed objects instances for persistent objects (assets etc.)
ReleaseObjects(false);
auto* flaxEngineModule = (NativeBinaryModule*)GetBinaryModuleFlaxEngine();
onEngineUnloading(flaxEngineModule->Assembly);
// Unload assemblies (from back to front)
{
LOG(Info, "Unloading binary modules");
auto modules = BinaryModule::GetModules();
for (int32 i = modules.Count() - 1; i >= 0; i--)
{
auto module = modules[i];
module->Destroy(false);
}
_nonNativeModules.ClearDelete();
_hasGameModulesLoaded = false;
}
// Cleanup
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Flush objects
ObjectsRemovalService::Flush();
// Switch domain
auto rootDomain = MCore::GetRootDomain();
if (rootDomain)
{
if (!rootDomain->SetCurrentDomain(false))
{
LOG(Error, "Failed to set current domain to root");
}
}
#if !USE_SCRIPTING_SINGLE_DOMAIN
MCore::UnloadDomain("Scripts Domain");
#endif
}
#if USE_EDITOR
void Scripting::Reload(bool canTriggerSceneReload)
{
// By default we allow to call it only from the main thread and when no scene is loaded.
// Otherwise call scene manager to perform clear scripts reload.
// It will call this method back on main thread without scenes loaded, see SceneActionType::ReloadScripts.
if (!IsInMainThread() || Level::IsAnySceneLoaded())
{
if (canTriggerSceneReload)
{
// Call scene to reload scripts
Level::ReloadScriptsAsync();
}
else
{
LOG(Warning, "Cannot reload scene on scripting reload. Flag is not set.");
}
return;
}
PROFILE_CPU();
// Ideally we would call Release and Load but this would also reload Editor objects which we want avoid
// Editor is also referencing assets and other managed objects so we should force reload everything.
// However Reload is called when no scene is loaded.
// Faster path - if no game assembly loaded yet
if (!_hasGameModulesLoaded)
{
// Just load missing assemblies
Load();
return;
}
LOG(Info, "Start user scripts reload");
ScriptsReloading();
// Destroy objects from game assemblies (eg. not released objects that might crash if persist in memory after reload)
ReleaseObjects(true);
// Unload all game modules
LOG(Info, "Unloading game binary modules");
auto modules = BinaryModule::GetModules();
for (int32 i = modules.Count() - 1; i >= 0; i--)
{
BinaryModule* module = modules[i];
if (module == GetBinaryModuleCorlib() || module == GetBinaryModuleFlaxEngine())
continue;
module->Destroy(true);
}
modules.Clear();
_nonNativeModules.ClearDelete();
_hasGameModulesLoaded = false;
// Release and create a new assembly load context for user assemblies
MCore::ReloadScriptingAssemblyLoadContext();
// Give GC a try to cleanup old user objects and the other mess
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Load all game modules
if (Load())
{
LOG(Error, "User assemblies reload failed.");
}
ScriptsReloaded();
LOG(Info, "End user scripts reload");
}
#endif
Array<ScriptingObject*, HeapAllocation> Scripting::GetObjects()
{
Array<ScriptingObject*> objects;
_objectsLocker.Lock();
_objectsDictionary.GetValues(objects);
_objectsLocker.Unlock();
return objects;
}
MClass* Scripting::FindClass(const StringAnsiView& fullname)
{
if (fullname.IsEmpty())
return nullptr;
PROFILE_CPU();
auto& modules = BinaryModule::GetModules();
for (auto module : modules)
{
auto managedModule = dynamic_cast<ManagedBinaryModule*>(module);
if (managedModule && managedModule->Assembly->IsLoaded())
{
MClass* result = managedModule->Assembly->GetClass(fullname);
if (result != nullptr)
return result;
}
}
return nullptr;
}
ScriptingTypeHandle Scripting::FindScriptingType(const StringAnsiView& fullname)
{
if (fullname.IsEmpty())
return ScriptingTypeHandle();
PROFILE_CPU();
auto& modules = BinaryModule::GetModules();
for (auto module : modules)
{
int32 typeIndex;
if (module->FindScriptingType(fullname, typeIndex))
{
return ScriptingTypeHandle(module, typeIndex);
}
}
return ScriptingTypeHandle();
}
ScriptingObject* Scripting::NewObject(const ScriptingTypeHandle& type)
{
if (!type)
{
LOG(Error, "Invalid type.");
return nullptr;
}
const ScriptingType& scriptingType = type.GetType();
// Create unmanaged object
const ScriptingObjectSpawnParams params(Guid::New(), type);
ScriptingObject* obj = scriptingType.Script.Spawn(params);
if (obj == nullptr)
LOG(Error, "Failed to spawn object of type \'{0}\'.", scriptingType.ToString());
return obj;
}
ScriptingObject* Scripting::NewObject(const MClass* type)
{
if (type == nullptr)
{
LOG(Error, "Invalid type.");
return nullptr;
}
#if USE_CSHARP
// Get the assembly with that class
auto module = ManagedBinaryModule::FindModule(type);
if (module == nullptr)
{
LOG(Error, "Cannot find scripting assembly for type \'{0}\'.", String(type->GetFullName()));
return nullptr;
}
// Try to find the scripting type for this class
int32 typeIndex;
if (!module->ClassToTypeIndex.TryGet(type, typeIndex))
{
LOG(Error, "Cannot spawn objects of type \'{0}\'.", String(type->GetFullName()));
return nullptr;
}
const ScriptingType& scriptingType = module->Types[typeIndex];
// Create unmanaged object
const ScriptingObjectSpawnParams params(Guid::New(), ScriptingTypeHandle(module, typeIndex));
ScriptingObject* obj = scriptingType.Script.Spawn(params);
if (obj == nullptr)
LOG(Error, "Failed to spawn object of type \'{0}\'.", scriptingType.ToString());
return obj;
#else
LOG(Error, "Not supported object creation from Managed class.");
return nullptr;
#endif
}
FLAXENGINE_API ScriptingObject* FindObject(const Guid& id, MClass* type)
{
return Scripting::FindObject(id, type);
}
void ScriptingObjectReferenceBase::OnSet(ScriptingObject* object)
{
auto e = _object;
if (e != object)
{
if (e)
e->Deleted.Unbind<ScriptingObjectReferenceBase, &ScriptingObjectReferenceBase::OnDeleted>(this);
_object = e = object;
if (e)
e->Deleted.Bind<ScriptingObjectReferenceBase, &ScriptingObjectReferenceBase::OnDeleted>(this);
Changed();
}
}
void ScriptingObjectReferenceBase::OnDeleted(ScriptingObject* obj)
{
if (_object == obj)
{
_object->Deleted.Unbind<ScriptingObjectReferenceBase, &ScriptingObjectReferenceBase::OnDeleted>(this);
_object = nullptr;
Changed();
}
}
ScriptingObject* Scripting::FindObject(Guid id, const MClass* type)
{
if (!id.IsValid())
return nullptr;
PROFILE_CPU();
// Try to map object id
const auto idsMapping = ObjectsLookupIdMapping.Get();
if (idsMapping)
{
idsMapping->TryGet(id, id);
}
// Try to find it
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
ScriptingObjectData data;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, data);
_objectsLocker.Unlock();
auto result = data.Ptr;
#else
ScriptingObject* result = nullptr;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, result);
_objectsLocker.Unlock();
#endif
if (result)
{
// Check type
if (!type || result->Is(type))
return result;
LOG(Warning, "Found scripting object with ID={0} of type {1} that doesn't match type {2}.", id, String(result->GetType().Fullname), String(type->GetFullName()));
return nullptr;
}
// Check if object can be an asset and try to load it
if (!type)
{
result = Content::LoadAsync<Asset>(id);
if (!result)
LOG(Warning, "Unable to find scripting object with ID={0}", id);
return result;
}
if (type == ScriptingObject::GetStaticClass() || type->IsSubClassOf(Asset::GetStaticClass()))
{
Asset* asset = Content::LoadAsync(id, type);
if (asset)
return asset;
}
LOG(Warning, "Unable to find scripting object with ID={0}. Required type {1}.", id, String(type->GetFullName()));
return nullptr;
}
ScriptingObject* Scripting::TryFindObject(Guid id, const MClass* type)
{
if (!id.IsValid())
return nullptr;
PROFILE_CPU();
// Try to map object id
const auto idsMapping = ObjectsLookupIdMapping.Get();
if (idsMapping)
{
idsMapping->TryGet(id, id);
}
// Try to find it
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
ScriptingObjectData data;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, data);
_objectsLocker.Unlock();
auto result = data.Ptr;
#else
ScriptingObject* result = nullptr;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, result);
_objectsLocker.Unlock();
#endif
// Check type
if (result && type && !result->Is(type))
{
result = nullptr;
}
return result;
}
ScriptingObject* Scripting::TryFindObject(const MClass* type)
{
if (type == nullptr)
return nullptr;
ScopeLock lock(_objectsLocker);
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
const auto obj = i->Value;
if (obj->GetClass() == type)
return obj;
}
return nullptr;
}
ScriptingObject* Scripting::FindObject(const MObject* managedInstance)
{
if (managedInstance == nullptr)
return nullptr;
PROFILE_CPU();
// TODO: optimize it by reading the unmanagedPtr or _internalId from managed Object property
ScopeLock lock(_objectsLocker);
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
const auto obj = i->Value;
if (obj->GetManagedInstance() == managedInstance)
return obj;
}
return nullptr;
}
void Scripting::OnManagedInstanceDeleted(ScriptingObject* obj)
{
PROFILE_CPU();
ASSERT(obj);
// Validate if object still exists
_objectsLocker.Lock();
if (_objectsDictionary.ContainsValue(obj))
{
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[OnManagedInstanceDeleted] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName));
#endif
obj->OnManagedInstanceDeleted();
}
else
{
//LOG(Warning, "Object finalization called for already removed object (address={0:x})", (uint64)obj);
}
_objectsLocker.Unlock();
}
bool Scripting::HasGameModulesLoaded()
{
return _hasGameModulesLoaded;
}
bool Scripting::IsEveryAssemblyLoaded()
{
auto& modules = BinaryModule::GetModules();
for (BinaryModule* module : modules)
{
if (!module->IsLoaded())
return false;
}
return true;
}
bool Scripting::IsTypeFromGameScripts(const MClass* type)
{
const auto binaryModule = ManagedBinaryModule::GetModule(type ? type->GetAssembly() : nullptr);
return binaryModule && binaryModule != GetBinaryModuleCorlib() && binaryModule != GetBinaryModuleFlaxEngine();
}
void Scripting::RegisterObject(ScriptingObject* obj)
{
const Guid id = obj->GetID();
ScopeLock lock(_objectsLocker);
//ASSERT(!_objectsDictionary.ContainsValue(obj));
#if ENABLE_ASSERTION
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
ScriptingObjectData other;
if (_objectsDictionary.TryGet(id, other))
#else
ScriptingObject* other;
if (_objectsDictionary.TryGet(id, other))
#endif
{
// Something went wrong...
LOG(Error, "Objects registry already contains object with ID={0} (type '{3}')! Trying to register object {1} (type '{2}').", id, obj->ToString(), String(obj->GetClass()->GetFullName()), String(other->GetClass()->GetFullName()));
}
#endif
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[RegisterObject] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName));
#endif
_objectsDictionary[id] = obj;
}
void Scripting::UnregisterObject(ScriptingObject* obj)
{
ScopeLock lock(_objectsLocker);
//ASSERT(!obj->_id.IsValid() || _objectsDictionary.ContainsValue(obj));
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[UnregisterObject] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName));
#endif
_objectsDictionary.Remove(obj->GetID());
}
void Scripting::OnObjectIdChanged(ScriptingObject* obj, const Guid& oldId)
{
ScopeLock lock(_objectsLocker);
ASSERT(obj && oldId.IsValid());
ASSERT(obj->GetID() != oldId);
ASSERT(_objectsDictionary.ContainsKey(oldId));
//ASSERT(_objectsDictionary.ContainsValue(obj));
ASSERT(!_objectsDictionary.ContainsKey(obj->GetID()));
_objectsDictionary.Remove(oldId);
_objectsDictionary.Add(obj->GetID(), obj);
}
bool initFlaxEngine()
{
// Cache common types
if (StdTypesContainer::Instance()->Gather())
return true;
#if !COMPILE_WITHOUT_CSHARP
// Init C# class library
{
auto scriptingClass = Scripting::GetStaticClass();
ASSERT(scriptingClass);
const auto initMethod = scriptingClass->GetMethod("Init");
ASSERT(initMethod);
MObject* exception = nullptr;
initMethod->Invoke(nullptr, nullptr, &exception);
if (exception)
{
MException ex(exception);
ex.Log(LogType::Fatal, TEXT("FlaxEngine.Scripting.Init"));
return true;
}
}
#endif
// TODO: move it somewhere to game instance class or similar
MainRenderTask::Instance = New<MainRenderTask>();
return false;
}
void onEngineLoaded(MAssembly* assembly)
{
if (initFlaxEngine())
{
LOG(Fatal, "Failed to initialize Flax Engine runtime.");
}
// Set flag
_isEngineAssemblyLoaded = true;
}
void onEngineUnloading(MAssembly* assembly)
{
// Clear flag
_isEngineAssemblyLoaded = false;
// Clear cached methods
_method_Update = nullptr;
_method_LateUpdate = nullptr;
_method_FixedUpdate = nullptr;
_method_Exit = nullptr;
StdTypesContainer::Instance()->Clear();
}
void ScriptingService::Dispose()
{
Scripting::Release();
MCore::UnloadEngine();
}