/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(AZ_MONOLITHIC_BUILD) extern "C" void CreateStaticModules(AZStd::vector& modulesOut); #endif // defined(AZ_MONOLITHIC_BUILD) // Add the "REMOTE_ASSET_PROCESSOR" define except in release // this makes it so that asset processor functions. Without this, all assets must be present and on local media // with this, the asset processor can be used to remotely process assets. #if !defined(_RELEASE) # define REMOTE_ASSET_PROCESSOR #endif void CVar_OnViewportPosition(const AZ::Vector2& value); namespace { void CVar_OnViewportResize(const AZ::Vector2& value); AZ_CVAR(AZ::Vector2, r_viewportSize, AZ::Vector2::CreateZero(), CVar_OnViewportResize, AZ::ConsoleFunctorFlags::DontReplicate, "The default size for the launcher viewport, 0 0 means full screen"); void CVar_OnViewportResize(const AZ::Vector2& value) { AzFramework::NativeWindowHandle windowHandle = nullptr; AzFramework::WindowSystemRequestBus::BroadcastResult(windowHandle, &AzFramework::WindowSystemRequestBus::Events::GetDefaultWindowHandle); AzFramework::WindowSize newSize = AzFramework::WindowSize(aznumeric_cast(value.GetX()), aznumeric_cast(value.GetY())); AzFramework::WindowRequestBus::Broadcast(&AzFramework::WindowRequestBus::Events::ResizeClientArea, newSize); } AZ_CVAR(AZ::Vector2, r_viewportPos, AZ::Vector2::CreateZero(), CVar_OnViewportPosition, AZ::ConsoleFunctorFlags::DontReplicate, "The default position for the launcher viewport, 0 0 means top left corner of your main desktop"); void ExecuteConsoleCommandFile(AzFramework::Application& application) { const AZStd::string_view customConCmdKey = "console-command-file"; const AZ::CommandLine* commandLine = application.GetCommandLine(); AZStd::size_t numSwitchValues = commandLine->GetNumSwitchValues(customConCmdKey); if (numSwitchValues > 0) { // The expectations for command line parameters is that the "last one wins" // That way it allows users and test scripts to override previous command line options by just listing them later on the invocation line const AZStd::string& consoleCmd = commandLine->GetSwitchValue(customConCmdKey, numSwitchValues - 1); if (!consoleCmd.empty()) { AZ::Interface::Get()->ExecuteConfigFile(consoleCmd.c_str()); } } } #if AZ_TRAIT_LAUNCHER_USE_CRY_DYNAMIC_MODULE_HANDLE // mimics AZ::DynamicModuleHandle but uses CryLibrary under the hood, // which is necessary to properly load legacy Cry libraries on some platforms class DynamicModuleHandle { public: AZ_CLASS_ALLOCATOR(DynamicModuleHandle, AZ::OSAllocator, 0) static AZStd::unique_ptr Create(const char* fullFileName) { return AZStd::unique_ptr(aznew DynamicModuleHandle(fullFileName)); } DynamicModuleHandle(const DynamicModuleHandle&) = delete; DynamicModuleHandle& operator=(const DynamicModuleHandle&) = delete; ~DynamicModuleHandle() { Unload(); } // argument is strictly to match the API of AZ::DynamicModuleHandle bool Load(bool unused) { AZ_UNUSED(unused); if (IsLoaded()) { return true; } m_moduleHandle = CryLoadLibrary(m_fileName.c_str()); return IsLoaded(); } bool Unload() { if (!IsLoaded()) { return false; } return CryFreeLibrary(m_moduleHandle); } bool IsLoaded() const { return m_moduleHandle != nullptr; } const AZ::OSString& GetFilename() const { return m_fileName; } template Function GetFunction(const char* functionName) const { if (IsLoaded()) { return reinterpret_cast(CryGetProcAddress(m_moduleHandle, functionName)); } else { return nullptr; } } private: DynamicModuleHandle(const char* fileFullName) : m_fileName() , m_moduleHandle(nullptr) { m_fileName = AZ::OSString::format("%s%s%s", CrySharedLibraryPrefix, fileFullName, CrySharedLibraryExtension); } AZ::OSString m_fileName; HMODULE m_moduleHandle; }; #else // mimics AZ::DynamicModuleHandle but also calls InjectEnvironmentFunction on // the loaded module which is necessary to properly load legacy Cry libraries class DynamicModuleHandle { public: AZ_CLASS_ALLOCATOR(DynamicModuleHandle, AZ::OSAllocator, 0); static AZStd::unique_ptr Create(const char* fullFileName) { return AZStd::unique_ptr(aznew DynamicModuleHandle(fullFileName)); } bool Load(bool isInitializeFunctionRequired) { const bool loaded = m_moduleHandle->Load(isInitializeFunctionRequired); if (loaded) { // We need to inject the environment first thing so that allocators are available immediately InjectEnvironmentFunction injectEnv = GetFunction(INJECT_ENVIRONMENT_FUNCTION); if (injectEnv) { auto env = AZ::Environment::GetInstance(); injectEnv(env); } } return loaded; } bool Unload() { bool unloaded = m_moduleHandle->Unload(); if (unloaded) { DetachEnvironmentFunction detachEnv = GetFunction(DETACH_ENVIRONMENT_FUNCTION); if (detachEnv) { detachEnv(); } } return unloaded; } template Function GetFunction(const char* functionName) const { return m_moduleHandle->GetFunction(functionName); } private: DynamicModuleHandle(const char* fileFullName) : m_moduleHandle(AZ::DynamicModuleHandle::Create(fileFullName)) { } AZStd::unique_ptr m_moduleHandle; }; #endif // AZ_TRAIT_LAUNCHER_USE_CRY_DYNAMIC_MODULE_HANDLE void RunMainLoop(AzGameFramework::GameApplication& gameApplication) { // Ideally we'd just call GameApplication::RunMainLoop instead, but // we'd have to stop calling ISystem::UpdatePreTickBus / PostTickBus // directly, and instead have something subscribe to the TickBus in // order to call them, using order ComponentTickBus::TICK_FIRST - 1 // and ComponentTickBus::TICK_LAST + 1 to ensure they get called at // the same time as they do now. Also, we'd need to pass a function // pointer to AzGameFramework::GameApplication::MainLoop that would // be used to call ITimer::GetFrameTime (unless we could also shift // our frame time to be managed by AzGameFramework::GameApplication // instead, which probably isn't going to happen anytime soon given // how many things depend on the ITimer interface). ISystem* system = gEnv ? gEnv->pSystem : nullptr; while (!gameApplication.WasExitMainLoopRequested()) { // Pump the system event loop gameApplication.PumpSystemEventLoopUntilEmpty(); if (gameApplication.WasExitMainLoopRequested()) { break; } // Update the AzFramework system tick bus gameApplication.TickSystem(); // Pre-update CrySystem if (system) { system->UpdatePreTickBus(); } // Update the AzFramework application tick bus gameApplication.Tick(gEnv->pTimer->GetFrameTime()); // Post-update CrySystem if (system) { system->UpdatePostTickBus(); } } } } namespace O3DELauncher { AZ_CVAR(bool, bg_ConnectToAssetProcessor, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "If true, the process will launch and connect to the asset processor"); bool PlatformMainInfo::CopyCommandLine(int argc, char** argv) { for (int argIndex = 0; argIndex < argc; ++argIndex) { if (!AddArgument(argv[argIndex])) { return false; } } return true; } bool PlatformMainInfo::AddArgument(const char* arg) { AZ_Error("Launcher", arg, "Attempting to add a nullptr command line argument!"); bool needsQuote = (strstr(arg, " ") != nullptr); bool needsSpace = (m_commandLine[0] != 0); // strip the previous null-term from the count to prevent double counting m_commandLineLen = (m_commandLineLen == 0) ? 0 : (m_commandLineLen - 1); // compute the expected length with the added argument size_t argLen = strlen(arg); size_t pendingLen = m_commandLineLen + argLen + 1 + (needsSpace ? 1 : 0) + (needsQuote ? 2 : 0); // +1 null-term, [+1 space], [+2 quotes] if (pendingLen >= AZ_COMMAND_LINE_LEN) { AZ_Assert(false, "Command line exceeds the %d character limit!", AZ_COMMAND_LINE_LEN); return false; } if (needsSpace) { m_commandLine[m_commandLineLen++] = ' '; } if (needsQuote) // Branching instead of using a ternary on the format string to avoid warning 4774 (format literal expected) { azsnprintf(m_commandLine + m_commandLineLen, AZ_COMMAND_LINE_LEN - m_commandLineLen, "\"%s\"", arg); } else { azsnprintf(m_commandLine + m_commandLineLen, AZ_COMMAND_LINE_LEN - m_commandLineLen, "%s", arg); } // Inject the argument in the argument buffer to preserve/replicate argC and argV azstrncpy(&m_commandLineArgBuffer[m_nextCommandLineArgInsertPoint], AZ_COMMAND_LINE_LEN - m_nextCommandLineArgInsertPoint, arg, argLen+1); m_argV[m_argC++] = &m_commandLineArgBuffer[m_nextCommandLineArgInsertPoint]; m_nextCommandLineArgInsertPoint += argLen + 1; m_commandLineLen = pendingLen; return true; } const char* GetReturnCodeString(ReturnCode code) { switch (code) { case ReturnCode::Success: return "Success"; case ReturnCode::ErrCommandLine: return "Failed to copy command line arguments"; case ReturnCode::ErrResourceLimit: return "A resource limit failed to update"; case ReturnCode::ErrAppDescriptor: return "Application descriptor file was not found"; case ReturnCode::ErrCrySystemLib: return "Failed to load the CrySystem library"; case ReturnCode::ErrCrySystemInterface: return "Failed to initialize the CrySystem Interface"; case ReturnCode::ErrCryEnvironment: return "Failed to initialize the global environment"; case ReturnCode::ErrAssetProccessor: return "Failed to connect to AssetProcessor while the /Amazon/AzCore/Bootstrap/wait_for_connect value is 1\n." "wait_for_connect can be set to 0 within the bootstrap to allow connecting to the AssetProcessor" " to not be an error if unsuccessful."; default: return "Unknown error code"; } } void CompileCriticalAssets(); void CreateRemoteFileIO(); bool ConnectToAssetProcessor() { bool connectedToAssetProcessor{}; // When the AssetProcessor is already launched it should take less than a second to perform a connection // but when the AssetProcessor needs to be launch it could take up to 15 seconds to have the AssetProcessor initialize // and able to negotiate a connection when running a debug build // and to negotiate a connection // Setting the connectTimeout to 3 seconds if not set within the settings registry AzFramework::AssetSystem::ConnectionSettings connectionSettings; AzFramework::AssetSystem::ReadConnectionSettingsFromSettingsRegistry(connectionSettings); connectionSettings.m_launchAssetProcessorOnFailedConnection = true; connectionSettings.m_connectionIdentifier = AzFramework::AssetSystem::ConnectionIdentifiers::Game; connectionSettings.m_loggingCallback = []([[maybe_unused]] AZStd::string_view logData) { AZ_TracePrintf("Launcher", "%.*s", aznumeric_cast(logData.size()), logData.data()); }; AzFramework::AssetSystemRequestBus::BroadcastResult(connectedToAssetProcessor, &AzFramework::AssetSystemRequestBus::Events::EstablishAssetProcessorConnection, connectionSettings); if (connectedToAssetProcessor) { AZ_TracePrintf("Launcher", "Connected to Asset Processor\n"); CreateRemoteFileIO(); CompileCriticalAssets(); } return connectedToAssetProcessor; } //! Compiles the critical assets that are within the Engine directory of Open 3D Engine //! This code should be in a centralized location, but doesn't belong in AzFramework //! since it is specific to how Open 3D Engine projects has assets setup void CompileCriticalAssets() { // VERY early on, as soon as we can, request that the asset system make sure the following assets take priority over others, // so that by the time we ask for them there is a greater likelihood that they're already good to go. // these can be loaded later but are still important: AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, "/texturemsg/"); AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, "engineassets/materials"); AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, "engineassets/geomcaches"); AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, "engineassets/objects"); // some are specifically extra important and will cause issues if missing completely: AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::CompileAssetSync, "engineassets/objects/default.cgf"); } //! Remote FileIO to use as a Virtual File System //! Communication of FileIOBase operations occur through an AssetProcessor connection void CreateRemoteFileIO() { AZ::SettingsRegistryInterface* settingsRegistry = AZ::SettingsRegistry::Get(); AZ::s64 allowRemoteFilesystem{}; AZ::SettingsRegistryMergeUtils::PlatformGet(*settingsRegistry, allowRemoteFilesystem, AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, "remote_filesystem"); if (allowRemoteFilesystem != 0) { // The SetInstance calls below will assert if this has already been set and we don't clear first // Application::StartCommon will set a LocalFileIO base first. // This provides an opportunity for the RemoteFileIO to override the direct instance auto remoteFileIo = new AZ::IO::RemoteFileIO(AZ::IO::FileIOBase::GetDirectInstance()); // Wrap LocalFileIO the direct instance AZ::IO::FileIOBase::SetDirectInstance(nullptr); // Wrap AZ:IO::LocalFileIO the direct instance AZ::IO::FileIOBase::SetDirectInstance(remoteFileIo); } } ReturnCode Run(const PlatformMainInfo& mainInfo) { if (mainInfo.m_updateResourceLimits && !mainInfo.m_updateResourceLimits()) { return ReturnCode::ErrResourceLimit; } // Game Application (AzGameFramework) int gameArgC = mainInfo.m_argC; char** gameArgV = const_cast(mainInfo.m_argV); constexpr size_t MaxCommandArgsCount = 128; using ArgumentContainer = AZStd::fixed_vector; ArgumentContainer argContainer(gameArgV, gameArgV + gameArgC); using FixedValueString = AZ::SettingsRegistryInterface::FixedValueString; // Inject the Engine Path, Project Path and Project Name into the CommandLine parameters to the command line // in order to be used in the Settings Registry // The command line overrides are stored in the following fixed strings // until the ComponentApplication constructor can parse the command line parameters FixedValueString projectNameOptionOverride; // Insert the project_name option to the front const AZStd::string_view launcherProjectName = GetProjectName(); if (!launcherProjectName.empty()) { const auto projectNameKey = FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + "/project_name"; projectNameOptionOverride = FixedValueString::format(R"(--regset="%s=%.*s")", projectNameKey.c_str(), aznumeric_cast(launcherProjectName.size()), launcherProjectName.data()); argContainer.emplace_back(projectNameOptionOverride.data()); } // Non-host platforms cannot use the project path that is #defined within the launcher. // In this case the the result of AZ::Utils::GetDefaultAppRoot is used instead #if !AZ_TRAIT_OS_IS_HOST_OS_PLATFORM FixedValueString projectPathOptionOverride; FixedValueString enginePathOptionOverride; AZStd::string_view projectPath; // Make sure the defaultAppRootPath variable is in scope long enough until the projectPath string_view is used below AZStd::optional defaultAppRootPath = AZ::Utils::GetDefaultAppRootPath(); if (defaultAppRootPath.has_value()) { projectPath = *defaultAppRootPath; } if (!projectPath.empty()) { const auto projectPathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/project_path"; projectPathOptionOverride = FixedValueString::format(R"(--regset="%s=%.*s")", projectPathKey.c_str(), aznumeric_cast(projectPath.size()), projectPath.data()); argContainer.emplace_back(projectPathOptionOverride.data()); // For non-host platforms set the engine root to be the project root // Since the directories available during execution are limited on those platforms AZStd::string_view enginePath = projectPath; const auto enginePathKey = FixedValueString(AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey) + "/engine_path"; enginePathOptionOverride = FixedValueString ::format(R"(--regset="%s=%.*s")", enginePathKey.c_str(), aznumeric_cast(enginePath.size()), enginePath.data()); argContainer.emplace_back(enginePathOptionOverride.data()); } #endif AzGameFramework::GameApplication gameApplication(aznumeric_cast(argContainer.size()), argContainer.data()); // The settings registry has been created by the AZ::ComponentApplication constructor at this point auto settingsRegistry = AZ::SettingsRegistry::Get(); if (settingsRegistry == nullptr) { // Settings registry must be available at this point in order to continue return ReturnCode::ErrValidation; } const AZStd::string_view buildTargetName = GetBuildTargetName(); AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddBuildSystemTargetSpecialization(*settingsRegistry, buildTargetName); AZ_TracePrintf("Launcher", R"(Running project "%.*s")" "\n" R"(The project name has been successfully set in the Settings Registry at key "%s/project_name")" R"( for Launcher target "%.*s")" "\n", aznumeric_cast(launcherProjectName.size()), launcherProjectName.data(), AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey, aznumeric_cast(buildTargetName.size()), buildTargetName.data()); AZ::SettingsRegistryInterface::FixedValueString pathToAssets; if (!settingsRegistry->Get(pathToAssets, AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder)) { // Default to mainInfo.m_appResource if the cache root folder is missing from the Settings Registry pathToAssets = mainInfo.m_appResourcesPath; AZ_Error("Launcher", false, "Unable to retrieve asset cache root folder from the settings registry at json pointer path %s", AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder); } else { AZ_TracePrintf("Launcher", "The asset cache folder of %s has been successfully read from the Settings Registry\n", pathToAssets.c_str()); } CryAllocatorsRAII cryAllocatorsRAII; // System Init Params ("Legacy" Open 3D Engine) SSystemInitParams systemInitParams; memset(&systemInitParams, 0, sizeof(SSystemInitParams)); { AzGameFramework::GameApplication::StartupParameters gameApplicationStartupParams; if (mainInfo.m_allocator) { gameApplicationStartupParams.m_allocator = mainInfo.m_allocator; } else if (AZ::AllocatorInstance::IsReady()) { gameApplicationStartupParams.m_allocator = &AZ::AllocatorInstance::Get(); } #if defined(AZ_MONOLITHIC_BUILD) gameApplicationStartupParams.m_createStaticModulesCallback = CreateStaticModules; gameApplicationStartupParams.m_loadDynamicModules = false; #endif // defined(AZ_MONOLITHIC_BUILD) gameApplication.Start({}, gameApplicationStartupParams); #if defined(REMOTE_ASSET_PROCESSOR) bool allowedEngineConnection = !systemInitParams.bToolMode && !systemInitParams.bTestMode && bg_ConnectToAssetProcessor; //connect to the asset processor using the bootstrap values if (allowedEngineConnection) { if (!ConnectToAssetProcessor()) { AZ::s64 waitForConnect{}; AZ::SettingsRegistryMergeUtils::PlatformGet(*settingsRegistry, waitForConnect, AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, "wait_for_connect"); if (waitForConnect != 0) { AZ_Error("Launcher", false, "Failed to connect to AssetProcessor."); return ReturnCode::ErrAssetProccessor; } } } #endif AZ_Assert(AZ::AllocatorInstance::IsReady(), "System allocator was not created or creation failed."); //Initialize the Debug trace instance to create necessary environment variables AZ::Debug::Trace::Instance().Init(); if (!IsDedicatedServer() && !systemInitParams.bToolMode && !systemInitParams.bTestMode) { if (auto nativeUI = AZ::Interface::Get(); nativeUI != nullptr) { nativeUI->SetMode(AZ::NativeUI::Mode::ENABLED); } } } if (mainInfo.m_onPostAppStart) { mainInfo.m_onPostAppStart(); } azstrncpy(systemInitParams.szSystemCmdLine, sizeof(systemInitParams.szSystemCmdLine), mainInfo.m_commandLine, mainInfo.m_commandLineLen); systemInitParams.pSharedEnvironment = AZ::Environment::GetInstance(); systemInitParams.sLogFileName = GetLogFilename(); systemInitParams.hInstance = mainInfo.m_instance; systemInitParams.hWnd = mainInfo.m_window; systemInitParams.pPrintSync = mainInfo.m_printSink; systemInitParams.bDedicatedServer = IsDedicatedServer(); if (IsDedicatedServer()) { AZ::Interface::Get()->PerformCommand("sv_isDedicated true"); } else { AZ::Interface::Get()->PerformCommand("sv_isDedicated false"); } bool remoteFileSystemEnabled{}; AZ::SettingsRegistryMergeUtils::PlatformGet(*settingsRegistry, remoteFileSystemEnabled, AZ::SettingsRegistryMergeUtils::BootstrapSettingsRootKey, "remote_filesystem"); if (remoteFileSystemEnabled) { AZ_TracePrintf("Launcher", "Application is configured for VFS"); AZ_TracePrintf("Launcher", "Log and cache files will be written to the Cache directory on your host PC"); #if defined(AZ_ENABLE_TRACING) constexpr const char* message = "If your game does not run, check any of the following:\n" "\t- Verify the remote_ip address is correct in bootstrap.cfg"; #endif if (mainInfo.m_additionalVfsResolution) { AZ_TracePrintf("Launcher", "%s\n%s", message, mainInfo.m_additionalVfsResolution) } else { AZ_TracePrintf("Launcher", "%s", message) } } else { AZ_TracePrintf("Launcher", "Application is configured to use device local files at %s\n", pathToAssets.c_str()); AZ_TracePrintf("Launcher", "Log and cache files will be written to device storage\n"); } // Create CrySystem. #if !defined(AZ_MONOLITHIC_BUILD) AZStd::unique_ptr crySystemLibrary; PFNCREATESYSTEMINTERFACE CreateSystemInterface = nullptr; crySystemLibrary = DynamicModuleHandle::Create("CrySystem"); if (crySystemLibrary->Load(false)) { CreateSystemInterface = crySystemLibrary->GetFunction("CreateSystemInterface"); if (CreateSystemInterface) { systemInitParams.pSystem = CreateSystemInterface(systemInitParams); } } #else systemInitParams.pSystem = CreateSystemInterface(systemInitParams); #endif // !defined(AZ_MONOLITHIC_BUILD) AZ::ComponentApplicationLifecycle::SignalEvent(*settingsRegistry, "LegacySystemInterfaceCreated", R"({})"); ReturnCode status = ReturnCode::Success; if (systemInitParams.pSystem) { // Process queued events before main loop. AZ::TickBus::ExecuteQueuedEvents(); #if !defined(SYS_ENV_AS_STRUCT) gEnv = systemInitParams.pSystem->GetGlobalEnvironment(); #endif // !defined(SYS_ENV_AS_STRUCT) if (gEnv && gEnv->pConsole) { // Execute autoexec.cfg to load the initial level auto autoExecFile = AZ::IO::FixedMaxPath{pathToAssets} / "autoexec.cfg"; AZ::Interface::Get()->ExecuteConfigFile(autoExecFile.Native()); // Find out if console command file was passed // via --console-command-file=%filename% and execute it ExecuteConsoleCommandFile(gameApplication); gEnv->pSystem->ExecuteCommandLine(false); // Run the main loop RunMainLoop(gameApplication); } else { status = ReturnCode::ErrCryEnvironment; } } else { status = ReturnCode::ErrCrySystemInterface; } #if !defined(AZ_MONOLITHIC_BUILD) delete systemInitParams.pSystem; crySystemLibrary.reset(nullptr); #endif // !defined(AZ_MONOLITHIC_BUILD) gameApplication.Stop(); AZ::Debug::Trace::Instance().Destroy(); return status; } }