b53bf52e0d
* Fix code that deregisters the Atom Scene subsystem from the AzFramework Scene The AzFramework Scene subsystem API is a generic container based on the type of argument that is passed to it. It maintains a vector of typeids, and only one object of any type is stored at a time. The Bootstrap system component registers the Atom scene as a `ScenePtr` (aka `AZStd::shared_ptr<RPI:Scene>`) with the AzFramework Scene's generic subsystem. However, the component was previously deregistering the type by value, `RPI::Scene`. Since no subsystem for the type `RPI::Scene` was set, unsetting this type did nothing. The result was that the `RPI::Scene` object would still be around by the time that all the Atom `InstanceDatabse`s were being destroyed, resulting in a large number of errors reported about leaked instances during global shutdown. This fixes the above issue by passing the `m_defaultScene` as a parameter to `AzFramework::Scene::UnsetSubsystem`, the same value that is passed to `SetSubsystem`. This is better, because instead of providing explicit template arguments (which were specifying the incorrect type), this now allows the compiler to deduce the correct type, and the syntax is symmetric with the call to `SetSubsystem`. Signed-off-by: Chris Burel <burelc@amazon.com> * Correctly release the AWS API from the `HttpRequestManager` module This code was incorrectly assuming that `AWSNativeSDKInit::InitializationManager::Shutdown()` would be called automatically by the `InitializationManager` itself. However, all that `InitAwsApi()` does is create an `AZ::EnvironmentVariable`, which is a ref-counted type, and stores it in a global static. That global static is defined in a static library (namely `AWSNativeSDKInit`), which is linked in to the `HttpRequestManager` dynamic lib. Because it is a global static, it has to be explicitly cleared with the call to `Shutdown()`. Otherwise the destructor of the EnvironmentVariable doesn't happen until global destruction, by which time the allocator that is supplied to the AWS SDK has already been destroyed, and the shutdown of the AWS SDK attempts to use the already-destroyed allocator. Signed-off-by: Chris Burel <burelc@amazon.com> * Avoid blocking the remote console server thread if there are no connections The Remote console server runs in a separate thread. Previously, it would directly call `AzSock::Accept()` and block the server thread until some client connected to it. However, if no client connected, the thread would continue to be blocked, even if the game launcher tried to exit. This adds a check to see if there's a client on the socket before calling `Accept()`, to avoid the deadlock on launcher exit. Signed-off-by: Chris Burel <burelc@amazon.com> * Fix a log message to print one message per line Signed-off-by: Chris Burel <burelc@amazon.com> * Allow pumping the event loop to close the launcher window Events from the OS are handled in the game's main loop. The general loop looks like this: * Read events from the OS * Tick the game application One of the events that can come from the OS is that the window hosting the game is closed. When this event happens, many resources provided by the renderer are freed, and the game application's `shouldExit` bit is set. However, when the game's `Tick()` is called, there is lots of code that assumes the renderer is still there. To avoid crashing in the `Tick()` call, check if the game should exit after pumping the system events. Signed-off-by: Chris Burel <burelc@amazon.com> * Unload the level when exiting the launcher This ensures that any resources held onto by the level are freed before the launcher exits. Signed-off-by: Chris Burel <burelc@amazon.com> * Add an explicit bus `Disconnect()` call to `AZCoreLogSink` This is necessary because this bus has virtual functions and can be called from multiple threads. Signed-off-by: Chris Burel <burelc@amazon.com> * Allow normal cleanup to take place when exiting the game launcher Previously, global cleanup was side-stepped by calling `TerminateProcess` or `exit`, when quitting the game launcher. This is in contrast to the call to `_exit` on Linux and Mac when exiting the Editor. That leading `_` makes a big difference: the former runs object destruction, the latter does not. Instead of making the launcher exit with `_exit` on Linux, instead, remove that call and actually run all the atexit code. This does not modify the Editor's behavior however. It still uses `_exit` and `TerminateProcess`. Signed-off-by: Chris Burel <burelc@amazon.com>
904 lines
29 KiB
C++
904 lines
29 KiB
C++
/*
|
|
* 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
|
|
*
|
|
*/
|
|
|
|
|
|
// [LYN-2376] Remove the entire file once legacy slice support is removed
|
|
|
|
#include "CrySystem_precompiled.h"
|
|
#include "LevelSystem.h"
|
|
#include "IMovieSystem.h"
|
|
#include <ILocalizationManager.h>
|
|
#include "CryPath.h"
|
|
|
|
#include <LoadScreenBus.h>
|
|
#include <CryCommon/StaticInstance.h>
|
|
|
|
#include <AzCore/Debug/AssetTracking.h>
|
|
#include <AzFramework/API/ApplicationAPI.h>
|
|
#include <AzFramework/IO/FileOperations.h>
|
|
#include <AzFramework/Entity/GameEntityContextBus.h>
|
|
#include <AzFramework/Input/Buses/Requests/InputChannelRequestBus.h>
|
|
#include <AzFramework/Spawnable/RootSpawnableInterface.h>
|
|
|
|
#include "MainThreadRenderRequestBus.h"
|
|
#include <LyShine/ILyShine.h>
|
|
#include <AzCore/Component/TickBus.h>
|
|
#include <AzCore/IO/Path/Path.h>
|
|
#include <AzCore/StringFunc/StringFunc.h>
|
|
|
|
#include <AzCore/Script/ScriptSystemBus.h>
|
|
|
|
namespace LegacyLevelSystem
|
|
{
|
|
static constexpr const char* ArchiveExtension = ".pak";
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
bool CLevelInfo::OpenLevelPak()
|
|
{
|
|
bool usePrefabSystemForLevels = false;
|
|
AzFramework::ApplicationRequests::Bus::BroadcastResult(
|
|
usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemForLevelsEnabled);
|
|
|
|
// The prefab system doesn't use level.pak
|
|
if (usePrefabSystemForLevels)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AZStd::string levelpak(m_levelPath);
|
|
levelpak += "/level.pak";
|
|
AZStd::fixed_string<AZ::IO::IArchive::MaxPath> fullLevelPakPath;
|
|
bool bOk = gEnv->pCryPak->OpenPack(
|
|
levelpak.c_str(), m_isPak ? AZ::IO::IArchive::FLAGS_LEVEL_PAK_INSIDE_PAK : (unsigned)0, NULL, &fullLevelPakPath, false);
|
|
m_levelPakFullPath.assign(fullLevelPakPath.c_str());
|
|
return bOk;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
void CLevelInfo::CloseLevelPak()
|
|
{
|
|
bool usePrefabSystemForLevels = false;
|
|
AzFramework::ApplicationRequests::Bus::BroadcastResult(
|
|
usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemForLevelsEnabled);
|
|
|
|
// The prefab system doesn't use level.pak
|
|
if (usePrefabSystemForLevels)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!m_levelPakFullPath.empty())
|
|
{
|
|
gEnv->pCryPak->ClosePack(m_levelPakFullPath.c_str(), AZ::IO::IArchive::FLAGS_PATH_REAL);
|
|
m_levelPakFullPath.clear();
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
bool CLevelInfo::ReadInfo()
|
|
{
|
|
bool usePrefabSystemForLevels = false;
|
|
AzFramework::ApplicationRequests::Bus::BroadcastResult(
|
|
usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemForLevelsEnabled);
|
|
|
|
// Set up a default game type for legacy code.
|
|
m_defaultGameTypeName = "Mission0";
|
|
|
|
if (usePrefabSystemForLevels)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
|
|
AZStd::string levelPath(m_levelPath);
|
|
AZStd::string xmlFile(levelPath);
|
|
xmlFile += "/LevelInfo.xml";
|
|
XmlNodeRef rootNode = GetISystem()->LoadXmlFromFile(xmlFile.c_str());
|
|
|
|
if (rootNode)
|
|
{
|
|
AZStd::string dataFile(levelPath);
|
|
dataFile += "/LevelDataAction.xml";
|
|
XmlNodeRef dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str());
|
|
if (!dataNode)
|
|
{
|
|
dataFile = levelPath + "/LevelData.xml";
|
|
dataNode = GetISystem()->LoadXmlFromFile(dataFile.c_str());
|
|
}
|
|
|
|
if (dataNode)
|
|
{
|
|
XmlNodeRef gameTypesNode = dataNode->findChild("Missions");
|
|
|
|
if ((gameTypesNode != 0) && (gameTypesNode->getChildCount() > 0))
|
|
{
|
|
m_defaultGameTypeName.clear();
|
|
|
|
for (int i = 0; i < gameTypesNode->getChildCount(); i++)
|
|
{
|
|
XmlNodeRef gameTypeNode = gameTypesNode->getChild(i);
|
|
|
|
if (gameTypeNode->isTag("Mission"))
|
|
{
|
|
const char* gameTypeName = gameTypeNode->getAttr("Name");
|
|
|
|
if (gameTypeName)
|
|
{
|
|
m_defaultGameTypeName = gameTypeName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return rootNode != 0;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
/// Used by console auto completion.
|
|
struct SLevelNameAutoComplete
|
|
: public IConsoleArgumentAutoComplete
|
|
{
|
|
AZStd::vector<AZStd::string> levels;
|
|
virtual int GetCount() const { return static_cast<int>(levels.size()); };
|
|
virtual const char* GetValue(int nIndex) const { return levels[nIndex].c_str(); };
|
|
};
|
|
// definition and declaration must be separated for devirtualization
|
|
static StaticInstance<SLevelNameAutoComplete, AZStd::no_destruct<SLevelNameAutoComplete>> g_LevelNameAutoComplete;
|
|
|
|
//------------------------------------------------------------------------
|
|
static void LoadMap(IConsoleCmdArgs* args)
|
|
{
|
|
if (gEnv->pSystem && gEnv->pSystem->GetILevelSystem() && !gEnv->IsEditor())
|
|
{
|
|
if (args->GetArgCount() > 1)
|
|
{
|
|
gEnv->pSystem->GetILevelSystem()->UnloadLevel();
|
|
gEnv->pSystem->GetILevelSystem()->LoadLevel(args->GetArg(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
static void UnloadMap([[maybe_unused]] IConsoleCmdArgs* args)
|
|
{
|
|
if (gEnv->pSystem && gEnv->pSystem->GetILevelSystem() && !gEnv->IsEditor())
|
|
{
|
|
gEnv->pSystem->GetILevelSystem()->UnloadLevel();
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
CLevelSystem::CLevelSystem(ISystem* pSystem, const char* levelsFolder)
|
|
: m_pSystem(pSystem)
|
|
, m_pCurrentLevel(0)
|
|
, m_pLoadingLevelInfo(0)
|
|
{
|
|
CRY_ASSERT(pSystem);
|
|
|
|
//if (!gEnv->IsEditor())
|
|
Rescan(levelsFolder);
|
|
|
|
m_fLastLevelLoadTime = 0;
|
|
m_fLastTime = 0;
|
|
m_bLevelLoaded = false;
|
|
|
|
m_levelLoadStartTime.SetValue(0);
|
|
|
|
m_nLoadedLevelsCount = 0;
|
|
|
|
REGISTER_COMMAND("map", LoadMap, VF_BLOCKFRAME, "Load a map");
|
|
REGISTER_COMMAND("unload", UnloadMap, 0, "Unload current map");
|
|
gEnv->pConsole->RegisterAutoComplete("map", &(*g_LevelNameAutoComplete));
|
|
|
|
AZ_Assert(gEnv && gEnv->pCryPak, "gEnv and CryPak must be initialized for loading levels.");
|
|
if (!gEnv || !gEnv->pCryPak)
|
|
{
|
|
return;
|
|
}
|
|
auto pPak = gEnv->pCryPak;
|
|
|
|
if (AZ::IO::IArchive::LevelPackOpenEvent* levelPakOpenEvent = pPak->GetLevelPackOpenEvent())
|
|
{
|
|
m_levelPackOpenHandler = AZ::IO::IArchive::LevelPackOpenEvent::Handler([this](const AZStd::vector<AZStd::string>& levelDirs)
|
|
{
|
|
for (AZStd::string dir : levelDirs)
|
|
{
|
|
AZ::StringFunc::Path::StripComponent(dir, true);
|
|
AZStd::string searchPattern = dir + AZ_FILESYSTEM_SEPARATOR_WILDCARD;
|
|
bool modFolder = false;
|
|
PopulateLevels(searchPattern, dir, gEnv->pCryPak, modFolder, false);
|
|
}
|
|
});
|
|
m_levelPackOpenHandler.Connect(*levelPakOpenEvent);
|
|
}
|
|
|
|
if (AZ::IO::IArchive::LevelPackCloseEvent* levelPakCloseEvent = pPak->GetLevelPackCloseEvent())
|
|
{
|
|
m_levelPackCloseHandler = AZ::IO::IArchive::LevelPackCloseEvent::Handler([this](AZStd::string_view)
|
|
{
|
|
Rescan(ILevelSystem::LevelsDirectoryName);
|
|
});
|
|
m_levelPackCloseHandler.Connect(*levelPakCloseEvent);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
CLevelSystem::~CLevelSystem()
|
|
{
|
|
UnloadLevel();
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::Rescan(const char* levelsFolder)
|
|
{
|
|
if (levelsFolder)
|
|
{
|
|
m_levelsFolder = levelsFolder;
|
|
}
|
|
|
|
CRY_ASSERT(!m_levelsFolder.empty());
|
|
m_levelInfos.clear();
|
|
m_levelInfos.reserve(64);
|
|
ScanFolder(0, false);
|
|
|
|
g_LevelNameAutoComplete->levels.clear();
|
|
for (int i = 0; i < (int)m_levelInfos.size(); i++)
|
|
{
|
|
g_LevelNameAutoComplete->levels.push_back(AZStd::string(PathUtil::GetFileName(m_levelInfos[i].GetName()).c_str()));
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------
|
|
void CLevelSystem::ScanFolder(const char* subfolder, bool modFolder)
|
|
{
|
|
AZStd::string folder;
|
|
if (subfolder && subfolder[0])
|
|
{
|
|
folder = subfolder;
|
|
}
|
|
|
|
AZStd::string search(m_levelsFolder);
|
|
if (!folder.empty())
|
|
{
|
|
if (AZ::StringFunc::StartsWith(folder.c_str(), m_levelsFolder.c_str()))
|
|
{
|
|
search = folder;
|
|
}
|
|
else
|
|
{
|
|
search += "/" + folder;
|
|
}
|
|
}
|
|
search += "/*";
|
|
|
|
AZ_Assert(gEnv && gEnv->pCryPak, "gEnv and must be initialized for loading levels.");
|
|
if (!gEnv || !gEnv->pCryPak)
|
|
{
|
|
return;
|
|
}
|
|
auto pPak = gEnv->pCryPak;
|
|
|
|
AZStd::unordered_set<AZStd::string> pakList;
|
|
|
|
AZ::IO::ArchiveFileIterator handle = pPak->FindFirst(search.c_str(), AZ::IO::IArchive::eFileSearchType_AllowOnDiskOnly);
|
|
|
|
if (handle)
|
|
{
|
|
do
|
|
{
|
|
AZStd::string extension;
|
|
AZStd::string levelName;
|
|
AZ::StringFunc::Path::Split(handle.m_filename.data(), nullptr, nullptr, &levelName, &extension);
|
|
if (extension == ArchiveExtension)
|
|
{
|
|
if (AZ::StringFunc::Equal(handle.m_filename.data(), LevelPakName))
|
|
{
|
|
// level folder contain pak files like 'level.pak'
|
|
// which we only want to load during level loading.
|
|
continue;
|
|
}
|
|
|
|
AZStd::string levelContainerPakPath;
|
|
AZ::StringFunc::Path::Join("@assets@", m_levelsFolder.c_str(), levelContainerPakPath);
|
|
if (subfolder && subfolder[0])
|
|
{
|
|
AZ::StringFunc::Path::Join(levelContainerPakPath.c_str(), subfolder, levelContainerPakPath);
|
|
}
|
|
AZ::StringFunc::Path::Join(levelContainerPakPath.c_str(), handle.m_filename.data(), levelContainerPakPath);
|
|
pakList.emplace(levelContainerPakPath);
|
|
continue;
|
|
}
|
|
} while (handle = pPak->FindNext(handle));
|
|
|
|
pPak->FindClose(handle);
|
|
}
|
|
|
|
// Open all the available paks found in the levels folder
|
|
for (auto iter = pakList.begin(); iter != pakList.end(); iter++)
|
|
{
|
|
AZStd::fixed_string<AZ::IO::IArchive::MaxPath> fullLevelPakPath;
|
|
gEnv->pCryPak->OpenPack(iter->c_str(), (unsigned)0, nullptr, &fullLevelPakPath, false);
|
|
}
|
|
|
|
// Levels in bundles now take priority over levels outside of bundles.
|
|
PopulateLevels(search, folder, pPak, modFolder, false);
|
|
// Load levels outside of the bundles to maintain backward compatibility.
|
|
PopulateLevels(search, folder, pPak, modFolder, true);
|
|
|
|
}
|
|
|
|
void CLevelSystem::PopulateLevels(
|
|
AZStd::string searchPattern, AZStd::string& folder, AZ::IO::IArchive* pPak, bool& modFolder, bool fromFileSystemOnly)
|
|
{
|
|
{
|
|
// allow this find first to actually touch the file system
|
|
// (causes small overhead but with minimal amount of levels this should only be around 150ms on actual DVD Emu)
|
|
AZ::IO::ArchiveFileIterator handle = pPak->FindFirst(searchPattern.c_str(), AZ::IO::IArchive::eFileSearchType_AllowOnDiskOnly);
|
|
|
|
if (handle)
|
|
{
|
|
do
|
|
{
|
|
if ((handle.m_fileDesc.nAttrib & AZ::IO::FileDesc::Attribute::Subdirectory) != AZ::IO::FileDesc::Attribute::Subdirectory ||
|
|
handle.m_filename == "." || handle.m_filename == "..")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AZStd::string levelFolder;
|
|
if (fromFileSystemOnly)
|
|
{
|
|
levelFolder =
|
|
(folder.empty() ? "" : (folder + "/")) + AZStd::string(handle.m_filename.data(), handle.m_filename.size());
|
|
}
|
|
else
|
|
{
|
|
AZStd::string levelName(AZ::IO::PathView(handle.m_filename).Filename().Native());
|
|
levelFolder = (folder.empty() ? "" : (folder + "/")) + levelName;
|
|
}
|
|
|
|
AZStd::string levelPath;
|
|
if (AZ::StringFunc::StartsWith(levelFolder.c_str(), m_levelsFolder.c_str()))
|
|
{
|
|
levelPath = levelFolder;
|
|
}
|
|
else
|
|
{
|
|
levelPath = m_levelsFolder + "/" + levelFolder;
|
|
}
|
|
|
|
const AZStd::string levelPakName = levelPath + "/" + LevelPakName;
|
|
const AZStd::string levelInfoName = levelPath + "/levelinfo.xml";
|
|
|
|
if (!pPak->IsFileExist(
|
|
levelPakName.c_str(),
|
|
fromFileSystemOnly ? AZ::IO::IArchive::eFileLocation_OnDisk : AZ::IO::IArchive::eFileLocation_InPak) &&
|
|
!pPak->IsFileExist(
|
|
levelInfoName.c_str(),
|
|
fromFileSystemOnly ? AZ::IO::IArchive::eFileLocation_OnDisk : AZ::IO::IArchive::eFileLocation_InPak))
|
|
{
|
|
ScanFolder(levelFolder.c_str(), modFolder);
|
|
continue;
|
|
}
|
|
|
|
// With the level.pak workflow, levelPath and levelName will point to a directory.
|
|
// levelPath: levels/mylevel
|
|
// levelName: mylevel
|
|
CLevelInfo levelInfo;
|
|
levelInfo.m_levelPath = levelPath;
|
|
levelInfo.m_levelName = levelFolder;
|
|
levelInfo.m_isPak = !fromFileSystemOnly;
|
|
|
|
CLevelInfo* pExistingInfo = GetLevelInfoInternal(levelInfo.m_levelName);
|
|
|
|
// Don't add the level if it is already in the list
|
|
if (pExistingInfo == NULL)
|
|
{
|
|
m_levelInfos.push_back(levelInfo);
|
|
}
|
|
else
|
|
{
|
|
// Levels in bundles take priority over levels outside bundles.
|
|
if (!pExistingInfo->m_isPak && levelInfo.m_isPak)
|
|
{
|
|
*pExistingInfo = levelInfo;
|
|
}
|
|
}
|
|
} while (handle = pPak->FindNext(handle));
|
|
|
|
pPak->FindClose(handle);
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
int CLevelSystem::GetLevelCount()
|
|
{
|
|
return (int)m_levelInfos.size();
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
ILevelInfo* CLevelSystem::GetLevelInfo(int level)
|
|
{
|
|
return GetLevelInfoInternal(level);
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
CLevelInfo* CLevelSystem::GetLevelInfoInternal(int level)
|
|
{
|
|
if ((level >= 0) && (level < GetLevelCount()))
|
|
{
|
|
return &m_levelInfos[level];
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
ILevelInfo* CLevelSystem::GetLevelInfo(const char* levelName)
|
|
{
|
|
return GetLevelInfoInternal(levelName);
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
CLevelInfo* CLevelSystem::GetLevelInfoInternal(const AZStd::string& levelName)
|
|
{
|
|
// If level not found by full name try comparing with only filename
|
|
for (AZStd::vector<CLevelInfo>::iterator it = m_levelInfos.begin(); it != m_levelInfos.end(); ++it)
|
|
{
|
|
if (!azstricmp(it->GetName(), levelName.c_str()))
|
|
{
|
|
return &(*it);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
for (AZStd::vector<CLevelInfo>::iterator it = m_levelInfos.begin(); it != m_levelInfos.end(); ++it)
|
|
{
|
|
{
|
|
if (!azstricmp(PathUtil::GetFileName(it->GetName()).c_str(), levelName.c_str()))
|
|
{
|
|
return &(*it);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try stripping out the folder to find the raw filename
|
|
AZStd::string sLevelName(levelName);
|
|
size_t lastSlash = sLevelName.find_last_of('\\');
|
|
if (lastSlash == AZStd::string::npos)
|
|
{
|
|
lastSlash = sLevelName.find_last_of('/');
|
|
}
|
|
if (lastSlash != AZStd::string::npos)
|
|
{
|
|
sLevelName = sLevelName.substr(lastSlash + 1, sLevelName.size() - lastSlash - 1);
|
|
return GetLevelInfoInternal(sLevelName.c_str());
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::AddListener(ILevelSystemListener* pListener)
|
|
{
|
|
AZStd::vector<ILevelSystemListener*>::iterator it = AZStd::find(m_listeners.begin(), m_listeners.end(), pListener);
|
|
|
|
if (it == m_listeners.end())
|
|
{
|
|
m_listeners.reserve(12);
|
|
m_listeners.push_back(pListener);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::RemoveListener(ILevelSystemListener* pListener)
|
|
{
|
|
AZStd::vector<ILevelSystemListener*>::iterator it = AZStd::find(m_listeners.begin(), m_listeners.end(), pListener);
|
|
|
|
if (it != m_listeners.end())
|
|
{
|
|
m_listeners.erase(it);
|
|
|
|
if (m_listeners.empty())
|
|
{
|
|
m_listeners.shrink_to_fit();
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
bool CLevelSystem::LoadLevel(const char* _levelName)
|
|
{
|
|
if (gEnv->IsEditor())
|
|
{
|
|
AZ_TracePrintf("CrySystem::CLevelSystem", "LoadLevel for %s was called in the editor - not actually loading.\n", _levelName);
|
|
return false;
|
|
}
|
|
|
|
// If a level is currently loaded, unload it before loading the next one.
|
|
if (IsLevelLoaded())
|
|
{
|
|
UnloadLevel();
|
|
}
|
|
|
|
gEnv->pSystem->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_LEVEL_LOAD_PREPARE, 0, 0);
|
|
PrepareNextLevel(_levelName);
|
|
|
|
ILevel* level = LoadLevelInternal(_levelName);
|
|
if (level)
|
|
{
|
|
OnLoadingComplete(_levelName);
|
|
}
|
|
|
|
return (level != nullptr);
|
|
}
|
|
//------------------------------------------------------------------------
|
|
ILevel* CLevelSystem::LoadLevelInternal(const char* _levelName)
|
|
{
|
|
gEnv->pSystem->SetSystemGlobalState(ESYSTEM_GLOBAL_STATE_LEVEL_LOAD_START);
|
|
AZ_ASSET_NAMED_SCOPE("Level: %s", _levelName);
|
|
|
|
CryLog ("Level system is loading \"%s\"", _levelName);
|
|
INDENT_LOG_DURING_SCOPE();
|
|
|
|
char levelName[256];
|
|
azstrcpy(levelName, AZ_ARRAY_SIZE(levelName), _levelName);
|
|
|
|
// Not remove a scope!!!
|
|
{
|
|
//m_levelLoadStartTime = gEnv->pTimer->GetAsyncTime();
|
|
|
|
CLevelInfo* pLevelInfo = GetLevelInfoInternal(levelName);
|
|
|
|
if (!pLevelInfo)
|
|
{
|
|
// alert the listener
|
|
OnLevelNotFound(levelName);
|
|
|
|
return 0;
|
|
}
|
|
|
|
m_bLevelLoaded = false;
|
|
|
|
m_lastLevelName = levelName;
|
|
|
|
delete m_pCurrentLevel;
|
|
CLevel* pLevel = new CLevel();
|
|
pLevel->m_levelInfo = *pLevelInfo;
|
|
m_pCurrentLevel = pLevel;
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Read main level info.
|
|
if (!pLevelInfo->ReadInfo())
|
|
{
|
|
OnLoadingError(levelName, "Failed to read level info (level.pak might be corrupted)!");
|
|
return 0;
|
|
}
|
|
//[AlexMcC|19.04.10]: Update the level's LevelInfo
|
|
pLevel->m_levelInfo = *pLevelInfo;
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
gEnv->pConsole->SetScrollMax(600);
|
|
ICVar* con_showonload = gEnv->pConsole->GetCVar("con_showonload");
|
|
if (con_showonload && con_showonload->GetIVal() != 0)
|
|
{
|
|
gEnv->pConsole->ShowConsole(true);
|
|
ICVar* g_enableloadingscreen = gEnv->pConsole->GetCVar("g_enableloadingscreen");
|
|
if (g_enableloadingscreen)
|
|
{
|
|
g_enableloadingscreen->Set(0);
|
|
}
|
|
}
|
|
|
|
m_pLoadingLevelInfo = pLevelInfo;
|
|
OnLoadingStart(levelName);
|
|
|
|
|
|
auto pPak = gEnv->pCryPak;
|
|
|
|
AZStd::string levelPath(pLevelInfo->GetPath());
|
|
|
|
ICVar* pSpamDelay = gEnv->pConsole->GetCVar("log_SpamDelay");
|
|
float spamDelay = 0.0f;
|
|
if (pSpamDelay)
|
|
{
|
|
spamDelay = pSpamDelay->GetFVal();
|
|
pSpamDelay->Set(0.0f);
|
|
}
|
|
|
|
{
|
|
AZStd::string missionXml("Mission_");
|
|
missionXml += pLevelInfo->m_defaultGameTypeName;
|
|
missionXml += ".xml";
|
|
AZStd::string xmlFile(pLevelInfo->GetPath());
|
|
xmlFile += "/";
|
|
xmlFile += missionXml;
|
|
|
|
if (!gEnv->IsEditor())
|
|
{
|
|
AZStd::string entitiesFilename =
|
|
AZStd::string::format("%s/%s.entities_xml", pLevelInfo->GetPath(), pLevelInfo->m_defaultGameTypeName.c_str());
|
|
AZStd::vector<char> fileBuffer;
|
|
CCryFile entitiesFile;
|
|
if (entitiesFile.Open(entitiesFilename.c_str(), "rt"))
|
|
{
|
|
fileBuffer.resize(entitiesFile.GetLength());
|
|
|
|
if (fileBuffer.size() == entitiesFile.ReadRaw(fileBuffer.begin(), fileBuffer.size()))
|
|
{
|
|
AZ::IO::ByteContainerStream<AZStd::vector<char>> fileStream(&fileBuffer);
|
|
EBUS_EVENT(AzFramework::GameEntityContextRequestBus, LoadFromStream, fileStream, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// Movie system must be reset after entities.
|
|
//////////////////////////////////////////////////////////////////////////
|
|
IMovieSystem* movieSys = gEnv->pMovieSystem;
|
|
if (movieSys != NULL)
|
|
{
|
|
// bSeekAllToStart needs to be false here as it's only of interest in the editor
|
|
movieSys->Reset(true, false);
|
|
}
|
|
|
|
gEnv->pSystem->SetSystemGlobalState(ESYSTEM_GLOBAL_STATE_LEVEL_LOAD_START_PRECACHE);
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
//////////////////////////////////////////////////////////////////////////
|
|
gEnv->pConsole->SetScrollMax(600 / 2);
|
|
|
|
pPak->GetResourceList(AZ::IO::IArchive::RFOM_NextLevel)->Clear();
|
|
|
|
if (pSpamDelay)
|
|
{
|
|
pSpamDelay->Set(spamDelay);
|
|
}
|
|
|
|
m_bLevelLoaded = true;
|
|
gEnv->pSystem->SetSystemGlobalState(ESYSTEM_GLOBAL_STATE_LEVEL_LOAD_END);
|
|
}
|
|
|
|
GetISystem()->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_LEVEL_LOAD_END, 0, 0);
|
|
|
|
if (auto cvar = gEnv->pConsole->GetCVar("sv_map"); cvar)
|
|
{
|
|
cvar->Set(levelName);
|
|
}
|
|
|
|
gEnv->pSystem->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_LEVEL_PRECACHE_START, 0, 0);
|
|
|
|
return m_pCurrentLevel;
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::PrepareNextLevel(const char* levelName)
|
|
{
|
|
CLevelInfo* pLevelInfo = GetLevelInfoInternal(levelName);
|
|
if (!pLevelInfo)
|
|
{
|
|
// alert the listener
|
|
OnLevelNotFound(levelName);
|
|
return;
|
|
}
|
|
|
|
// This work not required in-editor.
|
|
if (!gEnv || !gEnv->IsEditor())
|
|
{
|
|
m_levelLoadStartTime = gEnv->pTimer->GetAsyncTime();
|
|
|
|
// Open pak file for a new level.
|
|
pLevelInfo->OpenLevelPak();
|
|
|
|
// switched to level heap, so now imm start the loading screen (renderer will be reinitialized in the levelheap)
|
|
gEnv->pSystem->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_LEVEL_LOAD_START_LOADINGSCREEN, 0, 0);
|
|
gEnv->pSystem->SetSystemGlobalState(ESYSTEM_GLOBAL_STATE_LEVEL_LOAD_START_PREPARE);
|
|
}
|
|
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnPrepareNextLevel(pLevelInfo->GetName());
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::OnLevelNotFound(const char* levelName)
|
|
{
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnLevelNotFound(levelName);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::OnLoadingStart(const char* levelName)
|
|
{
|
|
if (gEnv->pCryPak->GetRecordFileOpenList() == AZ::IO::IArchive::RFOM_EngineStartup)
|
|
{
|
|
gEnv->pCryPak->RecordFileOpen(AZ::IO::IArchive::RFOM_Level);
|
|
}
|
|
|
|
m_fLastTime = gEnv->pTimer->GetAsyncCurTime();
|
|
|
|
GetISystem()->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_LEVEL_LOAD_START, 0, 0);
|
|
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnLoadingStart(levelName);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::OnLoadingError(const char* levelName, const char* error)
|
|
{
|
|
ILevelInfo* pLevelInfo = m_pLoadingLevelInfo;
|
|
if (!pLevelInfo)
|
|
{
|
|
CRY_ASSERT(false);
|
|
return;
|
|
}
|
|
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnLoadingError(levelName, error);
|
|
}
|
|
|
|
((CLevelInfo*)pLevelInfo)->CloseLevelPak();
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::OnLoadingComplete(const char* levelName)
|
|
{
|
|
CTimeValue t = gEnv->pTimer->GetAsyncTime();
|
|
m_fLastLevelLoadTime = (t - m_levelLoadStartTime).GetSeconds();
|
|
|
|
LogLoadingTime();
|
|
|
|
m_nLoadedLevelsCount++;
|
|
|
|
// Hide console after loading.
|
|
gEnv->pConsole->ShowConsole(false);
|
|
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnLoadingComplete(levelName);
|
|
}
|
|
|
|
#if AZ_LOADSCREENCOMPONENT_ENABLED
|
|
EBUS_EVENT(LoadScreenBus, Stop);
|
|
#endif // if AZ_LOADSCREENCOMPONENT_ENABLED
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::OnLoadingProgress(const char* levelName, int progressAmount)
|
|
{
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnLoadingProgress(levelName, progressAmount);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------
|
|
void CLevelSystem::OnUnloadComplete(const char* levelName)
|
|
{
|
|
for (AZStd::vector<ILevelSystemListener*>::const_iterator it = m_listeners.begin(); it != m_listeners.end(); ++it)
|
|
{
|
|
(*it)->OnUnloadComplete(levelName);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
void CLevelSystem::LogLoadingTime()
|
|
{
|
|
if (gEnv->IsEditor())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!GetISystem()->IsDevMode())
|
|
{
|
|
return;
|
|
}
|
|
|
|
char vers[128];
|
|
GetISystem()->GetFileVersion().ToString(vers, sizeof(vers));
|
|
|
|
const char* sChain = "";
|
|
if (m_nLoadedLevelsCount > 0)
|
|
{
|
|
sChain = " (Chained)";
|
|
}
|
|
|
|
gEnv->pLog->Log("Game Level Load Time: [%s] Level %s loaded in %.2f seconds%s", vers, m_lastLevelName.c_str(), m_fLastLevelLoadTime, sChain);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
void CLevelSystem::UnloadLevel()
|
|
{
|
|
if (gEnv->IsEditor())
|
|
{
|
|
return;
|
|
}
|
|
if (!m_pLoadingLevelInfo)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CryLog("UnloadLevel Start");
|
|
INDENT_LOG_DURING_SCOPE();
|
|
|
|
// Flush core buses. We're about to unload Cry modules and need to ensure we don't have module-owned functions left behind.
|
|
AZ::Data::AssetBus::ExecuteQueuedEvents();
|
|
AZ::TickBus::ExecuteQueuedEvents();
|
|
AZ::MainThreadRenderRequestBus::ExecuteQueuedEvents();
|
|
|
|
if (gEnv && gEnv->pSystem)
|
|
{
|
|
// clear all error messages to prevent stalling due to runtime file access check during chainloading
|
|
gEnv->pSystem->ClearErrorMessages();
|
|
}
|
|
|
|
if (gEnv && gEnv->pCryPak)
|
|
{
|
|
gEnv->pCryPak->DisableRuntimeFileAccess(false);
|
|
}
|
|
|
|
CTimeValue tBegin = gEnv->pTimer->GetAsyncTime();
|
|
|
|
// Clear level entities and prefab instances.
|
|
EBUS_EVENT(AzFramework::GameEntityContextRequestBus, ResetGameContext);
|
|
|
|
if (gEnv->pMovieSystem)
|
|
{
|
|
gEnv->pMovieSystem->Reset(false, false);
|
|
gEnv->pMovieSystem->RemoveAllSequences();
|
|
}
|
|
|
|
OnUnloadComplete(m_lastLevelName.c_str());
|
|
|
|
// -- kenzo: this will close all pack files for this level
|
|
// (even the ones which were not added through here, if this is not desired,
|
|
// then change code to close only level.pak)
|
|
if (m_pLoadingLevelInfo)
|
|
{
|
|
((CLevelInfo*)m_pLoadingLevelInfo)->CloseLevelPak();
|
|
m_pLoadingLevelInfo = NULL;
|
|
}
|
|
|
|
m_lastLevelName.clear();
|
|
|
|
SAFE_RELEASE(m_pCurrentLevel);
|
|
|
|
// Force Lua garbage collection (may no longer be needed now the legacy renderer has been removed).
|
|
// Normally the GC step is triggered at the end of this method (by the ESYSTEM_EVENT_LEVEL_POST_UNLOAD event).
|
|
EBUS_EVENT(AZ::ScriptSystemRequestBus, GarbageCollect);
|
|
|
|
// Perform level unload procedures for the LyShine UI system
|
|
if (gEnv && gEnv->pLyShine)
|
|
{
|
|
gEnv->pLyShine->OnLevelUnload();
|
|
}
|
|
|
|
m_bLevelLoaded = false;
|
|
|
|
CTimeValue tUnloadTime = gEnv->pTimer->GetAsyncTime() - tBegin;
|
|
CryLog("UnloadLevel End: %.1f sec", tUnloadTime.GetSeconds());
|
|
|
|
// Must be sent last.
|
|
// Cleanup all containers
|
|
GetISystem()->GetISystemEventDispatcher()->OnSystemEvent(ESYSTEM_EVENT_LEVEL_POST_UNLOAD, 0, 0);
|
|
AzFramework::InputChannelRequestBus::Broadcast(&AzFramework::InputChannelRequests::ResetState);
|
|
}
|
|
|
|
} // namespace LegacyLevelSystem
|