diff --git a/Code/Framework/AzCore/AzCore/Console/Console.cpp b/Code/Framework/AzCore/AzCore/Console/Console.cpp index 8bb50f77e0..016bd86e46 100644 --- a/Code/Framework/AzCore/AzCore/Console/Console.cpp +++ b/Code/Framework/AzCore/AzCore/Console/Console.cpp @@ -174,12 +174,28 @@ namespace AZ } } - bool Console::HasCommand(const char* command) + bool Console::ExecuteDeferredConsoleCommands() + { + auto DeferredCommandCallable = [this](const DeferredCommand& deferredCommand) + { + return this->DispatchCommand(deferredCommand.m_command, deferredCommand.m_arguments, deferredCommand.m_silentMode, + deferredCommand.m_invokedFrom, deferredCommand.m_requiredSet, deferredCommand.m_requiredClear); + }; + // Attempt to invoke the deferred command and remove it from the queue if successful + return AZStd::erase_if(m_deferredCommands, DeferredCommandCallable) != 0; + } + + void Console::ClearDeferredConsoleCommands() + { + m_deferredCommands = {}; + } + + bool Console::HasCommand(AZStd::string_view command) { return FindCommand(command) != nullptr; } - ConsoleFunctorBase* Console::FindCommand(const char* command) + ConsoleFunctorBase* Console::FindCommand(AZStd::string_view command) { CVarFixedString lowerName(command); AZStd::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](char value) { return std::tolower(value); }); @@ -200,11 +216,9 @@ namespace AZ return nullptr; } - AZStd::string Console::AutoCompleteCommand(const char* command, AZStd::vector* matches) + AZStd::string Console::AutoCompleteCommand(AZStd::string_view command, AZStd::vector* matches) { - const size_t commandLength = strlen(command); - - if (commandLength <= 0) + if (command.empty()) { return command; } @@ -219,7 +233,7 @@ namespace AZ continue; } - if (StringFunc::Equal(curr->m_name, command, false, commandLength)) + if (StringFunc::StartsWith(curr->m_name, command, false)) { AZLOG_INFO("- %s : %s\n", curr->m_name, curr->m_desc); commandSubset.push_back(curr->m_name); @@ -498,7 +512,8 @@ namespace AZ AZ::IO::PathView consoleRootCommandKey{ IConsole::ConsoleRootCommandKey, AZ::IO::PosixPathSeparator }; AZ::IO::PathView inputKey{ path, AZ::IO::PosixPathSeparator }; - if (inputKey.IsRelativeTo(consoleRootCommandKey)) + // The ConsoleRootComamndKey is not a command itself so strictly children keys are being examined + if (inputKey.IsRelativeTo(consoleRootCommandKey) && inputKey != consoleRootCommandKey) { FixedValueString command = inputKey.LexicallyRelative(consoleRootCommandKey).Native(); ConsoleCommandContainer commandArgs; @@ -560,7 +575,24 @@ namespace AZ commandTrace += commandArg; } - m_console.PerformCommand(command, commandArgs, ConsoleSilentMode::NotSilent, ConsoleInvokedFrom::AzConsole, ConsoleFunctorFlags::Null, ConsoleFunctorFlags::Null); + if (!m_console.PerformCommand(command, commandArgs, ConsoleSilentMode::NotSilent, + ConsoleInvokedFrom::AzConsole, ConsoleFunctorFlags::Null, ConsoleFunctorFlags::Null)) + { + // If the command could not be dispatched at this time add it to the + // deferred commands queue + using DeferredCommand = Console::DeferredCommand; + DeferredCommand deferredCommand + { + AZStd::string_view{command}, + DeferredCommand::DeferredArguments{commandArgs.begin(), commandArgs.end()}, + ConsoleSilentMode::NotSilent, + ConsoleInvokedFrom::AzConsole, + ConsoleFunctorFlags::Null, + ConsoleFunctorFlags::Null + }; + + m_console.m_deferredCommands.emplace_back(AZStd::move(deferredCommand)); + } } } diff --git a/Code/Framework/AzCore/AzCore/Console/Console.h b/Code/Framework/AzCore/AzCore/Console/Console.h index dbcf2b116f..0edda9f531 100644 --- a/Code/Framework/AzCore/AzCore/Console/Console.h +++ b/Code/Framework/AzCore/AzCore/Console/Console.h @@ -60,9 +60,13 @@ namespace AZ ) override; void ExecuteConfigFile(AZStd::string_view configFileName) override; void ExecuteCommandLine(const AZ::CommandLine& commandLine) override; - bool HasCommand(const char* command) override; - ConsoleFunctorBase* FindCommand(const char* command) override; - AZStd::string AutoCompleteCommand(const char* command, AZStd::vector* matches = nullptr) override; + bool ExecuteDeferredConsoleCommands() override; + + void ClearDeferredConsoleCommands() override; + + bool HasCommand(AZStd::string_view command) override; + ConsoleFunctorBase* FindCommand(AZStd::string_view command) override; + AZStd::string AutoCompleteCommand(AZStd::string_view command, AZStd::vector* matches = nullptr) override; void VisitRegisteredFunctors(const FunctorVisitor& visitor) override; void RegisterFunctor(ConsoleFunctorBase* functor) override; void UnregisterFunctor(ConsoleFunctorBase* functor) override; @@ -98,7 +102,20 @@ namespace AZ using CommandMap = AZStd::unordered_map>; CommandMap m_commands; AZ::SettingsRegistryInterface::NotifyEventHandler m_consoleCommandKeyHandler; + struct DeferredCommand + { + using DeferredArguments = AZStd::vector; + AZStd::string m_command; + DeferredArguments m_arguments; + ConsoleSilentMode m_silentMode; + ConsoleInvokedFrom m_invokedFrom; + ConsoleFunctorFlags m_requiredSet; + ConsoleFunctorFlags m_requiredClear; + }; + using DeferredCommandQueue = AZStd::deque; + DeferredCommandQueue m_deferredCommands; + friend struct ConsoleCommandKeyNotificationHandler; friend class ConsoleFunctorBase; }; } diff --git a/Code/Framework/AzCore/AzCore/Console/IConsole.h b/Code/Framework/AzCore/AzCore/Console/IConsole.h index ee2b2bd0fb..dafc284ce3 100644 --- a/Code/Framework/AzCore/AzCore/Console/IConsole.h +++ b/Code/Framework/AzCore/AzCore/Console/IConsole.h @@ -94,22 +94,30 @@ namespace AZ //! @param commandLine the concatenated command-line string to execute virtual void ExecuteCommandLine(const AZ::CommandLine& commandLine) = 0; + //! Attempts to invoke a "deferred console command", which is a console command + //! that has failed to execute previously due to the command not being registered yet. + //! @return boolean true if any deferred console commands have executed, false otherwise + virtual bool ExecuteDeferredConsoleCommands() = 0; + + //! Clear out any deferred console commands queue + virtual void ClearDeferredConsoleCommands() = 0; + //! HasCommand is used to determine if the console knows about a command. //! @param command the command we are checking for //! @return boolean true on if the command is registered, false otherwise - virtual bool HasCommand(const char* command) = 0; + virtual bool HasCommand(AZStd::string_view command) = 0; //! FindCommand finds the console command with the specified console string. //! @param command the command that is being searched for //! @return non-null pointer to the console command if found - virtual ConsoleFunctorBase* FindCommand(const char* command) = 0; + virtual ConsoleFunctorBase* FindCommand(AZStd::string_view command) = 0; //! Finds all commands where the input command is a prefix and returns //! the longest matching substring prefix the results have in common. //! @param command The prefix string to find all matching commands for. //! @param matches The list of all commands that match the input prefix. //! @return The longest matching substring prefix the results have in common. - virtual AZStd::string AutoCompleteCommand(const char* command, + virtual AZStd::string AutoCompleteCommand(AZStd::string_view command, AZStd::vector* matches = nullptr) = 0; //! Retrieves the value of the requested cvar. @@ -117,7 +125,7 @@ namespace AZ //! @param outValue reference to the instance to write the current cvar value to //! @return GetValueResult::Success if the operation succeeded, or an error result if the operation failed template - GetValueResult GetCvarValue(const char* command, RETURN_TYPE& outValue); + GetValueResult GetCvarValue(AZStd::string_view command, RETURN_TYPE& outValue); //! Visits all registered console functors. //! @param visitor the instance to visit all functors with @@ -176,7 +184,7 @@ namespace AZ } template - inline GetValueResult IConsole::GetCvarValue(const char* command, RETURN_TYPE& outValue) + inline GetValueResult IConsole::GetCvarValue(AZStd::string_view command, RETURN_TYPE& outValue) { ConsoleFunctorBase* cvarFunctor = FindCommand(command); if (cvarFunctor == nullptr) diff --git a/Code/Framework/AzCore/AzCore/Module/Module.h b/Code/Framework/AzCore/AzCore/Module/Module.h index 2c87b4f0fb..fb651e58a0 100644 --- a/Code/Framework/AzCore/AzCore/Module/Module.h +++ b/Code/Framework/AzCore/AzCore/Module/Module.h @@ -98,19 +98,26 @@ namespace AZ /// /// \param MODULE_NAME Name of module. /// \param MODULE_CLASSNAME Name of AZ::Module class (include namespace). +/// +/// Execute any deferred console commands after linking any new deferred functors +/// This allows deferred console commands defined within the module to now execute +/// at this point now that the module has been loaded #if defined(AZ_MONOLITHIC_BUILD) # define AZ_DECLARE_MODULE_CLASS(MODULE_NAME, MODULE_CLASSNAME) \ extern "C" AZ::Module * CreateModuleClass_##MODULE_NAME() { return aznew MODULE_CLASSNAME; } #else # define AZ_DECLARE_MODULE_CLASS(MODULE_NAME, MODULE_CLASSNAME) \ AZ_DECLARE_MODULE_INITIALIZATION \ - extern "C" AZ_DLL_EXPORT AZ::Module * CreateModuleClass() \ + extern "C" AZ_DLL_EXPORT AZ::Module* CreateModuleClass() \ { \ - AZ::ConsoleFunctorBase*& deferredHead = AZ::ConsoleFunctorBase::GetDeferredHead(); \ - AZ::Interface::Get()->LinkDeferredFunctors(deferredHead); \ + if (auto console = AZ::Interface::Get(); console != nullptr) \ + { \ + console->LinkDeferredFunctors(AZ::ConsoleFunctorBase::GetDeferredHead()); \ + console->ExecuteDeferredConsoleCommands(); \ + } \ return aznew MODULE_CLASSNAME; \ } \ - extern "C" AZ_DLL_EXPORT void DestroyModuleClass(AZ::Module * module) { delete module; } + extern "C" AZ_DLL_EXPORT void DestroyModuleClass(AZ::Module* module) { delete module; } #endif #endif // AZCORE_MODULE_INCLUDE_H diff --git a/Code/Framework/AzCore/Tests/Console/ConsoleTests.cpp b/Code/Framework/AzCore/Tests/Console/ConsoleTests.cpp index 3a56772b3c..d91ec5ba58 100644 --- a/Code/Framework/AzCore/Tests/Console/ConsoleTests.cpp +++ b/Code/Framework/AzCore/Tests/Console/ConsoleTests.cpp @@ -507,6 +507,70 @@ namespace ConsoleSettingsRegistryTests AZ::Interface::Unregister(&testConsole); } + template + using ConsoleDataWrapper = AZ::ConsoleDataWrapper>; + TEST_P(ConsoleSettingsRegistryFixture, Console_RecordsUnregisteredCommands_And_IsAbleToDeferDispatchCommand_Successfully) + { + AZ::Console testConsole(*m_registry); + AZ::Interface::Register(&testConsole); + // GetDeferredHead is invoked for the side effect of to set the s_deferredHeadInvoked value to true + // This allows scoped console variables to be attached immediately + [[maybe_unused]] auto deferredHead = AZ::ConsoleFunctorBase::GetDeferredHead(); + + + ConsoleDataWrapper localTestInit{ {}, nullptr, "testInit", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestChar{ {}, nullptr, "testChar", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestBool{ {}, nullptr, "testBool", "", AZ::ConsoleFunctorFlags::Null }; + + s_consoleFreeFunctionInvoked = false; + + // Invoke the Commands for Scoped CVar variables above + auto configFileParams = GetParam(); + auto testFilePath = m_testFolder / configFileParams.m_testConfigFileName; + EXPECT_TRUE(AZ::IO::SystemFile::Exists(testFilePath.c_str())); + testConsole.ExecuteConfigFile(testFilePath.Native()); + + EXPECT_EQ(3, localTestInit); + EXPECT_TRUE(static_cast(localTestBool)); + EXPECT_EQ('Q', localTestChar); + + // The following commands from the config files should have been deferred + ConsoleDataWrapper localTestInt8{ {}, nullptr, "testInt8", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestInt16{ {}, nullptr, "testInt16", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestInt32{ {}, nullptr, "testInt32", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestInt64{ {}, nullptr, "testInt64", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestUInt8{ {}, nullptr, "testUInt8", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestUInt16{ {}, nullptr, "testUInt16", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestUInt32{ {}, nullptr, "testUInt32", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestUInt64{ {}, nullptr, "testUInt64", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestFloat{ {}, nullptr, "testFloat", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestDouble{ {}, nullptr, "testDouble", "", AZ::ConsoleFunctorFlags::Null }; + ConsoleDataWrapper localTestString{ {}, nullptr, "testString", "", AZ::ConsoleFunctorFlags::Null }; + + + // The scoped cvars just above should have all been deferred for execution + // Each of them should have executed resulting in the expected return value + EXPECT_TRUE(testConsole.ExecuteDeferredConsoleCommands()); + + EXPECT_EQ(24, localTestInt8); + EXPECT_EQ(-32, localTestInt16); + EXPECT_EQ(41, localTestInt32); + EXPECT_EQ(-51, localTestInt64); + EXPECT_EQ(3, localTestUInt8); + EXPECT_EQ(5, localTestUInt16); + EXPECT_EQ(6, localTestUInt32); + EXPECT_EQ(0xFFFF'FFFF'FFFF'FFFF, localTestUInt64); + EXPECT_FLOAT_EQ(1.0f, localTestFloat); + EXPECT_DOUBLE_EQ(2, localTestDouble); + EXPECT_STREQ("Stable", static_cast(localTestString).c_str()); + + // All of the deferred console commands should have executed at this point + // Therefore this invocation should return false + EXPECT_FALSE(testConsole.ExecuteDeferredConsoleCommands()); + + AZ::Interface::Unregister(&testConsole); + } + static constexpr AZStd::string_view UserINIStyleContent = R"(