From a932d549f4e69f1d61905b17d1d8f6c0b528a8d7 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 3 Oct 2024 10:24:15 +0200 Subject: [PATCH] Debug Commands work in progress --- Source/Engine/Debug/DebugCommands.cpp | 293 ++++++++++++++++++++++++++ Source/Engine/Debug/DebugCommands.cs | 15 ++ Source/Engine/Debug/DebugCommands.h | 24 +++ Source/Engine/Engine/Engine.h | 2 +- Source/Engine/Graphics/Graphics.h | 2 +- 5 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 Source/Engine/Debug/DebugCommands.cpp create mode 100644 Source/Engine/Debug/DebugCommands.cs create mode 100644 Source/Engine/Debug/DebugCommands.h diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp new file mode 100644 index 000000000..e4af0268e --- /dev/null +++ b/Source/Engine/Debug/DebugCommands.cpp @@ -0,0 +1,293 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +#include "DebugCommands.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Collections/Array.h" +#include "Engine/Engine/EngineService.h" +#include "Engine/Threading/Threading.h" +#include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Scripting/BinaryModule.h" +#include "Engine/Scripting/Scripting.h" +#include "Engine/Scripting/ManagedCLR/MAssembly.h" +#include "Engine/Scripting/ManagedCLR/MClass.h" +#include "Engine/Scripting/ManagedCLR/MMethod.h" +#include "Engine/Scripting/ManagedCLR/MField.h" +#include "Engine/Scripting/ManagedCLR/MProperty.h" +#include "FlaxEngine.Gen.h" + +struct CommandData +{ + String Name; + BinaryModule* Module; + void* Method = nullptr; + void* MethodGet = nullptr; + void* MethodSet = nullptr; + void* Field = nullptr; + + void Invoke(StringView args) const + { + PROFILE_CPU(); + + // Get command signature + Array> sigParams; + if (Method) + { + ScriptingTypeMethodSignature sig; + Module->GetMethodSignature(Method, sig); + sigParams = MoveTemp(sig.Params); + } + else if (Field) + { + ScriptingTypeFieldSignature sig; + Module->GetFieldSignature(Field, sig); + auto& p = sigParams.AddOne(); + p.IsOut = false; + p.Type = sig.ValueType; + } + else if (MethodSet && args.HasChars()) + { + ScriptingTypeMethodSignature sig; + Module->GetMethodSignature(MethodSet, sig); + sigParams = MoveTemp(sig.Params); + sigParams.Resize(1); + } + + // Parse arguments + Array params; + params.Resize(sigParams.Count()); + Array argsSeparated; + String argsStr(args); + argsStr.Split(' ', argsSeparated); + for (int32 i = 0; i < argsSeparated.Count() && i < params.Count(); i++) + { + params[i] = Variant::Parse(argsSeparated[i], sigParams[i].Type); + } + + // Call command + Variant result; + if (Method) + { + Module->InvokeMethod(Method, Variant::Null, ToSpan(params), result); + } + else if (Field) + { + if (args.IsEmpty()) + Module->GetFieldValue(Field, Variant::Null, result); + else + Module->SetFieldValue(Field, Variant::Null, params[0]); + } + else if (MethodGet && args.IsEmpty()) + { + Module->InvokeMethod(MethodGet, Variant::Null, ToSpan(params), result); + } + else if (MethodSet && args.HasChars()) + { + Module->InvokeMethod(MethodSet, Variant::Null, ToSpan(params), result); + } + + // Print result + if (result != Variant()) + { + LOG_STR(Info, result.ToString()); + } + } +}; + +namespace +{ + CriticalSection Locker; + bool Inited = false; + Array Commands; + + void OnBinaryModuleLoaded(BinaryModule* module) + { + if (module == GetBinaryModuleCorlib()) + return; + +#if USE_CSHARP + if (auto* managedModule = dynamic_cast(module)) + { + const MClass* attribute = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly->GetClass("FlaxEngine.DebugCommand"); + ASSERT_LOW_LAYER(attribute); + const auto& classes = managedModule->Assembly->GetClasses(); + for (auto e : classes) + { + MClass* mclass = e.Value; + if (mclass->IsGeneric() || + mclass->IsInterface() || + mclass->IsEnum()) + continue; + const bool useClass = mclass->HasAttribute(attribute); + // TODO: optimize this via stack-based format buffer and then convert Ansi to UTF16 +#define BUILD_NAME(commandData, itemName) commandData.Name = String(mclass->GetName()) + TEXT(".") + String(itemName) + + // Process methods + const auto& methods = mclass->GetMethods(); + for (MMethod* method : methods) + { + if (!method->IsStatic()) + continue; + const StringAnsi& name = method->GetName(); + if (name.Contains("Internal_") || + mclass->GetFullName().Contains(".Interop.")) + continue; + if (name.StartsWith("get_") || + name.StartsWith("set_") || + name.StartsWith("op_") || + name.StartsWith("add_") || + name.StartsWith("remove_")) + continue; + if (!useClass && !method->HasAttribute(attribute)) + continue; + + auto& commandData = Commands.AddOne(); + BUILD_NAME(commandData, method->GetName()); + commandData.Module = module; + commandData.Method = method; + } + + // Process fields + const auto& fields = mclass->GetFields(); + for (MField* field : fields) + { + if (!field->IsStatic()) + continue; + if (!useClass && !field->HasAttribute(attribute)) + continue; + + auto& commandData = Commands.AddOne(); + BUILD_NAME(commandData, field->GetName()); + commandData.Module = module; + commandData.Field = field; + } + + // Process properties + const auto& properties = mclass->GetProperties(); + for (MProperty* property : properties) + { + if (!property->IsStatic()) + continue; + if (!useClass && !property->HasAttribute(attribute)) + continue; + + auto& commandData = Commands.AddOne(); + BUILD_NAME(commandData, property->GetName()); + commandData.Module = module; + commandData.MethodGet = property->GetGetMethod(); + commandData.MethodSet = property->GetSetMethod(); + } + } +#undef BUILD_NAME + } + else +#endif + { + // TODO: implement generic search for other modules (eg. Visual Scripts) + } + } + + void OnScriptsReloading() + { + // Reset + Inited = false; + Commands.Clear(); + } + + void InitCommands() + { + PROFILE_CPU(); + Inited = true; + const auto& modules = BinaryModule::GetModules(); + for (BinaryModule* module : modules) + { + OnBinaryModuleLoaded(module); + } + Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded); + Scripting::ScriptsReloading.Bind(&OnScriptsReloading); + } +} + +class DebugCommandsService : public EngineService +{ +public: + DebugCommandsService() + : EngineService(TEXT("DebugCommands"), 0) + { + } + + void Dispose() override + { + // Cleanup + ScopeLock lock(Locker); + Scripting::BinaryModuleLoaded.Unbind(&OnBinaryModuleLoaded); + Scripting::ScriptsReloading.Unbind(&OnScriptsReloading); + Commands.Clear(); + Inited = true; + } +}; + +DebugCommandsService DebugCommandsServiceInstance; + +void DebugCommands::Execute(StringView command) +{ + // Preprocess command text + while (command.HasChars() && StringUtils::IsWhitespace(command[0])) + command = StringView(command.Get() + 1, command.Length() - 1); + while (command.HasChars() && StringUtils::IsWhitespace(command[command.Length() - 1])) + command = StringView(command.Get(), command.Length() - 1); + if (command.IsEmpty()) + return; + StringView name = command; + StringView args; + int32 argsStart = name.Find(' '); + if (argsStart != -1) + { + name = command.Left(argsStart); + args = command.Right(argsStart + 1); + } + + // Ensure that commands cache has been created + ScopeLock lock(Locker); + if (!Inited) + InitCommands(); + + // Find command to run + for (const CommandData& command : Commands) + { + if (name.Length() == command.Name.Length() && + StringUtils::CompareIgnoreCase(name.Get(), command.Name.Get(), name.Length()) == 0) + { + command.Invoke(args); + return; + } + } + + LOG(Error, "Unknown command '{}'", name); +} + +bool DebugCommands::Iterate(const StringView& searchText, int32& index) +{ + ScopeLock lock(Locker); + if (index >= 0) + { + if (!Inited) + InitCommands(); + while (index < Commands.Count()) + { + auto& command = Commands.Get()[index]; + if (command.Name.StartsWith(searchText, StringSearchCase::IgnoreCase)) + { + return true; + } + index++; + } + } + return false; +} + +String DebugCommands::GetCommandName(int32 index) +{ + ScopeLock lock(Locker); + CHECK_RETURN(Commands.IsValidIndex(index), String::Empty); + return Commands.Get()[index].Name; +} diff --git a/Source/Engine/Debug/DebugCommands.cs b/Source/Engine/Debug/DebugCommands.cs new file mode 100644 index 000000000..3c67ca31e --- /dev/null +++ b/Source/Engine/Debug/DebugCommands.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +using System; + +namespace FlaxEngine +{ + /// + /// Marks static method as debug command that can be executed from the command line or via console. + /// + [Serializable] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class DebugCommand : Attribute + { + } +} diff --git a/Source/Engine/Debug/DebugCommands.h b/Source/Engine/Debug/DebugCommands.h new file mode 100644 index 000000000..cedf39f57 --- /dev/null +++ b/Source/Engine/Debug/DebugCommands.h @@ -0,0 +1,24 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Scripting/ScriptingType.h" + +/// +/// Debug commands and console variables system. +/// +API_CLASS(static) class FLAXENGINE_API DebugCommands +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(DebugCommands); + +public: + /// + /// Executees the command. + /// + /// The command line (optionally with arguments). + API_FUNCTION() static void Execute(StringView command); + +public: + static bool Iterate(const StringView& searchText, int32& index); + static String GetCommandName(int32 index); +}; diff --git a/Source/Engine/Engine/Engine.h b/Source/Engine/Engine/Engine.h index 85ffefda0..2f1728aa2 100644 --- a/Source/Engine/Engine/Engine.h +++ b/Source/Engine/Engine/Engine.h @@ -97,7 +97,7 @@ public: /// Exits the engine. /// /// The exit code. - static void Exit(int32 exitCode = -1); + API_FUNCTION(Attributes="DebugCommand") static void Exit(int32 exitCode = -1); /// /// Requests normal engine exit. diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h index 4c5e0ad26..fdcc706f7 100644 --- a/Source/Engine/Graphics/Graphics.h +++ b/Source/Engine/Graphics/Graphics.h @@ -9,7 +9,7 @@ /// /// Graphics device manager that creates, manages and releases graphics device and related objects. /// -API_CLASS(Static) class FLAXENGINE_API Graphics +API_CLASS(Static, Attributes="DebugCommand") class FLAXENGINE_API Graphics { DECLARE_SCRIPTING_TYPE_NO_SPAWN(Graphics); public: