// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using System.Threading; using System.Threading.Tasks; using FlaxEngine.GUI; using FlaxEngine.Interop; namespace FlaxEngine { /// /// The async tasks scheduler that runs the tasks on a main thread during game update. /// /// /// internal class MainThreadTaskScheduler : TaskScheduler, IDisposable { private readonly BlockingCollection _tasks = new BlockingCollection(); internal void Execute() { while (_tasks.TryTake(out var task)) { TryExecuteTask(task); } } private void Dispose(bool disposing) { if (!disposing) return; _tasks.CompleteAdding(); _tasks.Dispose(); } /// protected override void QueueTask(Task task) { _tasks.Add(task); } /// protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { return false; } /// protected override IEnumerable GetScheduledTasks() { return _tasks.ToArray(); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } /// /// C# scripting service. /// public static partial class Scripting { private static readonly List UpdateActions = new List(); private static readonly MainThreadTaskScheduler MainThreadTaskScheduler = new MainThreadTaskScheduler(); /// /// Occurs on scripting update. /// public static event Action Update; /// /// Occurs on scripting late update. /// public static event Action LateUpdate; /// /// Occurs on scripting fixed update. /// public static event Action FixedUpdate; /// /// Occurs on scripting late fixed update. /// public static event Action LateFixedUpdate; /// /// Occurs on scripting draw update. Called during frame rendering and can be used to invoke custom rendering with GPUDevice. /// public static event Action Draw; /// /// Occurs when scripting engine is disposing. Engine is during closing and some services may be unavailable (eg. loading scenes). This may be called after the engine fatal error event. /// public static event Action Exit; /// /// Gets the async tasks scheduler that runs the tasks on a main thread during game update. /// public static TaskScheduler MainThreadScheduler => MainThreadTaskScheduler; /// /// Calls the given action on the next scripting update. /// /// The action to invoke. public static void InvokeOnUpdate(Action action) { lock (UpdateActions) { UpdateActions.Add(action); } } /// /// Queues the specified work to run on the main thread and returns a object that represents that work. /// /// The work to execute asynchronously /// A task that represents the work queued to execute in the pool. public static Task RunOnUpdate(Action action) { return Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, MainThreadTaskScheduler); } private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) { if (e.ExceptionObject is Exception exception) { Debug.LogError($"Unhandled Exception: {exception.Message}"); Debug.LogException(exception); if (e.IsTerminating && !System.Diagnostics.Debugger.IsAttached) Platform.Fatal($"Unhandled Exception: {exception}"); } } private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { if (e.Exception != null) { foreach (var ex in e.Exception.InnerExceptions) { Debug.LogError("Unhandled Task Exception: " + ex.Message); Debug.LogException(ex); } } } /// /// Initializes Flax API. Called before everything else from native code. /// internal static void Init() { #if BUILD_DEBUG Debug.Logger.LogHandler.LogWrite(LogType.Info, "Using FlaxAPI in Debug"); #elif BUILD_DEVELOPMENT Debug.Logger.LogHandler.LogWrite(LogType.Info, "Using FlaxAPI in Development"); #elif BUILD_RELEASE Debug.Logger.LogHandler.LogWrite(LogType.Info, "Using FlaxAPI in Release"); #endif AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; Localization.LocalizationChanged += OnLocalizationChanged; OnLocalizationChanged(); if (!Engine.IsEditor) { CreateGuiStyle(); } } private static void OnLocalizationChanged() { var currentThread = Thread.CurrentThread; var language = Localization.CurrentLanguage; if (language != null) currentThread.CurrentUICulture = language; var culture = Localization.CurrentCulture; if (culture != null) currentThread.CurrentCulture = culture; } /// /// Sets the managed window as a main game window. Called after creating game window by the native code. /// /// The window. internal static void SetWindow(Window window) { // Link it as a GUI root control window.GUI.BackgroundColor = Color.Transparent; RootControl.GameRoot = window.GUI; } internal static Type MakeGenericType(Type genericType, Type[] genericArgs) { return genericType.MakeGenericType(genericArgs); } internal static void AddDictionaryItem(IDictionary dictionary, object key, object value) { // TODO: more generic approach to properly add value that is of custom boxed type? (eg. via NativeInterop.MarshalToManaged) if (value is ManagedArray managedArray) { var managedArrayHandle = ManagedHandle.Alloc(managedArray, GCHandleType.Normal); value = NativeInterop.MarshalToManaged((IntPtr)managedArrayHandle, managedArray.ArrayType); managedArrayHandle.Free(); } dictionary.Add(key, value); } internal static object[] GetDictionaryKeys(IDictionary dictionary) { if (dictionary == null) return null; var keys = dictionary.Keys; var result = new object[keys.Count]; keys.CopyTo(result, 0); return result; } internal static ManagedHandle VersionToManaged(int major, int minor, int build, int revision) { Version version = new Version(major, minor, Math.Max(build, 0), Math.Max(revision, 0)); return ManagedHandle.Alloc(version); } internal static ManagedHandle CultureInfoToManaged(int lcid) { return ManagedHandle.Alloc(new CultureInfo(lcid)); } [StructLayout(LayoutKind.Sequential)] internal struct VersionNative { public int Major; public int Minor; public int Build; public int Revision; } internal static void VersionToNative(ManagedHandle versionHandle, ref int major, ref int minor, ref int build, ref int revision) { Version version = Unsafe.As(versionHandle.Target); if (version != null) { major = version.Major; minor = version.Minor; build = version.Build; revision = version.Revision; } } private static void CreateGuiStyle() { var style = new Style { Background = Color.FromBgra(0xFF1C1C1C), LightBackground = Color.FromBgra(0xFF2D2D30), Foreground = Color.FromBgra(0xFFFFFFFF), ForegroundGrey = Color.FromBgra(0xFFA9A9B3), ForegroundDisabled = Color.FromBgra(0xFF787883), BackgroundHighlighted = Color.FromBgra(0xFF54545C), BorderHighlighted = Color.FromBgra(0xFF6A6A75), BackgroundSelected = Color.FromBgra(0xFF007ACC), BorderSelected = Color.FromBgra(0xFF1C97EA), BackgroundNormal = Color.FromBgra(0xFF3F3F46), BorderNormal = Color.FromBgra(0xFF54545C), TextBoxBackground = Color.FromBgra(0xFF333337), TextBoxBackgroundSelected = Color.FromBgra(0xFF3F3F46), CollectionBackgroundColor = Color.FromBgra(0x14CCCCCC), ProgressNormal = Color.FromBgra(0xFF0ad328), Statusbar = new Style.StatusbarStyle { PlayMode = Color.FromBgra(0xFF2F9135), Failed = Color.FromBgra(0xFF9C2424), Loading = Color.FromBgra(0xFF2D2D30), }, SharedTooltip = new Tooltip(), }; style.DragWindow = style.BackgroundSelected * 0.7f; // Use optionally bundled default font (matches Editor) var defaultFont = Content.LoadAsyncInternal("Editor/Fonts/Roboto-Regular"); if (defaultFont) { style.FontTitle = defaultFont.CreateFont(18); style.FontLarge = defaultFont.CreateFont(14); style.FontMedium = defaultFont.CreateFont(9); style.FontSmall = defaultFont.CreateFont(9); } Style.Current = style; } internal static void Internal_Update() { Update?.Invoke(); lock (UpdateActions) { for (int i = 0; i < UpdateActions.Count; i++) { try { UpdateActions[i](); } catch (Exception ex) { Debug.LogException(ex); } } UpdateActions.Clear(); } MainThreadTaskScheduler.Execute(); } internal static void Internal_LateUpdate() { LateUpdate?.Invoke(); } internal static void Internal_FixedUpdate() { FixedUpdate?.Invoke(); } internal static void Internal_LateFixedUpdate() { LateFixedUpdate?.Invoke(); } internal static void Internal_Draw() { Draw?.Invoke(); } internal static void Internal_Exit() { Exit?.Invoke(); MainThreadTaskScheduler.Dispose(); Json.JsonSerializer.Dispose(); } /// /// Returns true if game scripts assembly has been loaded. /// /// True if game scripts assembly is loaded, otherwise false. [LibraryImport("FlaxEngine", EntryPoint = "ScriptingInternal_HasGameModulesLoaded", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] [return: MarshalAs(UnmanagedType.U1)] public static partial bool HasGameModulesLoaded(); /// /// Returns true if given type is from one of the game scripts assemblies. /// /// True if the type is from game assembly, otherwise false. [LibraryImport("FlaxEngine", EntryPoint = "ScriptingInternal_IsTypeFromGameScripts", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] [return: MarshalAs(UnmanagedType.U1)] public static partial bool IsTypeFromGameScripts([MarshalUsing(typeof(SystemTypeMarshaller))] Type type); /// /// Flushes the removed objects (disposed objects using Object.Destroy). /// [LibraryImport("FlaxEngine", EntryPoint = "ScriptingInternal_FlushRemovedObjects", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] public static partial void FlushRemovedObjects(); } /// /// Provides C# scripts profiling methods. /// /// /// Profiler is available in the editor and Debug/Development builds. Release builds don't have profiling tools. /// public static partial class Profiler { /// /// Begins profiling a piece of code with a custom label. /// /// The name of the event. [LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_BeginEvent", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] public static partial void BeginEvent(string name); /// /// Ends profiling an event. /// [LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_EndEvent", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] public static partial void EndEvent(); /// /// Begins GPU profiling a piece of code with a custom label. /// /// The name of the event. [LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_BeginEventGPU", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] public static partial void BeginEventGPU(string name); /// /// Ends GPU profiling an event. /// [LibraryImport("FlaxEngine", EntryPoint = "ProfilerInternal_EndEventGPU", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] public static partial void EndEventGPU(); } }