Merge branch 'main' of https://github.com/aws-lumberyard/o3de into cgalvan/DuplicateEntities

This commit is contained in:
Chris Galvan
2021-05-20 10:40:50 -05:00
561 changed files with 15490 additions and 13213 deletions
+900
View File
@@ -0,0 +1,900 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
#include "CrySystem_precompiled.h"
#include "DebugCallStack.h"
#if defined(WIN32) || defined(WIN64)
#include <IConsole.h>
#include <CryPath.h>
#include "System.h"
#include <AzCore/Debug/StackTracer.h>
#include <AzCore/Debug/EventTraceDrillerBus.h>
#define VS_VERSION_INFO 1
#define IDD_CRITICAL_ERROR 101
#define IDB_CONFIRM_SAVE 102
#define IDB_DONT_SAVE 103
#define IDD_CONFIRM_SAVE_LEVEL 127
#define IDB_CRASH_FACE 128
#define IDD_EXCEPTION 245
#define IDC_CALLSTACK 1001
#define IDC_EXCEPTION_CODE 1002
#define IDC_EXCEPTION_ADDRESS 1003
#define IDC_EXCEPTION_MODULE 1004
#define IDC_EXCEPTION_DESC 1005
#define IDB_EXIT 1008
#define IDB_IGNORE 1010
__pragma(comment(lib, "version.lib"))
//! Needs one external of DLL handle.
extern HMODULE gDLLHandle;
#include <DbgHelp.h>
#define MAX_PATH_LENGTH 1024
#define MAX_SYMBOL_LENGTH 512
static HWND hwndException = 0;
static bool g_bUserDialog = true; // true=on crash show dialog box, false=supress user interaction
static int PrintException(EXCEPTION_POINTERS* pex);
static bool IsFloatingPointException(EXCEPTION_POINTERS* pex);
extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers);
extern LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE mdumpValue);
//=============================================================================
CONTEXT CaptureCurrentContext()
{
CONTEXT context;
memset(&context, 0, sizeof(context));
context.ContextFlags = CONTEXT_FULL;
RtlCaptureContext(&context);
return context;
}
LONG __stdcall CryUnhandledExceptionHandler(EXCEPTION_POINTERS* pex)
{
return DebugCallStack::instance()->handleException(pex);
}
BOOL CALLBACK EnumModules(
PCSTR ModuleName,
DWORD64 BaseOfDll,
PVOID UserContext)
{
DebugCallStack::TModules& modules = *static_cast<DebugCallStack::TModules*>(UserContext);
modules[(void*)BaseOfDll] = ModuleName;
return TRUE;
}
//=============================================================================
// Class Statics
//=============================================================================
// Return single instance of class.
IDebugCallStack* IDebugCallStack::instance()
{
static DebugCallStack sInstance;
return &sInstance;
}
//------------------------------------------------------------------------------------------------------------------------
// Sets up the symbols for functions in the debug file.
//------------------------------------------------------------------------------------------------------------------------
DebugCallStack::DebugCallStack()
: prevExceptionHandler(0)
, m_pSystem(0)
, m_nSkipNumFunctions(0)
, m_bCrash(false)
, m_szBugMessage(NULL)
{
}
DebugCallStack::~DebugCallStack()
{
}
void DebugCallStack::RemoveOldFiles()
{
RemoveFile("error.log");
RemoveFile("error.bmp");
RemoveFile("error.dmp");
}
void DebugCallStack::RemoveFile(const char* szFileName)
{
FILE* pFile = nullptr;
azfopen(&pFile, szFileName, "r");
const bool bFileExists = (pFile != NULL);
if (bFileExists)
{
fclose(pFile);
WriteLineToLog("Removing file \"%s\"...", szFileName);
if (remove(szFileName) == 0)
{
WriteLineToLog("File successfully removed.");
}
else
{
WriteLineToLog("Couldn't remove file!");
}
}
}
void DebugCallStack::installErrorHandler(ISystem* pSystem)
{
m_pSystem = pSystem;
prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler);
}
//////////////////////////////////////////////////////////////////////////
void DebugCallStack::SetUserDialogEnable(const bool bUserDialogEnable)
{
g_bUserDialog = bUserDialogEnable;
}
DWORD g_idDebugThreads[10];
const char* g_nameDebugThreads[10];
int g_nDebugThreads = 0;
volatile int g_lockThreadDumpList = 0;
void MarkThisThreadForDebugging(const char* name)
{
EBUS_EVENT(AZ::Debug::EventTraceDrillerSetupBus, SetThreadName, AZStd::this_thread::get_id(), name);
WriteLock lock(g_lockThreadDumpList);
DWORD id = GetCurrentThreadId();
if (g_nDebugThreads == sizeof(g_idDebugThreads) / sizeof(g_idDebugThreads[0]))
{
return;
}
for (int i = 0; i < g_nDebugThreads; i++)
{
if (g_idDebugThreads[i] == id)
{
return;
}
}
g_nameDebugThreads[g_nDebugThreads] = name;
g_idDebugThreads[g_nDebugThreads++] = id;
((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions);
}
void UnmarkThisThreadFromDebugging()
{
WriteLock lock(g_lockThreadDumpList);
DWORD id = GetCurrentThreadId();
for (int i = g_nDebugThreads - 1; i >= 0; i--)
{
if (g_idDebugThreads[i] == id)
{
memmove(g_idDebugThreads + i, g_idDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_idDebugThreads[0]));
memmove(g_nameDebugThreads + i, g_nameDebugThreads + i + 1, (g_nDebugThreads - 1 - i) * sizeof(g_nameDebugThreads[0]));
--g_nDebugThreads;
}
}
}
extern int prev_sys_float_exceptions;
void UpdateFPExceptionsMaskForThreads()
{
int mask = -iszero(g_cvars.sys_float_exceptions);
CONTEXT ctx;
for (int i = 0; i < g_nDebugThreads; i++)
{
if (g_idDebugThreads[i] != GetCurrentThreadId())
{
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]);
ctx.ContextFlags = CONTEXT_ALL;
SuspendThread(hThread);
GetThreadContext(hThread, &ctx);
#ifndef WIN64
(ctx.FloatSave.ControlWord |= 7) &= ~5 | mask;
(*(WORD*)(ctx.ExtendedRegisters + 24) |= 0x280) &= ~0x280 | mask;
#else
(ctx.FltSave.ControlWord |= 7) &= ~5 | mask;
(ctx.FltSave.MxCsr |= 0x280) &= ~0x280 | mask;
#endif
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
}
}
}
//////////////////////////////////////////////////////////////////////////
int DebugCallStack::handleException(EXCEPTION_POINTERS* exception_pointer)
{
if (gEnv == NULL)
{
return EXCEPTION_EXECUTE_HANDLER;
}
ResetFPU(exception_pointer);
prev_sys_float_exceptions = 0;
const int cached_sys_float_exceptions = g_cvars.sys_float_exceptions;
((CSystem*)gEnv->pSystem)->EnableFloatExceptions(0);
if (g_cvars.sys_WER)
{
gEnv->pLog->FlushAndClose();
return CryEngineExceptionFilterWER(exception_pointer);
}
if (g_cvars.sys_no_crash_dialog)
{
DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX);
}
m_bCrash = true;
if (g_cvars.sys_no_crash_dialog)
{
DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX);
}
static bool firstTime = true;
if (g_cvars.sys_dump_aux_threads)
{
for (int i = 0; i < g_nDebugThreads; i++)
{
if (g_idDebugThreads[i] != GetCurrentThreadId())
{
SuspendThread(OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]));
}
}
}
// uninstall our exception handler.
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)prevExceptionHandler);
if (!firstTime)
{
WriteLineToLog("Critical Exception! Called Multiple Times!");
gEnv->pLog->FlushAndClose();
// Exception called more then once.
return EXCEPTION_EXECUTE_HANDLER;
}
// Print exception info:
{
char excCode[80];
char excAddr[80];
WriteLineToLog("<CRITICAL EXCEPTION>");
sprintf_s(excAddr, "0x%04X:0x%p", exception_pointer->ContextRecord->SegCs, exception_pointer->ExceptionRecord->ExceptionAddress);
sprintf_s(excCode, "0x%08X", exception_pointer->ExceptionRecord->ExceptionCode);
WriteLineToLog("Exception: %s, at Address: %s", excCode, excAddr);
}
firstTime = false;
const int ret = SubmitBug(exception_pointer);
if (ret != IDB_IGNORE)
{
CryEngineExceptionFilterWER(exception_pointer);
}
gEnv->pLog->FlushAndClose();
if (exception_pointer->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
{
// This is non continuable exception. abort application now.
exit(exception_pointer->ExceptionRecord->ExceptionCode);
}
//typedef long (__stdcall *ExceptionFunc)(EXCEPTION_POINTERS*);
//ExceptionFunc prevFunc = (ExceptionFunc)prevExceptionHandler;
//return prevFunc( (EXCEPTION_POINTERS*)exception_pointer );
if (ret == IDB_EXIT)
{
// Immediate exit.
// on windows, exit() and _exit() do all sorts of things, unfortuantely
// TerminateProcess is the only way to die.
TerminateProcess(GetCurrentProcess(), exception_pointer->ExceptionRecord->ExceptionCode); // we crashed, so don't return a zero exit code!
// on linux based systems, _exit will not call ATEXIT and other things, which makes it more suitable for termination in an emergency such
// as an unhandled exception.
// however, this function is a windows exception handler.
}
else if (ret == IDB_IGNORE)
{
#ifndef WIN64
exception_pointer->ContextRecord->FloatSave.StatusWord &= ~31;
exception_pointer->ContextRecord->FloatSave.ControlWord |= 7;
(*(WORD*)(exception_pointer->ContextRecord->ExtendedRegisters + 24) &= 31) |= 0x1F80;
#else
exception_pointer->ContextRecord->FltSave.StatusWord &= ~31;
exception_pointer->ContextRecord->FltSave.ControlWord |= 7;
(exception_pointer->ContextRecord->FltSave.MxCsr &= 31) |= 0x1F80;
#endif
firstTime = true;
prevExceptionHandler = (void*)SetUnhandledExceptionFilter(CryUnhandledExceptionHandler);
g_cvars.sys_float_exceptions = cached_sys_float_exceptions;
((CSystem*)gEnv->pSystem)->EnableFloatExceptions(g_cvars.sys_float_exceptions);
return EXCEPTION_CONTINUE_EXECUTION;
}
// Continue;
return EXCEPTION_EXECUTE_HANDLER;
}
void DebugCallStack::ReportBug(const char* szErrorMessage)
{
WriteLineToLog("Reporting bug: %s", szErrorMessage);
m_szBugMessage = szErrorMessage;
m_context = CaptureCurrentContext();
SubmitBug(NULL);
m_szBugMessage = NULL;
}
void DebugCallStack::dumpCallStack(std::vector<string>& funcs)
{
WriteLineToLog("=============================================================================");
int len = (int)funcs.size();
for (int i = 0; i < len; i++)
{
const char* str = funcs[i].c_str();
WriteLineToLog("%2d) %s", len - i, str);
}
WriteLineToLog("=============================================================================");
}
//////////////////////////////////////////////////////////////////////////
void DebugCallStack::LogExceptionInfo(EXCEPTION_POINTERS* pex)
{
string path("");
if ((gEnv) && (gEnv->pFileIO))
{
const char* logAlias = gEnv->pFileIO->GetAlias("@log@");
if (!logAlias)
{
logAlias = gEnv->pFileIO->GetAlias("@root@");
}
if (logAlias)
{
path = logAlias;
path += "/";
}
}
string fileName = path;
fileName += "error.log";
struct stat fileInfo;
string timeStamp;
string backupPath;
if (gEnv->IsDedicated())
{
backupPath = PathUtil::ToUnixPath(PathUtil::AddSlash(path + "DumpBackups"));
gEnv->pFileIO->CreatePath(backupPath.c_str());
if (stat(fileName.c_str(), &fileInfo) == 0)
{
// Backup log
tm creationTime;
localtime_s(&creationTime, &fileInfo.st_mtime);
char tempBuffer[32];
strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime);
timeStamp = tempBuffer;
string backupFileName = backupPath + timeStamp + " error.log";
CopyFile(fileName.c_str(), backupFileName.c_str(), true);
}
}
FILE* f = nullptr;
azfopen(&f, fileName.c_str(), "wt");
static char errorString[s_iCallStackSize];
errorString[0] = 0;
// Time and Version.
char versionbuf[1024];
azstrcpy(versionbuf, AZ_ARRAY_SIZE(versionbuf), "");
PutVersion(versionbuf, AZ_ARRAY_SIZE(versionbuf));
cry_strcat(errorString, versionbuf);
cry_strcat(errorString, "\n");
char excCode[MAX_WARNING_LENGTH];
char excAddr[80];
char desc[1024];
char excDesc[MAX_WARNING_LENGTH];
// make sure the mouse cursor is visible
ShowCursor(TRUE);
const char* excName;
if (m_bIsFatalError || !pex)
{
const char* const szMessage = m_bIsFatalError ? s_szFatalErrorCode : m_szBugMessage;
excName = szMessage;
cry_strcpy(excCode, szMessage);
cry_strcpy(excAddr, "");
cry_strcpy(desc, "");
cry_strcpy(m_excModule, "");
cry_strcpy(excDesc, szMessage);
}
else
{
sprintf_s(excAddr, "0x%04X:0x%p", pex->ContextRecord->SegCs, pex->ExceptionRecord->ExceptionAddress);
sprintf_s(excCode, "0x%08X", pex->ExceptionRecord->ExceptionCode);
excName = TranslateExceptionCode(pex->ExceptionRecord->ExceptionCode);
cry_strcpy(desc, "");
sprintf_s(excDesc, "%s\r\n%s", excName, desc);
if (pex->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
if (pex->ExceptionRecord->NumberParameters > 1)
{
ULONG_PTR iswrite = pex->ExceptionRecord->ExceptionInformation[0];
DWORD64 accessAddr = pex->ExceptionRecord->ExceptionInformation[1];
if (iswrite)
{
sprintf_s(desc, "Attempt to write data to address 0x%08llu\r\nThe memory could not be \"written\"", accessAddr);
}
else
{
sprintf_s(desc, "Attempt to read from address 0x%08llu\r\nThe memory could not be \"read\"", accessAddr);
}
}
}
}
WriteLineToLog("Exception Code: %s", excCode);
WriteLineToLog("Exception Addr: %s", excAddr);
WriteLineToLog("Exception Module: %s", m_excModule);
WriteLineToLog("Exception Name : %s", excName);
WriteLineToLog("Exception Description: %s", desc);
cry_strcpy(m_excDesc, excDesc);
cry_strcpy(m_excAddr, excAddr);
cry_strcpy(m_excCode, excCode);
char errs[32768];
sprintf_s(errs, "Exception Code: %s\nException Addr: %s\nException Module: %s\nException Description: %s, %s\n",
excCode, excAddr, m_excModule, excName, desc);
cry_strcat(errs, "\nCall Stack Trace:\n");
std::vector<string> funcs;
{
AZ::Debug::StackFrame frames[25];
AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)];
unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 3);
if (numFrames)
{
AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines);
for (unsigned int i = 0; i < numFrames; i++)
{
funcs.push_back(lines[i]);
}
}
dumpCallStack(funcs);
// Fill call stack.
char str[s_iCallStackSize];
cry_strcpy(str, "");
for (unsigned int i = 0; i < funcs.size(); i++)
{
char temp[s_iCallStackSize];
sprintf_s(temp, "%2zd) %s", funcs.size() - i, (const char*)funcs[i].c_str());
cry_strcat(str, temp);
cry_strcat(str, "\r\n");
cry_strcat(errs, temp);
cry_strcat(errs, "\n");
}
cry_strcpy(m_excCallstack, str);
}
cry_strcat(errorString, errs);
if (f)
{
fwrite(errorString, strlen(errorString), 1, f);
{
if (g_cvars.sys_dump_aux_threads)
{
for (int i = 0; i < g_nDebugThreads; i++)
{
if (g_idDebugThreads[i] != GetCurrentThreadId())
{
fprintf(f, "\n\nSuspended thread (%s):\n", g_nameDebugThreads[i]);
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, TRUE, g_idDebugThreads[i]);
// mirrors the AZ::Debug::Trace::PrintCallstack() functionality, but prints to a file
{
AZ::Debug::StackFrame frames[10];
// Without StackFrame explicit alignment frames array is aligned to 4 bytes
// which causes the stack tracing to fail.
AZ::Debug::SymbolStorage::StackLine lines[AZ_ARRAY_SIZE(frames)];
unsigned int numFrames = AZ::Debug::StackRecorder::Record(frames, AZ_ARRAY_SIZE(frames), 0, hThread);
if (numFrames)
{
AZ::Debug::SymbolStorage::DecodeFrames(frames, numFrames, lines);
for (unsigned int i2 = 0; i2 < numFrames; ++i2)
{
fprintf(f, "%2d) %s\n", numFrames - i2, lines[i2]);
}
}
}
ResumeThread(hThread);
}
}
}
}
fflush(f);
fclose(f);
}
if (pex)
{
MINIDUMP_TYPE mdumpValue;
bool bDump = true;
switch (g_cvars.sys_dump_type)
{
case 0:
bDump = false;
break;
case 1:
mdumpValue = MiniDumpNormal;
break;
case 2:
mdumpValue = (MINIDUMP_TYPE)(MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithDataSegs);
break;
case 3:
mdumpValue = MiniDumpWithFullMemory;
break;
default:
mdumpValue = (MINIDUMP_TYPE)g_cvars.sys_dump_type;
break;
}
if (bDump)
{
fileName = path + "error.dmp";
if (gEnv->IsDedicated() && stat(fileName.c_str(), &fileInfo) == 0)
{
// Backup dump (use timestamp from error.log if available)
if (timeStamp.empty())
{
tm creationTime;
localtime_s(&creationTime, &fileInfo.st_mtime);
char tempBuffer[32];
strftime(tempBuffer, sizeof(tempBuffer), "%d %b %Y (%H %M %S)", &creationTime);
timeStamp = tempBuffer;
}
string backupFileName = backupPath + timeStamp + " error.dmp";
CopyFile(fileName.c_str(), backupFileName.c_str(), true);
}
CryEngineExceptionFilterMiniDump(pex, fileName.c_str(), mdumpValue);
}
}
//if no crash dialog don't even submit the bug
if (m_postBackupProcess && g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog)
{
m_postBackupProcess();
}
else
{
// lawsonn: Disabling the JIRA-based crash reporter for now
// we'll need to deal with it our own way, pending QA.
// if you're customizing the engine this is also your opportunity to deal with it.
if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog)
{
// ------------ place custom crash handler here ---------------------
// it should launch an executable!
/// by this time, error.bmp will be in the engine root folder
// error.log and error.dmp will also be present in the engine root folder
// if your error dumper wants those, it should zip them up and send them or offer to do so.
// ------------------------------------------------------------------
}
}
const bool bQuitting = !gEnv || !gEnv->pSystem || gEnv->pSystem->IsQuitting();
//[AlexMcC|16.04.10] When the engine is shutting down, MessageBox doesn't display a box
// and immediately returns IDYES. Avoid this by just not trying to save if we're quitting.
// Don't ask to save if this isn't a real crash (a real crash has exception pointers)
if (g_cvars.sys_no_crash_dialog == 0 && g_bUserDialog && gEnv->IsEditor() && !bQuitting && pex)
{
BackupCurrentLevel();
const INT_PTR res = DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CONFIRM_SAVE_LEVEL), NULL, DebugCallStack::ConfirmSaveDialogProc, NULL);
if (res == IDB_CONFIRM_SAVE)
{
if (SaveCurrentLevel())
{
MessageBox(NULL, "Level has been successfully saved!\r\nPress Ok to terminate Editor.", "Save", MB_OK);
}
else
{
MessageBox(NULL, "Error saving level.\r\nPress Ok to terminate Editor.", "Save", MB_OK | MB_ICONWARNING);
}
}
}
if (g_cvars.sys_no_crash_dialog != 0 || !g_bUserDialog)
{
// terminate immediately - since we're in a crash, there is no point unwinding stack, we've already done access violation or worse.
// calling exit will only cause further death down the line...
TerminateProcess(GetCurrentProcess(), pex->ExceptionRecord->ExceptionCode);
}
}
INT_PTR CALLBACK DebugCallStack::ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
static EXCEPTION_POINTERS* pex;
static char errorString[32768] = "";
switch (message)
{
case WM_INITDIALOG:
{
pex = (EXCEPTION_POINTERS*)lParam;
HWND h;
if (pex->ExceptionRecord->ExceptionFlags & EXCEPTION_NONCONTINUABLE)
{
// Disable continue button for non continuable exceptions.
//h = GetDlgItem( hwndDlg,IDB_CONTINUE );
//if (h) EnableWindow( h,FALSE );
}
DebugCallStack* pDCS = static_cast<DebugCallStack*>(DebugCallStack::instance());
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_DESC);
if (h)
{
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excDesc);
}
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_CODE);
if (h)
{
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excCode);
}
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_MODULE);
if (h)
{
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excModule);
}
h = GetDlgItem(hwndDlg, IDC_EXCEPTION_ADDRESS);
if (h)
{
SendMessage(h, EM_REPLACESEL, FALSE, (LONG_PTR)pDCS->m_excAddr);
}
// Fill call stack.
HWND callStack = GetDlgItem(hwndDlg, IDC_CALLSTACK);
if (callStack)
{
SendMessage(callStack, WM_SETTEXT, FALSE, (LPARAM)pDCS->m_excCallstack);
}
if (hwndException)
{
DestroyWindow(hwndException);
hwndException = 0;
}
if (IsFloatingPointException(pex))
{
EnableWindow(GetDlgItem(hwndDlg, IDB_IGNORE), TRUE);
}
}
break;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDB_EXIT:
case IDB_IGNORE:
// Fall through.
EndDialog(hwndDlg, wParam);
return TRUE;
}
}
return FALSE;
}
INT_PTR CALLBACK DebugCallStack::ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, [[maybe_unused]] LPARAM lParam)
{
switch (message)
{
case WM_INITDIALOG:
{
// The user might be holding down the spacebar while the engine crashes.
// If we don't remove keyboard focus from this dialog, the keypress will
// press the default button before the dialog actually appears, even if
// the user has already released the key, which is bad.
SetFocus(NULL);
} break;
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case IDB_CONFIRM_SAVE: // Fall through
case IDB_DONT_SAVE:
{
EndDialog(hwndDlg, wParam);
return TRUE;
}
}
} break;
}
return FALSE;
}
bool DebugCallStack::BackupCurrentLevel()
{
CSystem* pSystem = static_cast<CSystem*>(m_pSystem);
if (pSystem && pSystem->GetUserCallback())
{
return pSystem->GetUserCallback()->OnBackupDocument();
}
return false;
}
bool DebugCallStack::SaveCurrentLevel()
{
CSystem* pSystem = static_cast<CSystem*>(m_pSystem);
if (pSystem && pSystem->GetUserCallback())
{
return pSystem->GetUserCallback()->OnSaveDocument();
}
return false;
}
int DebugCallStack::SubmitBug(EXCEPTION_POINTERS* exception_pointer)
{
int ret = IDB_EXIT;
assert(!hwndException);
RemoveOldFiles();
AZ::Debug::Trace::PrintCallstack("", 2);
LogExceptionInfo(exception_pointer);
if (IsFloatingPointException(exception_pointer))
{
//! Print exception dialog.
ret = PrintException(exception_pointer);
}
return ret;
}
void DebugCallStack::ResetFPU(EXCEPTION_POINTERS* pex)
{
if (IsFloatingPointException(pex))
{
// How to reset FPU: http://www.experts-exchange.com/Programming/System/Windows__Programming/Q_10310953.html
_clearfp();
#ifndef WIN64
pex->ContextRecord->FloatSave.ControlWord |= 0x2F;
pex->ContextRecord->FloatSave.StatusWord &= ~0x8080;
#endif
}
}
string DebugCallStack::GetModuleNameForAddr(void* addr)
{
if (m_modules.empty())
{
return "[unknown]";
}
if (addr < m_modules.begin()->first)
{
return "[unknown]";
}
TModules::const_iterator it = m_modules.begin();
TModules::const_iterator end = m_modules.end();
for (; ++it != end; )
{
if (addr < it->first)
{
return (--it)->second;
}
}
//if address is higher than the last module, we simply assume it is in the last module.
return m_modules.rbegin()->second;
}
void DebugCallStack::GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line)
{
AZ::Debug::SymbolStorage::StackLine func, file, module;
AZ::Debug::SymbolStorage::FindFunctionFromIP(addr, &func, &file, &module, line, baseAddr);
procName = func;
filename = file;
}
string DebugCallStack::GetCurrentFilename()
{
char fullpath[MAX_PATH_LENGTH + 1];
GetModuleFileName(NULL, fullpath, MAX_PATH_LENGTH);
return fullpath;
}
static bool IsFloatingPointException(EXCEPTION_POINTERS* pex)
{
if (!pex)
{
return false;
}
DWORD exceptionCode = pex->ExceptionRecord->ExceptionCode;
switch (exceptionCode)
{
case EXCEPTION_FLT_DENORMAL_OPERAND:
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
case EXCEPTION_FLT_INEXACT_RESULT:
case EXCEPTION_FLT_INVALID_OPERATION:
case EXCEPTION_FLT_OVERFLOW:
case EXCEPTION_FLT_UNDERFLOW:
case STATUS_FLOAT_MULTIPLE_FAULTS:
case STATUS_FLOAT_MULTIPLE_TRAPS:
return true;
default:
return false;
}
}
int DebugCallStack::PrintException(EXCEPTION_POINTERS* exception_pointer)
{
return (int)DialogBoxParam(gDLLHandle, MAKEINTRESOURCE(IDD_CRITICAL_ERROR), NULL, DebugCallStack::ExceptionDialogProc, (LPARAM)exception_pointer);
}
#else
void MarkThisThreadForDebugging(const char*) {}
void UnmarkThisThreadFromDebugging() {}
void UpdateFPExceptionsMaskForThreads() {}
#endif //WIN32
+95
View File
@@ -0,0 +1,95 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
#ifndef CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H
#define CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H
#pragma once
#include "IDebugCallStack.h"
#if defined (WIN32) || defined (WIN64)
//! Limits the maximal number of functions in call stack.
const int MAX_DEBUG_STACK_ENTRIES_FILE_DUMP = 12;
struct ISystem;
//!============================================================================
//!
//! DebugCallStack class, capture call stack information from symbol files.
//!
//!============================================================================
class DebugCallStack
: public IDebugCallStack
{
public:
DebugCallStack();
virtual ~DebugCallStack();
ISystem* GetSystem() { return m_pSystem; };
virtual string GetModuleNameForAddr(void* addr);
virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line);
virtual string GetCurrentFilename();
void installErrorHandler(ISystem* pSystem);
virtual int handleException(EXCEPTION_POINTERS* exception_pointer);
virtual void ReportBug(const char*);
void dumpCallStack(std::vector<string>& functions);
void SetUserDialogEnable(const bool bUserDialogEnable);
typedef std::map<void*, string> TModules;
protected:
static void RemoveOldFiles();
static void RemoveFile(const char* szFileName);
static int PrintException(EXCEPTION_POINTERS* exception_pointer);
static INT_PTR CALLBACK ExceptionDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam);
static INT_PTR CALLBACK ConfirmSaveDialogProc(HWND hwndDlg, UINT message, WPARAM wParam, LPARAM lParam);
void LogExceptionInfo(EXCEPTION_POINTERS* exception_pointer);
bool BackupCurrentLevel();
bool SaveCurrentLevel();
int SubmitBug(EXCEPTION_POINTERS* exception_pointer);
void ResetFPU(EXCEPTION_POINTERS* pex);
static const int s_iCallStackSize = 32768;
char m_excLine[256];
char m_excModule[128];
char m_excDesc[MAX_WARNING_LENGTH];
char m_excCode[MAX_WARNING_LENGTH];
char m_excAddr[80];
char m_excCallstack[s_iCallStackSize];
void* prevExceptionHandler;
bool m_bCrash;
const char* m_szBugMessage;
ISystem* m_pSystem;
int m_nSkipNumFunctions;
CONTEXT m_context;
TModules m_modules;
};
#endif //WIN32
#endif // CRYINCLUDE_CRYSYSTEM_DEBUGCALLSTACK_H
+11
View File
@@ -14,6 +14,7 @@
#include "CrySystem_precompiled.h"
#include "System.h"
#include <AZCrySystemInitLogSink.h>
#include "DebugCallStack.h"
#if defined(AZ_RESTRICTED_PLATFORM)
#undef AZ_RESTRICTED_SECTION
@@ -87,6 +88,16 @@ CRYSYSTEM_API ISystem* CreateSystemInterface(const SSystemInitParams& startupPar
startupParams.pUserCallback->OnSystemConnect(pSystem);
}
#if defined(WIN32)
// Environment Variable to signal we don't want to override our exception handler - our crash report system will set this
auto envVar = AZ::Environment::FindVariable<bool>("ExceptionHandlerIsSet");
const bool handlerIsSet = (envVar && *envVar);
if (!handlerIsSet)
{
((DebugCallStack*)IDebugCallStack::instance())->installErrorHandler(pSystem);
}
#endif
bool retVal = false;
{
AZ::Debug::StartupLogSinkReporter<AZ::Debug::CrySystemInitLogSink> initLogSink;
@@ -0,0 +1,275 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
// Description : A multiplatform base class for handling errors and collecting call stacks
#include "CrySystem_precompiled.h"
#include "IDebugCallStack.h"
#include "System.h"
#include <AzFramework/IO/FileOperations.h>
#include <AzCore/NativeUI/NativeUIRequests.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <AzCore/Utils/Utils.h>
//#if !defined(LINUX)
#include <ISystem.h>
const char* const IDebugCallStack::s_szFatalErrorCode = "FATAL_ERROR";
IDebugCallStack::IDebugCallStack()
: m_bIsFatalError(false)
, m_postBackupProcess(0)
, m_memAllocFileHandle(AZ::IO::InvalidHandle)
{
}
IDebugCallStack::~IDebugCallStack()
{
StopMemLog();
}
#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_SINGLETON
IDebugCallStack* IDebugCallStack::instance()
{
static IDebugCallStack sInstance;
return &sInstance;
}
#endif
void IDebugCallStack::FileCreationCallback(void (* postBackupProcess)())
{
m_postBackupProcess = postBackupProcess;
}
//////////////////////////////////////////////////////////////////////////
void IDebugCallStack::LogCallstack()
{
AZ::Debug::Trace::PrintCallstack("", 2);
}
const char* IDebugCallStack::TranslateExceptionCode(DWORD dwExcept)
{
switch (dwExcept)
{
#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_TRANSLATE
case EXCEPTION_ACCESS_VIOLATION:
return "EXCEPTION_ACCESS_VIOLATION";
break;
case EXCEPTION_DATATYPE_MISALIGNMENT:
return "EXCEPTION_DATATYPE_MISALIGNMENT";
break;
case EXCEPTION_BREAKPOINT:
return "EXCEPTION_BREAKPOINT";
break;
case EXCEPTION_SINGLE_STEP:
return "EXCEPTION_SINGLE_STEP";
break;
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
return "EXCEPTION_ARRAY_BOUNDS_EXCEEDED";
break;
case EXCEPTION_FLT_DENORMAL_OPERAND:
return "EXCEPTION_FLT_DENORMAL_OPERAND";
break;
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
break;
case EXCEPTION_FLT_INEXACT_RESULT:
return "EXCEPTION_FLT_INEXACT_RESULT";
break;
case EXCEPTION_FLT_INVALID_OPERATION:
return "EXCEPTION_FLT_INVALID_OPERATION";
break;
case EXCEPTION_FLT_OVERFLOW:
return "EXCEPTION_FLT_OVERFLOW";
break;
case EXCEPTION_FLT_STACK_CHECK:
return "EXCEPTION_FLT_STACK_CHECK";
break;
case EXCEPTION_FLT_UNDERFLOW:
return "EXCEPTION_FLT_UNDERFLOW";
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
break;
case EXCEPTION_INT_OVERFLOW:
return "EXCEPTION_INT_OVERFLOW";
break;
case EXCEPTION_PRIV_INSTRUCTION:
return "EXCEPTION_PRIV_INSTRUCTION";
break;
case EXCEPTION_IN_PAGE_ERROR:
return "EXCEPTION_IN_PAGE_ERROR";
break;
case EXCEPTION_ILLEGAL_INSTRUCTION:
return "EXCEPTION_ILLEGAL_INSTRUCTION";
break;
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
return "EXCEPTION_NONCONTINUABLE_EXCEPTION";
break;
case EXCEPTION_STACK_OVERFLOW:
return "EXCEPTION_STACK_OVERFLOW";
break;
case EXCEPTION_INVALID_DISPOSITION:
return "EXCEPTION_INVALID_DISPOSITION";
break;
case EXCEPTION_GUARD_PAGE:
return "EXCEPTION_GUARD_PAGE";
break;
case EXCEPTION_INVALID_HANDLE:
return "EXCEPTION_INVALID_HANDLE";
break;
//case EXCEPTION_POSSIBLE_DEADLOCK: return "EXCEPTION_POSSIBLE_DEADLOCK"; break ;
case STATUS_FLOAT_MULTIPLE_FAULTS:
return "STATUS_FLOAT_MULTIPLE_FAULTS";
break;
case STATUS_FLOAT_MULTIPLE_TRAPS:
return "STATUS_FLOAT_MULTIPLE_TRAPS";
break;
#endif
default:
return "Unknown";
break;
}
}
void IDebugCallStack::PutVersion(char* str, size_t length)
{
AZ_PUSH_DISABLE_WARNING(4996, "-Wunknown-warning-option")
if (!gEnv || !gEnv->pSystem)
{
return;
}
char sFileVersion[128];
gEnv->pSystem->GetFileVersion().ToString(sFileVersion, sizeof(sFileVersion));
char sProductVersion[128];
gEnv->pSystem->GetProductVersion().ToString(sProductVersion, sizeof(sFileVersion));
//! Get time.
time_t ltime;
time(&ltime);
tm* today = localtime(&ltime);
char s[1024];
//! Use strftime to build a customized time string.
strftime(s, 128, "Logged at %#c\n", today);
azstrcat(str, length, s);
sprintf_s(s, "FileVersion: %s\n", sFileVersion);
azstrcat(str, length, s);
sprintf_s(s, "ProductVersion: %s\n", sProductVersion);
azstrcat(str, length, s);
if (gEnv->pLog)
{
const char* logfile = gEnv->pLog->GetFileName();
if (logfile)
{
sprintf (s, "LogFile: %s\n", logfile);
azstrcat(str, length, s);
}
}
AZ::IO::FixedMaxPathString projectPath = AZ::Utils::GetProjectPath();
azstrcat(str, length, "ProjectDir: ");
azstrcat(str, length, projectPath.c_str());
azstrcat(str, length, "\n");
#if AZ_LEGACY_CRYSYSTEM_TRAIT_DEBUGCALLSTACK_APPEND_MODULENAME
GetModuleFileNameA(NULL, s, sizeof(s));
// Log EXE filename only if possible (not full EXE path which could contain sensitive info)
AZStd::string exeName;
if (AZ::StringFunc::Path::GetFullFileName(s, exeName))
{
azstrcat(str, length, "Executable: ");
azstrcat(str, length, exeName.c_str());
# ifdef AZ_DEBUG_BUILD
azstrcat(str, length, " (debug: yes");
# else
azstrcat(str, length, " (debug: no");
# endif
}
#endif
AZ_POP_DISABLE_WARNING
}
//Crash the application, in this way the debug callstack routine will be called and it will create all the necessary files (error.log, dump, and eventually screenshot)
void IDebugCallStack::FatalError(const char* description)
{
m_bIsFatalError = true;
WriteLineToLog(description);
#ifndef _RELEASE
bool bShowDebugScreen = g_cvars.sys_no_crash_dialog == 0;
// showing the debug screen is not safe when not called from mainthread
// it normally leads to a infinity recursion followed by a stack overflow, preventing
// useful call stacks, thus they are disabled
bShowDebugScreen = bShowDebugScreen && gEnv->mMainThreadId == CryGetCurrentThreadId();
if (bShowDebugScreen)
{
EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "Open 3D Engine Fatal Error", description, false);
}
#endif
#if defined(WIN32) || !defined(_RELEASE)
int* p = 0x0;
PREFAST_SUPPRESS_WARNING(6011) * p = 1; // we're intentionally crashing here
#endif
}
void IDebugCallStack::WriteLineToLog(const char* format, ...)
{
va_list ArgList;
char szBuffer[MAX_WARNING_LENGTH];
va_start(ArgList, format);
vsnprintf_s(szBuffer, sizeof(szBuffer), sizeof(szBuffer) - 1, format, ArgList);
cry_strcat(szBuffer, "\n");
szBuffer[sizeof(szBuffer) - 1] = '\0';
va_end(ArgList);
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\error.log", AZ::IO::GetOpenModeFromStringMode("a+t"), fileHandle);
if (fileHandle != AZ::IO::InvalidHandle)
{
AZ::IO::FileIOBase::GetDirectInstance()->Write(fileHandle, szBuffer, strlen(szBuffer));
AZ::IO::FileIOBase::GetDirectInstance()->Flush(fileHandle);
AZ::IO::FileIOBase::GetDirectInstance()->Close(fileHandle);
}
}
//////////////////////////////////////////////////////////////////////////
void IDebugCallStack::StartMemLog()
{
AZ::IO::FileIOBase::GetDirectInstance()->Open("@Log@\\memallocfile.log", AZ::IO::OpenMode::ModeWrite, m_memAllocFileHandle);
assert(m_memAllocFileHandle != AZ::IO::InvalidHandle);
}
//////////////////////////////////////////////////////////////////////////
void IDebugCallStack::StopMemLog()
{
if (m_memAllocFileHandle != AZ::IO::InvalidHandle)
{
AZ::IO::FileIOBase::GetDirectInstance()->Close(m_memAllocFileHandle);
m_memAllocFileHandle = AZ::IO::InvalidHandle;
}
}
//#endif //!defined(LINUX)
@@ -0,0 +1,90 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
// Description : A multiplatform base class for handling errors and collecting call stacks
#ifndef CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H
#define CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H
#pragma once
#include "System.h"
#if AZ_LEGACY_CRYSYSTEM_TRAIT_FORWARD_EXCEPTION_POINTERS
struct EXCEPTION_POINTERS;
#endif
//! Limits the maximal number of functions in call stack.
enum
{
MAX_DEBUG_STACK_ENTRIES = 80
};
class IDebugCallStack
{
public:
// Returns single instance of DebugStack
static IDebugCallStack* instance();
virtual int handleException([[maybe_unused]] EXCEPTION_POINTERS* exception_pointer){return 0; }
// returns the module name of a given address
virtual string GetModuleNameForAddr([[maybe_unused]] void* addr) { return "[unknown]"; }
// returns the function name of a given address together with source file and line number (if available) of a given address
virtual void GetProcNameForAddr(void* addr, string& procName, void*& baseAddr, string& filename, int& line)
{
filename = "[unknown]";
line = 0;
baseAddr = addr;
#if defined(PLATFORM_64BIT)
procName.Format("[%016llX]", addr);
#else
procName.Format("[%08X]", addr);
#endif
}
// returns current filename
virtual string GetCurrentFilename() { return "[unknown]"; }
//! Dumps Current Call Stack to log.
virtual void LogCallstack();
//triggers a fatal error, so the DebugCallstack can create the error.log and terminate the application
void FatalError(const char*);
//Reports a bug and continues execution
virtual void ReportBug(const char*) {}
virtual void FileCreationCallback(void (* postBackupProcess)());
static void WriteLineToLog(const char* format, ...);
virtual void StartMemLog();
virtual void StopMemLog();
protected:
IDebugCallStack();
virtual ~IDebugCallStack();
static const char* TranslateExceptionCode(DWORD dwExcept);
static void PutVersion(char* str, size_t length);
bool m_bIsFatalError;
static const char* const s_szFatalErrorCode;
void (* m_postBackupProcess)();
AZ::IO::HandleType m_memAllocFileHandle;
};
#endif // CRYINCLUDE_CRYSYSTEM_IDEBUGCALLSTACK_H
+1
View File
@@ -208,6 +208,7 @@ struct SSystemCVars
int sys_no_crash_dialog;
int sys_no_error_report_window;
int sys_dump_aux_threads;
int sys_WER;
int sys_dump_type;
int sys_ai;
int sys_entitysystem;
+19
View File
@@ -121,6 +121,10 @@
# include <AzFramework/Network/AssetProcessorConnection.h>
#endif
#ifdef WIN32
extern LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers);
#endif
#if defined(AZ_RESTRICTED_PLATFORM)
#define AZ_RESTRICTED_SECTION SYSTEMINIT_CPP_SECTION_14
#include AZ_RESTRICTED_FILE(SystemInit_cpp)
@@ -1484,6 +1488,13 @@ AZ_POP_DISABLE_WARNING
InlineInitializationProcessing("CSystem::Init LoadConfigurations");
#ifdef WIN32
if (g_cvars.sys_WER)
{
SetUnhandledExceptionFilter(CryEngineExceptionFilterWER);
}
#endif
//////////////////////////////////////////////////////////////////////////
// Localization
//////////////////////////////////////////////////////////////////////////
@@ -2020,6 +2031,14 @@ void CSystem::CreateSystemVars()
REGISTER_CVAR2("sys_update_profile_time", &g_cvars.sys_update_profile_time, 1.0f, 0, "Time to keep updates timings history for.");
REGISTER_CVAR2("sys_no_crash_dialog", &g_cvars.sys_no_crash_dialog, m_bNoCrashDialog, VF_NULL, "Whether to disable the crash dialog window");
REGISTER_CVAR2("sys_no_error_report_window", &g_cvars.sys_no_error_report_window, m_bNoErrorReportWindow, VF_NULL, "Whether to disable the error report list");
#if defined(_RELEASE)
if (!gEnv->IsDedicated())
{
REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 1, 0, "Enables Windows Error Reporting");
}
#else
REGISTER_CVAR2("sys_WER", &g_cvars.sys_WER, 0, 0, "Enables Windows Error Reporting");
#endif
#ifdef USE_HTTP_WEBSOCKETS
REGISTER_CVAR2("sys_simple_http_base_port", &g_cvars.sys_simple_http_base_port, 1880, VF_REQUIRE_APP_RESTART,
+5
View File
@@ -46,6 +46,8 @@
#include <shlobj.h>
#endif
#include "IDebugCallStack.h"
#if defined(APPLE) || defined(LINUX)
#include <pwd.h>
#endif
@@ -355,6 +357,7 @@ void CSystem::FatalError(const char* format, ...)
}
// Dump callstack.
IDebugCallStack::instance()->FatalError(szBuffer);
#endif
CryDebugBreak();
@@ -396,6 +399,8 @@ void CSystem::ReportBug([[maybe_unused]] const char* format, ...)
va_start(ArgList, format);
azvsnprintf(szBuffer + strlen(sPrefix), MAX_WARNING_LENGTH - strlen(sPrefix), format, ArgList);
va_end(ArgList);
IDebugCallStack::instance()->ReportBug(szBuffer);
#endif
}
@@ -0,0 +1,137 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.
// Description : Support for Windows Error Reporting (WER)
#include "CrySystem_precompiled.h"
#ifdef WIN32
#include "System.h"
#include <windows.h>
#include <tchar.h>
#include "errorrep.h"
#include "ISystem.h"
#include <DbgHelp.h>
static WCHAR szPath[MAX_PATH + 1];
static WCHAR szFR[] = L"\\System32\\FaultRep.dll";
WCHAR* GetFullPathToFaultrepDll(void)
{
UINT rc = GetSystemWindowsDirectoryW(szPath, ARRAYSIZE(szPath));
if (rc == 0 || rc > ARRAYSIZE(szPath) - ARRAYSIZE(szFR) - 1)
{
return NULL;
}
wcscat_s(szPath, szFR);
return szPath;
}
typedef BOOL (WINAPI * MINIDUMPWRITEDUMP)(HANDLE hProcess, DWORD dwPid, HANDLE hFile, MINIDUMP_TYPE DumpType,
CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
//////////////////////////////////////////////////////////////////////////
LONG WINAPI CryEngineExceptionFilterMiniDump(struct _EXCEPTION_POINTERS* pExceptionPointers, const char* szDumpPath, MINIDUMP_TYPE DumpType)
{
// note: In debug mode, this dll is loaded on startup anyway, so this should not incur an additional load unless it crashes
// very early during startup.
fflush(nullptr); // according to MSDN on fflush, calling fflush on null flushes all buffers.
HMODULE hndDBGHelpDLL = LoadLibraryA("DBGHELP.DLL");
if (!hndDBGHelpDLL)
{
CryLogAlways("Failed to record DMP file: Could not open DBGHELP.DLL");
return EXCEPTION_CONTINUE_SEARCH;
}
MINIDUMPWRITEDUMP dumpFnPtr = (MINIDUMPWRITEDUMP)::GetProcAddress(hndDBGHelpDLL, "MiniDumpWriteDump");
if (!dumpFnPtr)
{
CryLogAlways("Failed to record DMP file: Unable to find MiniDumpWriteDump in DBGHELP.DLL");
return EXCEPTION_CONTINUE_SEARCH;
}
HANDLE hFile = ::CreateFile(szDumpPath, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
CryLogAlways("Failed to record DMP file: could not open file '%s' for writing - error code: %d", szDumpPath, GetLastError());
return EXCEPTION_CONTINUE_SEARCH;
}
_MINIDUMP_EXCEPTION_INFORMATION ExInfo;
ExInfo.ThreadId = ::GetCurrentThreadId();
ExInfo.ExceptionPointers = pExceptionPointers;
ExInfo.ClientPointers = NULL;
BOOL bOK = dumpFnPtr(GetCurrentProcess(), GetCurrentProcessId(), hFile, DumpType, &ExInfo, NULL, NULL);
::CloseHandle(hFile);
if (bOK)
{
CryLogAlways("Successfully recorded DMP file: '%s'", szDumpPath);
return EXCEPTION_EXECUTE_HANDLER; // SUCCESS! you can execute your handlers now
}
else
{
CryLogAlways("Failed to record DMP file: '%s' - error code: %d", szDumpPath, GetLastError());
}
return EXCEPTION_CONTINUE_SEARCH;
}
//////////////////////////////////////////////////////////////////////////
LONG WINAPI CryEngineExceptionFilterWER(struct _EXCEPTION_POINTERS* pExceptionPointers)
{
if (g_cvars.sys_WER > 1)
{
char szScratch [_MAX_PATH];
const char* szDumpPath = gEnv->pCryPak->AdjustFileName("@log@/CE2Dump.dmp", szScratch, AZ_ARRAY_SIZE(szScratch), 0);
MINIDUMP_TYPE mdumpValue = (MINIDUMP_TYPE)(MiniDumpNormal);
if (g_cvars.sys_WER > 1)
{
mdumpValue = (MINIDUMP_TYPE)(g_cvars.sys_WER - 2);
}
return CryEngineExceptionFilterMiniDump(pExceptionPointers, szDumpPath, mdumpValue);
}
LONG lRet = EXCEPTION_CONTINUE_SEARCH;
WCHAR* psz = GetFullPathToFaultrepDll();
if (psz)
{
HMODULE hFaultRepDll = LoadLibraryW(psz);
if (hFaultRepDll)
{
pfn_REPORTFAULT pfn = (pfn_REPORTFAULT)GetProcAddress(hFaultRepDll, "ReportFault");
if (pfn)
{
pfn(pExceptionPointers, 0);
lRet = EXCEPTION_EXECUTE_HANDLER;
}
FreeLibrary(hFaultRepDll);
}
}
return lRet;
}
#endif // WIN32
@@ -15,6 +15,8 @@ set(FILES
CmdLineArg.cpp
ConsoleBatchFile.cpp
ConsoleHelpGen.cpp
DebugCallStack.cpp
IDebugCallStack.cpp
Log.cpp
System.cpp
SystemCFG.cpp
@@ -31,6 +33,8 @@ set(FILES
CmdLineArg.h
ConsoleBatchFile.h
ConsoleHelpGen.h
DebugCallStack.h
IDebugCallStack.h
Log.h
SimpleStringPool.h
CrySystem_precompiled.h
@@ -72,4 +76,5 @@ set(FILES
ViewSystem/ViewSystem.cpp
ViewSystem/ViewSystem.h
CrySystem_precompiled.cpp
WindowsErrorReporting.cpp
)
@@ -307,6 +307,8 @@ namespace AZ
Asset(AssetLoadBehavior loadBehavior = AssetLoadBehavior::Default);
/// Create an asset from a valid asset data (created asset), might not be loaded or currently loading.
Asset(AssetData* assetData, AssetLoadBehavior loadBehavior);
/// Create an asset from a valid asset data (created asset) and set the asset id for both, might not be loaded or currently loading.
Asset(const AZ::Data::AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior);
/// Initialize asset pointer with id, type, and hint. No data construction will occur until QueueLoad is called.
Asset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint = AZStd::string());
@@ -787,6 +789,18 @@ namespace AZ
SetData(assetData);
}
//=========================================================================
template<class T>
Asset<T>::Asset(const AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior)
: m_assetId(id)
, m_assetType(azrtti_typeid<T>())
, m_loadBehavior(loadBehavior)
{
AZ_Assert(!assetData->m_assetId.IsValid(), "Asset data already has an ID set.");
assetData->m_assetId = id;
SetData(assetData);
}
//=========================================================================
template<class T>
Asset<T>::Asset(const AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint)
@@ -41,10 +41,6 @@ namespace AZ
if (const char* homePath = std::getenv("HOME"); homePath != nullptr)
{
AZ::IO::FixedMaxPath path{homePath};
if (!path.empty())
{
path /= ".o3de";
}
return path.Native();
}
return {};
@@ -21,7 +21,7 @@ namespace AzPhysics
namespace
{
const float TimestepMin = 0.001f; //1000fps
const float TimestepMax = 0.05f; //20fps
const float TimestepMax = 0.1f; //10fps
}
AZ_CLASS_ALLOCATOR_IMPL(SystemConfiguration, AZ::SystemAllocator, 0);
@@ -34,7 +34,7 @@ namespace AzPhysics
static constexpr float DefaultFixedTimestep = 0.0166667f; //! Value represents 1/60th or 60 FPS.
float m_maxTimestep = 1.f / 20.f; //!< Maximum fixed timestep in seconds to run the physics update.
float m_maxTimestep = 0.1f; //!< Maximum fixed timestep in seconds to run the physics update (10FPS).
float m_fixedTimestep = DefaultFixedTimestep; //!< Timestep in seconds to run the physics update. See DefaultFixedTimestep.
AZ::u64 m_raycastBufferSize = 32; //!< Maximum number of hits that will be returned from a raycast.
@@ -0,0 +1,78 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/std/string/string.h>
namespace AzFramework
{
//! SessionConnectionConfig
//! The properties for handling join session request.
struct SessionConnectionConfig
{
// A unique identifier for registered player in session.
AZStd::string m_playerSessionId;
// The DNS identifier assigned to the instance that is running the session.
AZStd::string m_dnsName;
// The IP address of the session.
AZStd::string m_ipAddress;
// The port number for the session.
uint16_t m_port;
};
//! SessionConnectionConfig
//! The properties for handling player connect/disconnect
struct PlayerConnectionConfig
{
// A unique identifier for player connection.
uint32_t m_playerConnectionId;
// A unique identifier for registered player in session.
AZStd::string m_playerSessionId;
};
//! ISessionHandlingClientRequests
//! The session handling events to invoke multiplayer component handle the work on client side
class ISessionHandlingClientRequests
{
public:
// Handle the player join session process
// @param sessionConnectionConfig The required properties to handle the player join session process
// @return The result of player join session process
virtual bool HandlePlayerJoinSession(const SessionConnectionConfig& sessionConnectionConfig) = 0;
// Handle the player leave session process
virtual void HandlePlayerLeaveSession() = 0;
};
//! ISessionHandlingServerRequests
//! The session handling events to invoke server provider handle the work on server side
class ISessionHandlingServerRequests
{
public:
// Handle the destroy session process
virtual void HandleDestroySession() = 0;
// Validate the player join session process
// @param playerConnectionConfig The required properties to validate the player join session process
// @return The result of player join session validation
virtual bool ValidatePlayerJoinSession(const PlayerConnectionConfig& playerConnectionConfig) = 0;
// Handle the player leave session process
// @param playerConnectionConfig The required properties to handle the player leave session process
virtual void HandlePlayerLeaveSession(const PlayerConnectionConfig& playerConnectionConfig) = 0;
};
} // namespace AzFramework
@@ -0,0 +1,130 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Session/ISessionRequests.h>
#include <AzFramework/Session/SessionConfig.h>
namespace AzFramework
{
void CreateSessionRequest::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<CreateSessionRequest>()
->Version(0)
->Field("creatorId", &CreateSessionRequest::m_creatorId)
->Field("sessionProperties", &CreateSessionRequest::m_sessionProperties)
->Field("sessionName", &CreateSessionRequest::m_sessionName)
->Field("maxPlayer", &CreateSessionRequest::m_maxPlayer)
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<CreateSessionRequest>("CreateSessionRequest", "The container for CreateSession request parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_creatorId,
"CreatorId", "A unique identifier for a player or entity creating the session")
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_sessionProperties,
"SessionProperties", "A collection of custom properties for a session")
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_sessionName,
"SessionName", "A descriptive label that is associated with a session")
->DataElement(AZ::Edit::UIHandlers::Default, &CreateSessionRequest::m_maxPlayer,
"MaxPlayer", "The maximum number of players that can be connected simultaneously to the session")
;
}
}
}
void SearchSessionsRequest::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SearchSessionsRequest>()
->Version(0)
->Field("filterExpression", &SearchSessionsRequest::m_filterExpression)
->Field("sortExpression", &SearchSessionsRequest::m_sortExpression)
->Field("maxResult", &SearchSessionsRequest::m_maxResult)
->Field("nextToken", &SearchSessionsRequest::m_nextToken)
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<SearchSessionsRequest>("SearchSessionsRequest", "The container for SearchSessions request parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_filterExpression,
"FilterExpression", "String containing the search criteria for the session search")
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_sortExpression,
"SortExpression", "Instructions on how to sort the search results")
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_maxResult,
"MaxResult", "The maximum number of results to return")
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsRequest::m_nextToken,
"NextToken", "A token that indicates the start of the next sequential page of results")
;
}
}
}
void SearchSessionsResponse::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SearchSessionsResponse>()
->Version(0)
->Field("sessionConfigs", &SearchSessionsResponse::m_sessionConfigs)
->Field("nextToken", &SearchSessionsResponse::m_nextToken)
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<SearchSessionsResponse>("SearchSessionsResponse", "The container for SearchSession request results")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsResponse::m_sessionConfigs,
"SessionConfigs", "A collection of sessions that match the search criteria and sorted in specific order")
->DataElement(AZ::Edit::UIHandlers::Default, &SearchSessionsResponse::m_nextToken,
"NextToken", "A token that indicates the start of the next sequential page of results")
;
}
}
}
void JoinSessionRequest::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<JoinSessionRequest>()
->Version(0)
->Field("sessionId", &JoinSessionRequest::m_sessionId)
->Field("playerId", &JoinSessionRequest::m_playerId)
->Field("playerData", &JoinSessionRequest::m_playerData)
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<JoinSessionRequest>("JoinSessionRequest", "The container for JoinSession request parameters")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_sessionId,
"SessionId", "A unique identifier for the session")
->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_playerId,
"PlayerId", "A unique identifier for a player. Player IDs are developer-defined")
->DataElement(AZ::Edit::UIHandlers::Default, &JoinSessionRequest::m_playerData,
"PlayerData", "Developer-defined information related to a player")
;
}
}
}
} // namespace AzFramework
@@ -0,0 +1,192 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
#include <AzCore/Outcome/Outcome.h>
namespace AzFramework
{
struct SessionConfig;
//! CreateSessionRequest
//! The container for CreateSession request parameters.
struct CreateSessionRequest
{
AZ_RTTI(CreateSessionRequest, "{E39C2A45-89C9-4CFB-B337-9734DC798930}");
static void Reflect(AZ::ReflectContext* context);
CreateSessionRequest() = default;
virtual ~CreateSessionRequest() = default;
// A unique identifier for a player or entity creating the session.
AZStd::string m_creatorId;
// A collection of custom properties for a session.
AZStd::unordered_map<AZStd::string, AZStd::string> m_sessionProperties;
// A descriptive label that is associated with a session.
AZStd::string m_sessionName;
// The maximum number of players that can be connected simultaneously to the session.
uint64_t m_maxPlayer;
};
//! SearchSessionsRequest
//! The container for SearchSessions request parameters.
struct SearchSessionsRequest
{
AZ_RTTI(SearchSessionsRequest, "{B49207A8-8549-4ADB-B7D9-D7A4932F9B4B}");
static void Reflect(AZ::ReflectContext* context);
SearchSessionsRequest() = default;
virtual ~SearchSessionsRequest() = default;
// String containing the search criteria for the session search. If no filter expression is included, the request returns results
// for all active sessions.
AZStd::string m_filterExpression;
// Instructions on how to sort the search results. If no sort expression is included, the request returns results in random order.
AZStd::string m_sortExpression;
// The maximum number of results to return.
uint8_t m_maxResult;
// A token that indicates the start of the next sequential page of results.
AZStd::string m_nextToken;
};
//! SearchSessionsResponse
//! The container for SearchSession request results.
struct SearchSessionsResponse
{
AZ_RTTI(SearchSessionsResponse, "{F93DE7DC-D381-4E08-8A3B-0B08F7C38714}");
static void Reflect(AZ::ReflectContext* context);
SearchSessionsResponse() = default;
virtual ~SearchSessionsResponse() = default;
// A collection of sessions that match the search criteria and sorted in specific order.
AZStd::vector<SessionConfig> m_sessionConfigs;
// A token that indicates the start of the next sequential page of results.
AZStd::string m_nextToken;
};
//! JoinSessionRequest
//! The container for JoinSession request parameters.
struct JoinSessionRequest
{
AZ_RTTI(JoinSessionRequest, "{519769E8-3CDE-4385-A0D7-24DBB3685657}");
static void Reflect(AZ::ReflectContext* context);
JoinSessionRequest() = default;
virtual ~JoinSessionRequest() = default;
// A unique identifier for the session.
AZStd::string m_sessionId;
// A unique identifier for a player. Player IDs are developer-defined.
AZStd::string m_playerId;
// Developer-defined information related to a player.
AZStd::string m_playerData;
};
//! ISessionRequests
//! Pure virtual session interface class to abstract the details of session handling from application code.
class ISessionRequests
{
public:
AZ_RTTI(ISessionRequests, "{D6C41A71-DD8D-47FE-8515-FAF90670AE2F}");
ISessionRequests() = default;
virtual ~ISessionRequests() = default;
// Create a session for players to find and join.
// @param createSessionRequest The request of CreateSession operation
// @return The request id if session creation request succeeds; empty if it fails
virtual AZStd::string CreateSession(const CreateSessionRequest& createSessionRequest) = 0;
// Retrieve all active sessions that match the given search criteria and sorted in specific order.
// @param searchSessionsRequest The request of SearchSessions operation
// @return The response of SearchSessions operation
virtual SearchSessionsResponse SearchSessions(const SearchSessionsRequest& searchSessionsRequest) const = 0;
// Reserve an open player slot in a session, and perform connection from client to server.
// @param joinSessionRequest The request of JoinSession operation
// @return True if joining session succeeds; False otherwise
virtual bool JoinSession(const JoinSessionRequest& joinSessionRequest) = 0;
// Disconnect player from session.
virtual void LeaveSession() = 0;
};
//! ISessionAsyncRequests
//! Async version of ISessionRequests
class ISessionAsyncRequests
{
public:
AZ_RTTI(ISessionAsyncRequests, "{471542AF-96B9-4930-82FE-242A4E68432D}");
ISessionAsyncRequests() = default;
virtual ~ISessionAsyncRequests() = default;
// CreateSession Async
// @param createSessionRequest The request of CreateSession operation
virtual void CreateSessionAsync(const CreateSessionRequest& createSessionRequest) = 0;
// SearchSessions Async
// @param searchSessionsRequest The request of SearchSessions operation
virtual void SearchSessionsAsync(const SearchSessionsRequest& searchSessionsRequest) const = 0;
// JoinSession Async
// @param joinSessionRequest The request of JoinSession operation
virtual void JoinSessionAsync(const JoinSessionRequest& joinSessionRequest) = 0;
// LeaveSession Async
virtual void LeaveSessionAsync() = 0;
};
//! SessionAsyncRequestNotifications
//! The notifications correspond to session async requests
class SessionAsyncRequestNotifications
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnCreateSessionAsyncComplete is fired once CreateSessionAsync completes
// @param createSessionResponse The request id if session creation request succeeds; empty if it fails
virtual void OnCreateSessionAsyncComplete(const AZStd::string& createSessionReponse) = 0;
// OnSearchSessionsAsyncComplete is fired once SearchSessionsAsync completes
// @param searchSessionsResponse The response of SearchSessions call
virtual void OnSearchSessionsAsyncComplete(const SearchSessionsResponse& searchSessionsResponse) = 0;
// OnJoinSessionAsyncComplete is fired once JoinSessionAsync completes
// @param joinSessionsResponse True if joining session succeeds; False otherwise
virtual void OnJoinSessionAsyncComplete(bool joinSessionsResponse) = 0;
// OnLeaveSessionAsyncComplete is fired once LeaveSessionAsync completes
virtual void OnLeaveSessionAsyncComplete() = 0;
};
using SessionAsyncRequestNotificationBus = AZ::EBus<SessionAsyncRequestNotifications>;
} // namespace AzFramework
@@ -0,0 +1,74 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Session/SessionConfig.h>
namespace AzFramework
{
void SessionConfig::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<SessionConfig>()
->Version(0)
->Field("creationTime", &SessionConfig::m_creationTime)
->Field("terminationTime", &SessionConfig::m_terminationTime)
->Field("creatorId", &SessionConfig::m_creatorId)
->Field("sessionProperties", &SessionConfig::m_sessionProperties)
->Field("sessionId", &SessionConfig::m_sessionId)
->Field("sessionName", &SessionConfig::m_sessionName)
->Field("dnsName", &SessionConfig::m_dnsName)
->Field("ipAddress", &SessionConfig::m_ipAddress)
->Field("port", &SessionConfig::m_port)
->Field("maxPlayer", &SessionConfig::m_maxPlayer)
->Field("currentPlayer", &SessionConfig::m_currentPlayer)
->Field("status", &SessionConfig::m_status)
->Field("statusReason", &SessionConfig::m_statusReason)
;
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<SessionConfig>("SessionConfig", "Properties describing a session")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_creationTime,
"CreationTime", "A time stamp indicating when this session was created. Format is a number expressed in Unix time as milliseconds.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_terminationTime,
"TerminationTime", "A time stamp indicating when this data object was terminated. Same format as creation time.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_creatorId,
"CreatorId", "A unique identifier for a player or entity creating the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionProperties,
"SessionProperties", "A collection of custom properties for a session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionId,
"SessionId", "A unique identifier for the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_sessionName,
"SessionName", "A descriptive label that is associated with a session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_dnsName,
"DnsName", "The DNS identifier assigned to the instance that is running the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_ipAddress,
"IpAddress", "The IP address of the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_port,
"Port", "The port number for the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_maxPlayer,
"MaxPlayer", "The maximum number of players that can be connected simultaneously to the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_currentPlayer,
"CurrentPlayer", "Number of players currently in the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_status,
"Status", "Current status of the session.")
->DataElement(AZ::Edit::UIHandlers::Default, &AzFramework::SessionConfig::m_statusReason,
"StatusReason", "Provides additional information about session status.");
}
}
}
} // namespace AzFramework
@@ -0,0 +1,70 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/RTTI/ReflectContext.h>
#include <AzCore/std/containers/unordered_map.h>
#include <AzCore/std/string/string.h>
namespace AzFramework
{
//! SessionConfig
//! Properties describing a session.
struct SessionConfig
{
AZ_RTTI(SessionConfig, "{992DD4BE-8BA5-4071-8818-B99FD2952086}");
static void Reflect(AZ::ReflectContext* context);
SessionConfig() = default;
virtual ~SessionConfig() = default;
// A time stamp indicating when this session was created. Format is a number expressed in Unix time as milliseconds.
uint64_t m_creationTime;
// A time stamp indicating when this data object was terminated. Same format as creation time.
uint64_t m_terminationTime;
// A unique identifier for a player or entity creating the session.
AZStd::string m_creatorId;
// A collection of custom properties for a session.
AZStd::unordered_map<AZStd::string, AZStd::string> m_sessionProperties;
// A unique identifier for the session.
AZStd::string m_sessionId;
// A descriptive label that is associated with a session.
AZStd::string m_sessionName;
// The DNS identifier assigned to the instance that is running the session.
AZStd::string m_dnsName;
// The IP address of the session.
AZStd::string m_ipAddress;
// The port number for the session.
uint16_t m_port;
// The maximum number of players that can be connected simultaneously to the session.
uint64_t m_maxPlayer;
// Number of players currently in the session.
uint64_t m_currentPlayer;
// Current status of the session.
AZStd::string m_status;
// Provides additional information about session status.
AZStd::string m_statusReason;
};
} // namespace AzFramework
@@ -0,0 +1,47 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/EBus/EBus.h>
namespace AzFramework
{
struct SessionConfig;
//! SessionNotifications
//! The session notifications to listen for performing required operations
class SessionNotifications
: public AZ::EBusTraits
{
public:
//////////////////////////////////////////////////////////////////////////
// EBusTraits overrides
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
//////////////////////////////////////////////////////////////////////////
// OnSessionHealthCheck is fired in health check process
// @return The result of all OnSessionHealthCheck
virtual bool OnSessionHealthCheck() = 0;
// OnCreateSessionBegin is fired at the beginning of session creation
// @param sessionConfig The properties to describe a session
// @return The result of all OnCreateSessionBegin notifications
virtual bool OnCreateSessionBegin(const SessionConfig& sessionConfig) = 0;
// OnDestroySessionBegin is fired at the beginning of session termination
// @return The result of all OnDestroySessionBegin notifications
virtual bool OnDestroySessionBegin() = 0;
};
using SessionNotificationBus = AZ::EBus<SessionNotifications>;
} // namespace AzFramework
@@ -84,6 +84,7 @@ namespace AzFramework
};
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using EntityPreInsertionCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket&)>;
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
@@ -110,7 +111,8 @@ namespace AzFramework
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made the function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback = {}) = 0;
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) = 0;
//! Spawn instances of some entities in the spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
@@ -118,7 +120,7 @@ namespace AzFramework
//! a different thread than the one that made this function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntitySpawnCallback completionCallback = {}) = 0;
EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from
@@ -11,20 +11,24 @@
*/
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Serialization/IdUtils.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/parallel/scoped_lock.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Components/TransformComponent.h>
#include <AzFramework/Entity/GameEntityContextBus.h>
#include <AzFramework/Spawnable/Spawnable.h>
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
namespace AzFramework
{
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback)
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback,
EntitySpawnCallback completionCallback)
{
SpawnAllEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
@@ -32,13 +36,15 @@ namespace AzFramework
}
}
void SpawnableEntitiesManager::SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntitySpawnCallback completionCallback)
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback)
{
SpawnEntitiesCommand queueEntry;
queueEntry.m_ticket = &ticket;
queueEntry.m_entityIndices = AZStd::move(entityIndices);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
{
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
@@ -205,32 +211,85 @@ namespace AzFramework
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
clone->SetId(AZ::Entity::MakeId());
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, clone);
return clone;
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext)
{
return AZ::IdUtils::Remapper<AZ::EntityId>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneEntityIdMap, &serializeContext);
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext)
{
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
{
size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size();
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
const Spawnable::EntityList& entities = ticket.m_spawnable->GetEntities();
size_t entitiesSize = entities.size();
ticket.m_spawnedEntities.reserve(ticket.m_spawnedEntities.size() + entitiesSize);
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesSize);
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
for(size_t i=0; i<entitiesSize; ++i)
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = entitiesToSpawn.size();
// Map keeps track of ids from template (spawnable) to clone (instance)
// Allowing patch ups of fields referring to entityIds outside of a given entity
EntityIdMap templateToCloneEntityIdMap;
// Reserve buffers
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
// Mark all indices as spawned
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext));
ticket.m_spawnedEntityIndices.push_back(i);
const AZ::Entity& entityTemplate = *entitiesToSpawn[i];
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.emplace_back(clone);
spawnedEntityIndices.push_back(i);
}
// loadAll is true if every entity has been spawned only once
if (spawnedEntities.size() == entitiesToSpawnSize)
{
ticket.m_loadAll = true;
}
else
{
// Case where there were already spawns from a previous request
ticket.m_loadAll = false;
}
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(*request.m_ticket, SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Add to the game context, now the entities are active
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
[](AZ::Entity* entity)
{
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
});
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end()));
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
m_onSpawnedEvent.Signal(ticket.m_spawnable);
@@ -249,24 +308,56 @@ namespace AzFramework
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
{
size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size();
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
const Spawnable::EntityList& entities = ticket.m_spawnable->GetEntities();
size_t entitiesSize = entities.size();
ticket.m_spawnedEntities.reserve(ticket.m_spawnedEntities.size() + entitiesSize);
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesSize);
// Keep track how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
for (size_t index : request.m_entityIndices)
{
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[index], serializeContext));
ticket.m_spawnedEntityIndices.push_back(index);
if (index < entitiesToSpawn.size())
{
const AZ::Entity& entityTemplate = *entitiesToSpawn[index];
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
clone->SetId(AZ::Entity::MakeId());
spawnedEntities.push_back(clone);
spawnedEntityIndices.push_back(index);
}
}
ticket.m_loadAll = false;
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
request.m_preInsertionCallback(
*request.m_ticket,
SpawnableEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
// Add to the game context, now the entities are active
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
[](AZ::Entity* entity)
{
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
});
if (request.m_completionCallback)
{
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end()));
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
}
m_onSpawnedEvent.Signal(ticket.m_spawnable);
@@ -343,10 +434,23 @@ namespace AzFramework
// to load every, simply start over.
ticket.m_spawnedEntityIndices.clear();
size_t entitiesSize = entities.size();
for (size_t i = 0; i < entitiesSize; ++i)
size_t entitiesToSpawnSize = entities.size();
// Map keeps track of ids from template (spawnable) to clone (instance)
// Allowing patch ups of fields referring to entityIds outside of a given entity
EntityIdMap templateToCloneEntityIdMap;
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
// Mark all indices as spawned
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext));
const AZ::Entity& entityTemplate = *entities[i];
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.emplace_back(clone);
ticket.m_spawnedEntityIndices.push_back(i);
}
}
@@ -29,6 +29,8 @@ namespace AZ
namespace AzFramework
{
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
class SpawnableEntitiesManager
: public SpawnableEntitiesInterface::Registrar
{
@@ -47,8 +49,8 @@ namespace AzFramework
// The following functions are thread safe
//
void SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback = {}) override;
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override;
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override;
@@ -90,6 +92,7 @@ namespace AzFramework
struct SpawnAllEntitiesCommand
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
@@ -97,6 +100,7 @@ namespace AzFramework
{
AZStd::vector<size_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
EntitySpawnTicket* m_ticket;
uint32_t m_ticketId;
};
@@ -140,7 +144,11 @@ namespace AzFramework
using Requests = AZStd::variant<SpawnAllEntitiesCommand, SpawnEntitiesCommand, DespawnAllEntitiesCommand, ReloadSpawnableCommand,
ListEntitiesCommand, ClaimEntitiesCommand, BarrierCommand, DestroyTicketCommand>;
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext);
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate,
AZ::SerializeContext& serializeContext);
AZ::Entity* CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext);
@@ -193,13 +193,12 @@ namespace AzFramework
bool handling = false;
for (auto& cameraInput : m_activeCameraInputs)
{
cameraInput->HandleEvents(event, cursorDelta, scrollDelta);
handling = !cameraInput->Idle() || handling;
handling = cameraInput->HandleEvents(event, cursorDelta, scrollDelta) || handling;
}
for (auto& cameraInput : m_idleCameraInputs)
{
cameraInput->HandleEvents(event, cursorDelta, scrollDelta);
handling = cameraInput->HandleEvents(event, cursorDelta, scrollDelta) || handling;
}
return handling;
@@ -262,17 +261,26 @@ namespace AzFramework
{
m_activeCameraInputs[i]->Reset();
m_idleCameraInputs.push_back(m_activeCameraInputs[i]);
m_activeCameraInputs[i] = m_activeCameraInputs[m_activeCameraInputs.size() - 1];
using AZStd::swap;
swap(m_activeCameraInputs[i], m_activeCameraInputs[m_activeCameraInputs.size() - 1]);
m_activeCameraInputs.pop_back();
}
}
void Cameras::Clear()
{
Reset();
AZ_Assert(m_activeCameraInputs.empty(), "Active Camera Inputs is not empty");
m_idleCameraInputs.clear();
}
RotateCameraInput::RotateCameraInput(const InputChannelId rotateChannelId)
: m_rotateChannelId(rotateChannelId)
{
}
void RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
bool RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
const ClickDetector::ClickEvent clickEvent = [&event, this] {
if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&event))
@@ -304,6 +312,11 @@ namespace AzFramework
// noop
break;
}
// note - must also check !ending to ensure the mouse up (release) event
// is not consumed and can be propagated to other systems.
// (don't swallow mouse up events)
return !Idle() && !Ending();
}
Camera RotateCameraInput::StepCamera(
@@ -330,7 +343,7 @@ namespace AzFramework
{
}
void PanCameraInput::HandleEvents(
bool PanCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&event))
@@ -347,6 +360,8 @@ namespace AzFramework
}
}
}
return !Idle();
}
Camera PanCameraInput::StepCamera(
@@ -411,7 +426,7 @@ namespace AzFramework
{
}
void TranslateCameraInput::HandleEvents(
bool TranslateCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&event))
@@ -429,7 +444,8 @@ namespace AzFramework
m_boost = true;
}
}
else if (input->m_state == InputChannel::State::Ended)
// ensure we don't process end events in the idle state
else if (input->m_state == InputChannel::State::Ended && !Idle())
{
m_translation &= ~(translationFromKey(input->m_channelId));
if (m_translation == TranslationType::Nil)
@@ -442,6 +458,8 @@ namespace AzFramework
}
}
}
return !Idle();
}
Camera TranslateCameraInput::StepCamera(
@@ -503,7 +521,7 @@ namespace AzFramework
m_boost = false;
}
void OrbitCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta)
bool OrbitCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta)
{
if (const auto* input = AZStd::get_if<DiscreteInputEvent>(&event))
{
@@ -522,8 +540,10 @@ namespace AzFramework
if (Active())
{
m_orbitCameras.HandleEvents(event, cursorDelta, scrollDelta);
return m_orbitCameras.HandleEvents(event, cursorDelta, scrollDelta);
}
return !Idle();
}
Camera OrbitCameraInput::StepCamera(
@@ -533,7 +553,7 @@ namespace AzFramework
if (Beginning())
{
const auto hasLookAt = [&nextCamera, &targetCamera, lookAtFn = m_lookAtFn] {
const auto hasLookAt = [&nextCamera, &targetCamera, &lookAtFn = m_lookAtFn] {
if (lookAtFn)
{
if (const auto lookAt = lookAtFn())
@@ -585,13 +605,15 @@ namespace AzFramework
return nextCamera;
}
void OrbitDollyScrollCameraInput::HandleEvents(
bool OrbitDollyScrollCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto* scroll = AZStd::get_if<ScrollEvent>(&event))
{
BeginActivation();
}
return !Idle();
}
Camera OrbitDollyScrollCameraInput::StepCamera(
@@ -609,7 +631,7 @@ namespace AzFramework
{
}
void OrbitDollyCursorMoveCameraInput::HandleEvents(
bool OrbitDollyCursorMoveCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto& input = AZStd::get_if<DiscreteInputEvent>(&event))
@@ -626,6 +648,8 @@ namespace AzFramework
}
}
}
return !Idle();
}
Camera OrbitDollyCursorMoveCameraInput::StepCamera(
@@ -637,13 +661,15 @@ namespace AzFramework
return nextCamera;
}
void ScrollTranslationCameraInput::HandleEvents(
bool ScrollTranslationCameraInput::HandleEvents(
const InputEvent& event, [[maybe_unused]] const ScreenVector& cursorDelta, [[maybe_unused]] float scrollDelta)
{
if (const auto* scroll = AZStd::get_if<ScrollEvent>(&event))
{
BeginActivation();
}
return !Idle();
}
Camera ScrollTranslationCameraInput::StepCamera(
@@ -149,7 +149,7 @@ namespace AzFramework
ResetImpl();
}
virtual void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) = 0;
virtual bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) = 0;
virtual Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) = 0;
virtual bool Exclusive() const
@@ -171,16 +171,29 @@ namespace AzFramework
class Cameras
{
public:
void AddCamera(AZStd::shared_ptr<CameraInput> cameraInput);
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta);
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime);
void AddCamera(AZStd::shared_ptr<CameraInput> cameraInput);
//! Reset the state of all cameras.
void Reset();
//! Remove all cameras that were added.
void Clear();
//! Is one of the cameras in the active camera inputs marked as 'exclusive'.
//! @note This implies no other sibling cameras can begin while the exclusive camera is running.
bool Exclusive() const;
private:
AZStd::vector<AZStd::shared_ptr<CameraInput>> m_activeCameraInputs;
AZStd::vector<AZStd::shared_ptr<CameraInput>> m_idleCameraInputs;
};
inline bool Cameras::Exclusive() const
{
return AZStd::any_of(
m_activeCameraInputs.begin(), m_activeCameraInputs.end(), [](const auto& cameraInput) { return cameraInput->Exclusive(); });
}
class CameraSystem
{
public:
@@ -200,7 +213,7 @@ namespace AzFramework
explicit RotateCameraInput(InputChannelId rotateChannelId);
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
private:
@@ -241,7 +254,7 @@ namespace AzFramework
PanCameraInput(InputChannelId panChannelId, PanAxesFn panAxesFn);
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
private:
@@ -282,7 +295,7 @@ namespace AzFramework
explicit TranslateCameraInput(TranslationAxesFn translationAxesFn);
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
void ResetImpl() override;
@@ -352,7 +365,7 @@ namespace AzFramework
{
public:
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
};
@@ -362,7 +375,7 @@ namespace AzFramework
explicit OrbitDollyCursorMoveCameraInput(InputChannelId dollyChannelId);
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
private:
@@ -373,7 +386,7 @@ namespace AzFramework
{
public:
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
};
@@ -383,7 +396,7 @@ namespace AzFramework
using LookAtFn = AZStd::function<AZStd::optional<AZ::Vector3>()>;
// CameraInput overrides ...
void HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
bool HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, float scrollDelta) override;
Camera StepCamera(const Camera& targetCamera, const ScreenVector& cursorDelta, float scrollDelta, float deltaTime) override;
bool Exclusive() const override;
@@ -17,6 +17,17 @@ namespace AzFramework
{
ClickDetector::ClickOutcome ClickDetector::DetectClick(const ClickEvent clickEvent, const ScreenVector& cursorDelta)
{
const auto previousDetectionState = m_detectionState;
if (previousDetectionState == DetectionState::WaitingForMove)
{
// only allow the action to begin if the mouse has been moved a small amount
m_moveAccumulator += ScreenVectorLength(cursorDelta);
if (m_moveAccumulator > m_deadZone)
{
m_detectionState = DetectionState::Moved;
}
}
if (clickEvent == ClickEvent::Down)
{
const auto now = std::chrono::steady_clock::now();
@@ -52,15 +63,9 @@ namespace AzFramework
return clickOutcome;
}
if (m_detectionState == DetectionState::WaitingForMove)
if (previousDetectionState == DetectionState::WaitingForMove && m_detectionState == DetectionState::Moved)
{
// only allow the action to begin if the mouse has been moved a small amount
m_moveAccumulator += ScreenVectorLength(cursorDelta);
if (m_moveAccumulator > m_deadZone)
{
m_detectionState = DetectionState::Moved;
return ClickOutcome::Move;
}
return ClickOutcome::Move;
}
return ClickOutcome::Nil;
@@ -50,7 +50,11 @@ namespace AzFramework
//! Called from any type of 'handle event' function.
ClickOutcome DetectClick(ClickEvent clickEvent, const ScreenVector& cursorDelta);
//! Override the default double click interval.
//! @note Default is 400ms - system default.
void SetDoubleClickInterval(float doubleClickInterval);
//! Override the dead zone before a 'move' outcome will be triggered.
void SetDeadZone(float deadZone);
private:
//! Internal state of ClickDetector based on incoming events.
@@ -72,4 +76,9 @@ namespace AzFramework
{
m_doubleClickInterval = doubleClickInterval;
}
inline void ClickDetector::SetDeadZone(const float deadZone)
{
m_deadZone = deadZone;
}
} // namespace AzFramework
@@ -188,6 +188,12 @@ set(FILES
Script/ScriptDebugMsgReflection.h
Script/ScriptRemoteDebugging.cpp
Script/ScriptRemoteDebugging.h
Session/ISessionHandlingRequests.h
Session/ISessionRequests.cpp
Session/ISessionRequests.h
Session/SessionConfig.cpp
Session/SessionConfig.h
Session/SessionNotifications.h
StreamingInstall/StreamingInstall.h
StreamingInstall/StreamingInstall.cpp
StreamingInstall/StreamingInstallRequests.h
@@ -116,8 +116,8 @@ namespace AzNetworking
int32_t TcpSocket::Receive(uint8_t* outData, uint32_t size) const
{
AZ_Assert(size > 0, "Invalid data size for send");
AZ_Assert(outData != nullptr, "NULL data pointer passed to send");
AZ_Assert(size > 0, "Invalid data size for receive");
AZ_Assert(outData != nullptr, "NULL data pointer passed to receive");
if (!IsOpen())
{
return SocketOpResultErrorNotOpen;
@@ -176,7 +176,7 @@ namespace AzNetworking
if (::bind(aznumeric_cast<int32_t>(m_socketFd), (const sockaddr*)&hints, sizeof(hints)) != 0)
{
const int32_t error = GetLastNetworkError();
AZLOG_ERROR("Failed to bind socket (%d:%s)", error, GetNetworkErrorDesc(error));
AZLOG_ERROR("Failed to bind TCP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
return false;
}
@@ -162,7 +162,7 @@ namespace AzNetworking
const UdpReaderThread::ReceivedPackets* packets = m_readerThread.GetReceivedPackets(m_socket.get());
if (packets == nullptr)
{
AZ_Assert(false, "nullptr was retrieved for the received packet buffer, check that the socket has been registered with the reader thread");
// Socket is not yet registered with the reader thread and is likely still pending, try again later
return;
}
@@ -82,7 +82,7 @@ namespace AzNetworking
if (::bind(static_cast<int32_t>(m_socketFd), (const sockaddr *)&hints, sizeof(hints)) != 0)
{
const int32_t error = GetLastNetworkError();
AZLOG_ERROR("Failed to bind socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
AZLOG_ERROR("Failed to bind UDP socket to port %u (%d:%s)", uint32_t(port), error, GetNetworkErrorDesc(error));
return false;
}
}
@@ -12,6 +12,7 @@
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/IO/GenericStreams.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
@@ -46,6 +47,10 @@ namespace AzToolsFramework
virtual Prefab::InstanceOptionalReference GetRootPrefabInstance() = 0;
//! Get all Assets generated by Prefab processing when entering Play-In Editor mode (Ctrl+G)
//! /return The vector of Assets generated by Prefab processing
virtual const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetPlayInEditorAssetData() = 0;
virtual bool LoadFromStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0;
virtual bool SaveToStream(AZ::IO::GenericStream& stream, AZStd::string_view filename) = 0;
@@ -314,6 +314,11 @@ namespace AzToolsFramework
return *m_rootInstance;
}
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& PrefabEditorEntityOwnershipService::GetPlayInEditorAssetData()
{
return m_playInEditorData.m_assets;
}
void PrefabEditorEntityOwnershipService::OnEntityRemoved(AZ::EntityId entityId)
{
AzFramework::SliceEntityRequestBus::MultiHandler::BusDisconnect(entityId);
@@ -195,6 +195,8 @@ namespace AzToolsFramework
AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override;
Prefab::InstanceOptionalReference GetRootPrefabInstance() override;
const AZStd::vector<AZ::Data::Asset<AZ::Data::AssetData>>& GetPlayInEditorAssetData() override;
//////////////////////////////////////////////////////////////////////////
void OnEntityRemoved(AZ::EntityId entityId);
@@ -134,9 +134,13 @@ namespace AzToolsFramework
}
}
auto findInstancesResult = m_templateInstanceMapperInterface->FindInstancesOwnedByTemplate(instanceTemplateId)->get();
auto findInstancesResult = m_templateInstanceMapperInterface->FindInstancesOwnedByTemplate(instanceTemplateId);
AZ_Assert(
findInstancesResult.has_value(), "Prefab Instances corresponding to template with id %llu couldn't be found.",
instanceTemplateId);
if (findInstancesResult.find(instanceToUpdate) == findInstancesResult.end())
if (findInstancesResult == AZStd::nullopt ||
findInstancesResult->get().find(instanceToUpdate) == findInstancesResult->get().end())
{
// Since nested instances get reconstructed during propagation, remove any nested instance that no longer
// maps to a template.
@@ -182,16 +182,16 @@ namespace AzToolsFramework
else
{
AZ::JsonSerializationResult::ResultCode applyPatchResult = AZ::JsonSerialization::ApplyPatch(
linkedInstanceDom,
sourceTemplateDomCopy,
targetTemplatePrefabDom.GetAllocator(),
sourceTemplatePrefabDom,
patchesReference->get(),
AZ::JsonMergeApproach::JsonPatch);
linkedInstanceDom.CopyFrom(sourceTemplateDomCopy, targetTemplatePrefabDom.GetAllocator());
if (applyPatchResult.GetProcessing() != AZ::JsonSerializationResult::Processing::Completed)
{
AZ_Error("Prefab", false,
"Link::UpdateTarget - "
"ApplyPatches failed for Prefab DOM from source Template '%u' and target Template '%u'.",
AZ_Error(
"Prefab", false,
"Link::UpdateTarget - ApplyPatches failed for Prefab DOM from source Template '%u' and target Template '%u'.",
m_sourceTemplateId, m_targetTemplateId);
return false;
}
@@ -135,11 +135,14 @@ namespace AzToolsFramework
AZ::EntityId containerEntityId = instanceToCreate->get().GetContainerEntityId();
// Parent the entities to the container entity. Parenting the container entities of the instances passed to createPrefab
// will be done during the creation of links below.
for (AZ::Entity* topLevelEntity : entities)
// Parent the non-container top level entities to the container entity.
// Parenting the top level container entities will be done during the creation of links.
for (AZ::Entity* topLevelEntity : topLevelEntities)
{
AZ::TransformBus::Event(topLevelEntity->GetId(), &AZ::TransformBus::Events::SetParent, containerEntityId);
if (!IsInstanceContainerEntity(topLevelEntity->GetId()))
{
AZ::TransformBus::Event(topLevelEntity->GetId(), &AZ::TransformBus::Events::SetParent, containerEntityId);
}
}
// Update the template of the instance since the entities are modified since the template creation.
@@ -155,11 +158,25 @@ namespace AzToolsFramework
AZ_Assert(
nestedInstanceContainerEntity, "Invalid container entity found for the nested instance used in prefab creation.");
AZ::EntityId parentId;
AZ::TransformBus::EventResult(
parentId, nestedInstanceContainerEntity->get().GetId(), &AZ::TransformBus::Events::GetParentId);
auto entityIterator = AZStd::find_if(
entities.begin(), entities.end(), [parentId](AZ::Entity* entity) { return entity->GetId() == parentId; });
// If the previous parent entity of the nested instance is not part of the entities of the newly created prefab,
// then set the parent of the nested prefab as the container entity of the newly created prefab.
if (entityIterator == entities.end())
{
parentId = containerEntityId;
}
// These link creations shouldn't be undone because that would put the template in a non-usable state if a user
// chooses to instantiate the template after undoing the creation.
CreateLink(
{&nestedInstanceContainerEntity->get()}, *nestedInstance, instanceToCreate->get().GetTemplateId(),
undoBatch.GetUndoBatch(), containerEntityId, false);
undoBatch.GetUndoBatch(), parentId, false);
});
// Create a link between the templates of the newly created instance and the instance it's being parented under.
@@ -27,6 +27,7 @@ AZ_PUSH_DISABLE_WARNING(4244 4251 4800, "-Wunknown-warning-option") // 4244: con
#include <QtGui/QTextLayout>
#include <QtGui/QPainter>
#include <QMessageBox>
#include <QStylePainter>
AZ_POP_DISABLE_WARNING
static const int LabelColumnStretch = 2;
@@ -121,6 +122,19 @@ namespace AzToolsFramework
setLayout(m_mainLayout);
}
void PropertyRowWidget::paintEvent(QPaintEvent* event)
{
QStylePainter p(this);
if (CanBeReordered())
{
const QPen linePen(QColor(0x3B3E3F));
p.setPen(linePen);
int indent = m_treeDepth * m_treeIndentation;
p.drawLine(event->rect().topLeft() + QPoint(indent, 0), event->rect().topRight());
}
}
bool PropertyRowWidget::HasChildWidgetAlready() const
{
return m_childWidget != nullptr;
@@ -1661,6 +1675,20 @@ namespace AzToolsFramework
m_nameLabel->setFilter(m_currentFilterString);
}
bool PropertyRowWidget::CanChildrenBeReordered() const
{
return m_containerEditable;
}
bool PropertyRowWidget::CanBeReordered() const
{
if (!m_parentRow)
{
return false;
}
return m_parentRow->CanChildrenBeReordered();
}
}
#include "UI/PropertyEditor/moc_PropertyRowWidget.cpp"
@@ -44,6 +44,7 @@ namespace AzToolsFramework
Q_PROPERTY(bool hasChildRows READ HasChildRows);
Q_PROPERTY(bool isTopLevel READ IsTopLevel);
Q_PROPERTY(int getLevel READ GetLevel);
Q_PROPERTY(bool canBeReordered READ CanBeReordered);
Q_PROPERTY(bool appendDefaultLabelToName READ GetAppendDefaultLabelToName WRITE AppendDefaultLabelToName)
public:
AZ_CLASS_ALLOCATOR(PropertyRowWidget, AZ::SystemAllocator, 0)
@@ -126,6 +127,7 @@ namespace AzToolsFramework
void SetSelectionEnabled(bool selectionEnabled);
void SetSelected(bool selected);
bool eventFilter(QObject *watched, QEvent *event) override;
void paintEvent(QPaintEvent*) override;
/// Apply tooltip to widget and some of its children.
void SetDescription(const QString& text);
@@ -146,6 +148,9 @@ namespace AzToolsFramework
QLabel* GetNameLabel() { return m_nameLabel; }
void SetIndentSize(int w);
void SetAsCustom(bool custom) { m_custom = custom; }
bool CanChildrenBeReordered() const;
bool CanBeReordered() const;
protected:
int CalculateLabelWidth() const;
@@ -18,6 +18,7 @@
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzFramework/Viewport/CameraState.h>
#include <AzFramework/Viewport/ViewportId.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/Viewport/ViewportTypes.h>
@@ -304,4 +305,24 @@ namespace AzToolsFramework
return entityContextId;
}
//! Maps a mouse interaction event to a ClickDetector event.
//! @note Function only cares about up or down events, all other events are mapped to Nil (ignored).
inline AzFramework::ClickDetector::ClickEvent ClickDetectorEventFromViewportInteraction(
const ViewportInteraction::MouseInteractionEvent& mouseInteraction)
{
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left())
{
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down)
{
return AzFramework::ClickDetector::ClickEvent::Down;
}
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Up)
{
return AzFramework::ClickDetector::ClickEvent::Up;
}
}
return AzFramework::ClickDetector::ClickEvent::Nil;
}
} // namespace AzToolsFramework
@@ -14,6 +14,7 @@
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <QApplication>
@@ -27,8 +28,11 @@ namespace AzToolsFramework
{
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down)
m_cursorState.SetCurrentPosition(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
const auto selectClickEvent = ClickDetectorEventFromViewportInteraction(mouseInteraction);
const auto clickOutcome = m_clickDetector.DetectClick(selectClickEvent, m_cursorState.CursorDelta());
if (clickOutcome == AzFramework::ClickDetector::ClickOutcome::Move)
{
if (m_leftMouseDown)
{
@@ -58,8 +62,7 @@ namespace AzToolsFramework
}
}
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Up)
if (clickOutcome == AzFramework::ClickDetector::ClickOutcome::Release)
{
if (m_leftMouseUp)
{
@@ -77,6 +80,8 @@ namespace AzToolsFramework
{
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
m_cursorState.Update();
if (m_boxSelectRegion)
{
debugDisplay.DepthTestOff();
@@ -14,6 +14,8 @@
#include <AzCore/std/functional.h>
#include <AzCore/std/optional.h>
#include <AzFramework/Viewport/ClickDetector.h>
#include <AzFramework/Viewport/CursorState.h>
#include <AzToolsFramework/Viewport/ViewportTypes.h>
#include <QRect>
@@ -26,49 +28,49 @@ namespace AzFramework
namespace AzToolsFramework
{
/// Utility to provide box select (click and drag) support for viewport types.
/// Users can override the mouse event callbacks and display scene function to customize behavior.
//! Utility to provide box select (click and drag) support for viewport types.
//! Users can override the mouse event callbacks and display scene function to customize behavior.
class EditorBoxSelect
{
public:
EditorBoxSelect() = default;
/// Return if a box select action is currently taking place.
//! Return if a box select action is currently taking place.
bool Active() const { return m_boxSelectRegion.has_value(); }
/// Update the box select for various mouse events.
/// Call HandleMouseInteraction from type/system implementing MouseViewportRequests interface.
//! Update the box select for various mouse events.
//! Call HandleMouseInteraction from type/system implementing MouseViewportRequests interface.
void HandleMouseInteraction(
const ViewportInteraction::MouseInteractionEvent& mouseInteraction);
/// Responsible for drawing the 2d box representing the selection in screen space.
//! Responsible for drawing the 2d box representing the selection in screen space.
void Display2d(const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay);
/// Custom drawing behavior to happen during a box select.
//! Custom drawing behavior to happen during a box select.
void DisplayScene(
const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay);
/// Set the left mouse down callback.
//! Set the left mouse down callback.
void InstallLeftMouseDown(
const AZStd::function<void(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)>& leftMouseDown);
/// Set the mouse move callback.
//! Set the mouse move callback.
void InstallMouseMove(
const AZStd::function<void(const ViewportInteraction::MouseInteractionEvent& mouseInteraction)>& mouseMove);
/// Set the left mouse up callback.
//! Set the left mouse up callback.
void InstallLeftMouseUp(
const AZStd::function<void()>& leftMouseUp);
/// Set the display scene callback.
//! Set the display scene callback.
void InstallDisplayScene(
const AZStd::function<void(
const AzFramework::ViewportInfo& viewportInfo,
AzFramework::DebugDisplayRequests& debugDisplay)>& displayScene);
/// Return the box select region.
/// If a box selection is being made, return the current rectangle representing the area.
/// If there is currently no active box select, then the Maybe type will be empty (there will be no region/area).
//! Return the box select region.
//! If a box selection is being made, return the current rectangle representing the area.
//! If there is currently no active box select, then the Maybe type will be empty (there will be no region/area).
const AZStd::optional<QRect>& BoxRegion() const { return m_boxSelectRegion; }
/// Return the active modifiers from the previous frame.
//! Return the active modifiers from the previous frame.
ViewportInteraction::KeyboardModifiers PreviousModifiers() const { return m_previousModifiers; }
private:
@@ -79,7 +81,9 @@ namespace AzToolsFramework
AZStd::function<void(
const AzFramework::ViewportInfo& viewportInfo, AzFramework::DebugDisplayRequests& debugDisplay)> m_displayScene;
AZStd::optional<QRect> m_boxSelectRegion; ///< Maybe/optional value to store box select region while active.
ViewportInteraction::KeyboardModifiers m_previousModifiers; ///< Modifier keys active on the previous frame.
AZStd::optional<QRect> m_boxSelectRegion; //!< Maybe/optional value to store box select region while active.
ViewportInteraction::KeyboardModifiers m_previousModifiers; //!< Modifier keys active on the previous frame.
AzFramework::ClickDetector m_clickDetector; //!< Utility type to detect if a mouse click or move has occurred.
AzFramework::CursorState m_cursorState; //!< Utility type to track the current cursor position (and movement/delta).
};
} // namespace AzToolsFramework
@@ -1782,22 +1782,7 @@ namespace AzToolsFramework
m_cachedEntityIdUnderCursor = m_editorHelpers->HandleMouseInteraction(cameraState, mouseInteraction);
const AzFramework::ClickDetector::ClickEvent selectClickEvent = [&mouseInteraction] {
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left())
{
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down)
{
return AzFramework::ClickDetector::ClickEvent::Down;
}
if (mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Up)
{
return AzFramework::ClickDetector::ClickEvent::Up;
}
}
return AzFramework::ClickDetector::ClickEvent::Nil;
}();
const auto selectClickEvent = ClickDetectorEventFromViewportInteraction(mouseInteraction);
m_cursorState.SetCurrentPosition(mouseInteraction.m_mouseInteraction.m_mousePick.m_screenCoordinates);
const auto clickOutcome = m_clickDetector.DetectClick(selectClickEvent, m_cursorState.CursorDelta());
+90
View File
@@ -0,0 +1,90 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzFramework/Viewport/CameraInput.h>
#include <AzFramework/Windowing/WindowBus.h>
namespace UnitTest
{
class CameraInputFixture : public AllocatorsTestFixture
{
public:
AzFramework::Camera m_camera;
AzFramework::Camera m_targetCamera;
AZStd::shared_ptr<AzFramework::CameraSystem> m_cameraSystem;
bool HandleEventAndUpdate(const AzFramework::InputEvent& event)
{
constexpr float deltaTime = 0.01666f; // 60fps
const bool consumed = m_cameraSystem->HandleEvents(event);
m_camera = m_cameraSystem->StepCamera(m_targetCamera, deltaTime);
return consumed;
}
void SetUp() override
{
AllocatorsTestFixture::SetUp();
AzFramework::ReloadCameraKeyBindings();
m_cameraSystem = AZStd::make_shared<AzFramework::CameraSystem>();
auto firstPersonRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Right);
auto firstPersonTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(AzFramework::LookTranslation);
auto orbitCamera = AZStd::make_shared<AzFramework::OrbitCameraInput>();
auto orbitRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(AzFramework::InputDeviceMouse::Button::Left);
auto orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(AzFramework::OrbitTranslation);
orbitCamera->m_orbitCameras.AddCamera(orbitRotateCamera);
orbitCamera->m_orbitCameras.AddCamera(orbitTranslateCamera);
m_cameraSystem->m_cameras.AddCamera(firstPersonRotateCamera);
m_cameraSystem->m_cameras.AddCamera(firstPersonTranslateCamera);
m_cameraSystem->m_cameras.AddCamera(orbitCamera);
}
void TearDown() override
{
m_cameraSystem->m_cameras.Clear();
m_cameraSystem.reset();
AllocatorsTestFixture::TearDown();
}
};
TEST_F(CameraInputFixture, BeginEndOrbitCameraConsumesCorrectEvents)
{
// set initial mouse position
const bool consumed1 = HandleEventAndUpdate(AzFramework::CursorEvent{AzFramework::ScreenPoint(5, 5)});
// begin orbit camera
const bool consumed2 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{AzFramework::InputDeviceKeyboard::Key::ModifierAltL, AzFramework::InputChannel::State::Began});
// begin listening for orbit rotate (click detector) - event is not consumed
const bool consumed3 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began});
// begin orbit rotate (mouse has moved sufficient distance to initiate)
const bool consumed4 = HandleEventAndUpdate(AzFramework::CursorEvent{AzFramework::ScreenPoint(10, 10)});
// end orbit (mouse up) - event is not consumed
const bool consumed5 = HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Ended});
const auto allConsumed = AZStd::vector<bool>{consumed1, consumed2, consumed3, consumed4, consumed5};
using ::testing::ElementsAre;
EXPECT_THAT(allConsumed, ElementsAre(false, true, false, true, false));
}
} // namespace UnitTest
@@ -139,4 +139,21 @@ namespace UnitTest
EXPECT_THAT(secondaryDownOutcome, Eq(ClickDetector::ClickOutcome::Nil)); // ignored double click
EXPECT_THAT(secondaryUpOutcome, Eq(ClickDetector::ClickOutcome::Nil)); // click not registered
}
// if the click detector registers a mouse down event, but then all intermediate calls are ignored
// (another system may start intercepting events and swallowing them) then when we do receive a mouse
// up event we should ensure we take into account the current delta - if the delta is large, then the
// outcome will be release
TEST_F(ClickDetectorFixture, ClickIsNotRegisteredAfterIgnoringMouseMovesBeforeMouseUpWithLargeDelta)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome downOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome upOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(50, 50));
EXPECT_THAT(downOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(upOutcome, Eq(ClickDetector::ClickOutcome::Release));
}
} // namespace UnitTest
@@ -17,6 +17,7 @@ set(FILES
BinToTextEncode.cpp
ComponentAddRemove.cpp
ComponentAdapterTests.cpp
CameraInputTests.cpp
ClickDetectorTests.cpp
CursorStateTests.cpp
EntityContext.cpp
@@ -97,17 +97,38 @@ namespace SandboxEditor
AzFramework::ViewportDebugDisplayEventBus::Handler::BusDisconnect();
}
// should the camera system respond to this particular event
static bool ShouldHandle(const AzFramework::ViewportControllerPriority priority, const bool exclusive)
{
// ModernViewportCameraControllerInstance receives events at all priorities, it should only respond
// to normal priority events if it is not in 'exclusive' mode and when in 'exclusive' mode it should
// only respond to the highest priority events
return !exclusive && priority == AzFramework::ViewportControllerPriority::Normal ||
exclusive && priority == AzFramework::ViewportControllerPriority::Highest;
}
bool ModernViewportCameraControllerInstance::HandleInputChannelEvent(const AzFramework::ViewportControllerInputEvent& event)
{
AzFramework::WindowSize windowSize;
AzFramework::WindowRequestBus::EventResult(
windowSize, event.m_windowHandle, &AzFramework::WindowRequestBus::Events::GetClientAreaSize);
return m_cameraSystem.HandleEvents(AzFramework::BuildInputEvent(event.m_inputChannel, windowSize));
if (ShouldHandle(event.m_priority, m_cameraSystem.m_cameras.Exclusive()))
{
return m_cameraSystem.HandleEvents(AzFramework::BuildInputEvent(event.m_inputChannel, windowSize));
}
return false;
}
void ModernViewportCameraControllerInstance::UpdateViewport(const AzFramework::ViewportControllerUpdateEvent& event)
{
// only update for a single priority (normal is the default)
if (event.m_priority != AzFramework::ViewportControllerPriority::Normal)
{
return;
}
if (auto viewportContext = RetrieveViewportContext(GetViewportId()))
{
m_updatingTransform = true;
@@ -22,7 +22,9 @@
namespace SandboxEditor
{
class ModernViewportCameraControllerInstance;
class ModernViewportCameraController : public AzFramework::MultiViewportController<ModernViewportCameraControllerInstance>
class ModernViewportCameraController
: public AzFramework::MultiViewportController<
ModernViewportCameraControllerInstance, AzFramework::ViewportControllerPriority::DispatchToAllPriorities>
{
public:
using CameraListBuilder = AZStd::function<void(AzFramework::Cameras&)>;
+5
View File
@@ -38,6 +38,11 @@ AzToolsFramework--ComponentPaletteWidget > QTreeView
background-color: #222222;
}
AzToolsFramework--PropertyRowWidget[canBeReordered="true"] QLabel#Name
{
font-weight: bold;
}
/* Style for visualizing property values overridden from their prefab values */
AzToolsFramework--PropertyRowWidget[IsOverridden=true] #Name QLabel,
AzToolsFramework--ComponentEditorHeader #Title[IsOverridden="true"]
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7088e902885d98953f6a1715efab319c063a4ab8918fd0e810251c8ed82b8514
size 542983
@@ -0,0 +1,4 @@
SPDX-FileCopyrightText: Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy,
SPDX-FileCopyrightText: modify, distribute, perform, and use photos from Unsplash for free, including for commercial
SPDX-FileCopyrightText: purposes, without permission from or attributing the photographer or Unsplash. This license does
SPDX-FileCopyrightText: not include the right to compile photos from Unsplash to replicate a similar or competing service.
@@ -12,18 +12,64 @@
#include <FirstTimeUseScreen.h>
#include <Source/ui_FirstTimeUseScreen.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QIcon>
#include <QSpacerItem>
namespace O3DE::ProjectManager
{
inline constexpr static int s_contentMargins = 80;
inline constexpr static int s_buttonSpacing = 30;
inline constexpr static int s_iconSize = 24;
inline constexpr static int s_spacerSize = 20;
inline constexpr static int s_boxButtonWidth = 210;
inline constexpr static int s_boxButtonHeight = 280;
FirstTimeUseScreen::FirstTimeUseScreen(QWidget* parent)
: ScreenWidget(parent)
, m_ui(new Ui::FirstTimeUseClass())
{
m_ui->setupUi(this);
QVBoxLayout* vLayout = new QVBoxLayout();
setLayout(vLayout);
vLayout->setContentsMargins(s_contentMargins, s_contentMargins, s_contentMargins, s_contentMargins);
connect(m_ui->createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
connect(m_ui->openProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleOpenProjectButton);
QLabel* titleLabel = new QLabel(this);
titleLabel->setText(tr("Ready. Set. Create!"));
titleLabel->setStyleSheet("font-size: 60px");
vLayout->addWidget(titleLabel);
QLabel* introLabel = new QLabel(this);
introLabel->setTextFormat(Qt::AutoText);
introLabel->setText(tr("<html><head/><body><p>Welcome to O3DE! Start something new by creating a project. Not sure what to create? </p><p>Explore what\342\200\231s available by downloading our sample project.</p></body></html>"));
introLabel->setStyleSheet("font-size: 14px");
vLayout->addWidget(introLabel);
QHBoxLayout* buttonLayout = new QHBoxLayout();
buttonLayout->setSpacing(s_buttonSpacing);
m_createProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Add.svg"), tr("Create Project"), this);
m_createProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
buttonLayout->addWidget(m_createProjectButton);
m_addProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Select_Folder.svg"), tr("Add a Project"), this);
m_addProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
buttonLayout->addWidget(m_addProjectButton);
QSpacerItem* buttonSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Expanding, QSizePolicy::Minimum);
buttonLayout->addItem(buttonSpacer);
vLayout->addItem(buttonLayout);
QSpacerItem* verticalSpacer = new QSpacerItem(s_spacerSize, s_spacerSize, QSizePolicy::Minimum, QSizePolicy::Expanding);
vLayout->addItem(verticalSpacer);
// Using border-image allows for scaling options background-image does not support
setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Resources/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }");
connect(m_createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
connect(m_addProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleAddProjectButton);
}
ProjectManagerScreen FirstTimeUseScreen::GetScreenEnum()
@@ -36,9 +82,21 @@ namespace O3DE::ProjectManager
emit ResetScreenRequest(ProjectManagerScreen::NewProjectSettingsCore);
emit ChangeScreenRequest(ProjectManagerScreen::NewProjectSettingsCore);
}
void FirstTimeUseScreen::HandleOpenProjectButton()
void FirstTimeUseScreen::HandleAddProjectButton()
{
emit ChangeScreenRequest(ProjectManagerScreen::ProjectsHome);
}
QPushButton* FirstTimeUseScreen::CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent)
{
QPushButton* largeBoxButton = new QPushButton(icon, text, parent);
largeBoxButton->setFixedSize(s_boxButtonWidth, s_boxButtonHeight);
largeBoxButton->setFlat(true);
largeBoxButton->setFocusPolicy(Qt::FocusPolicy::NoFocus);
largeBoxButton->setStyleSheet("QPushButton { font-size: 14px; background-color: rgba(0, 0, 0, 191); }");
return largeBoxButton;
}
} // namespace O3DE::ProjectManager
@@ -15,10 +15,8 @@
#include <ScreenWidget.h>
#endif
namespace Ui
{
class FirstTimeUseClass;
}
QT_FORWARD_DECLARE_CLASS(QIcon)
QT_FORWARD_DECLARE_CLASS(QPushButton)
namespace O3DE::ProjectManager
{
@@ -32,10 +30,13 @@ namespace O3DE::ProjectManager
protected slots:
void HandleNewProjectButton();
void HandleOpenProjectButton();
void HandleAddProjectButton();
private:
QScopedPointer<Ui::FirstTimeUseClass> m_ui;
QPushButton* CreateLargeBoxButton(const QIcon& icon, const QString& text, QWidget* parent = nullptr);
QPushButton* m_createProjectButton;
QPushButton* m_addProjectButton;
};
} // namespace O3DE::ProjectManager
@@ -1,93 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FirstTimeUseClass</class>
<widget class="QWidget" name="FirstTimeUseClass">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>881</width>
<height>555</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>30</pointsize>
</font>
</property>
<property name="text">
<string>READY. SET. CREATE!</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Welcome to O3DE! Start something new by creating a project. Not sure what to create? &lt;/p&gt;&lt;p&gt;Explore whats available by downloading our sample project.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::AutoText</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QPushButton" name="createProjectButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Create Project</string>
</property>
<property name="icon">
<iconset resource="../project_manager.qrc">
<normaloff>:/Resources/Add.svg</normaloff>:/Resources/Add.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openProjectButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Open a Project</string>
</property>
<property name="icon">
<iconset resource="../project_manager.qrc">
<normaloff>:/Resources/Select_Folder.svg</normaloff>:/Resources/Select_Folder.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../project_manager.qrc"/>
</resources>
<connections/>
</ui>
@@ -27,6 +27,12 @@ namespace O3DE::ProjectManager
, m_ui(new Ui::ProjectManagerWindowClass())
{
m_ui->setupUi(this);
QLayout* layout = m_ui->centralWidget->layout();
layout->setMargin(0);
layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0);
setFixedSize(this->geometry().width(), this->geometry().height());
m_pythonBindings = AZStd::make_unique<PythonBindings>(engineRootPath);
@@ -6,10 +6,16 @@
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
<width>1200</width>
<height>800</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>O3DE Project Manager</string>
</property>
@@ -21,7 +27,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<width>1200</width>
<height>36</height>
</rect>
</property>
@@ -15,18 +15,20 @@
#include <ScreenDefs.h>
#include <QWidget>
#include <QStyleOption>
#include <QPainter>
#endif
namespace O3DE::ProjectManager
{
class ScreenWidget
: public QWidget
: public QFrame
{
Q_OBJECT
public:
explicit ScreenWidget(QWidget* parent = nullptr)
: QWidget(parent)
: QFrame(parent)
{
}
~ScreenWidget() = default;
@@ -22,6 +22,9 @@ namespace O3DE::ProjectManager
: QWidget(parent)
{
QVBoxLayout* vLayout = new QVBoxLayout();
vLayout->setMargin(0);
vLayout->setSpacing(0);
vLayout->setContentsMargins(0, 0, 0, 0);
setLayout(vLayout);
m_screenStack = new QStackedWidget();
@@ -9,5 +9,6 @@
<file>Resources/iOS.svg</file>
<file>Resources/Linux.svg</file>
<file>Resources/macOS.svg</file>
<file>Resources/Backgrounds/FirstTimeBackgroundImage.jpg</file>
</qresource>
</RCC>
@@ -22,7 +22,6 @@ set(FILES
Source/EngineInfo.cpp
Source/FirstTimeUseScreen.h
Source/FirstTimeUseScreen.cpp
Source/FirstTimeUseScreen.ui
Source/ProjectManagerWindow.h
Source/ProjectManagerWindow.cpp
Source/ProjectTemplateInfo.h
@@ -41,18 +41,6 @@ namespace AZ
static AZ::SceneAPI::FbxSceneImporter::FbxImportRequestHandler* g_fbxImporter = nullptr;
static AZStd::vector<AZ::ComponentDescriptor*> g_componentDescriptors;
void Initialize()
{
// Currently it's still needed to explicitly create an instance of this instead of letting
// it be a normal component. This is because ResourceCompilerScene needs to return
// the list of available extensions before it can start the application.
if (!g_fbxImporter)
{
g_fbxImporter = aznew AZ::SceneAPI::FbxSceneImporter::FbxImportRequestHandler();
g_fbxImporter->Activate();
}
}
void Reflect(AZ::SerializeContext* /*context*/)
{
// Descriptor registration is done in Reflect instead of Initialize because the ResourceCompilerScene initializes the libraries before
@@ -64,6 +52,7 @@ namespace AZ
{
// Global importer and behavior
g_componentDescriptors.push_back(FbxSceneBuilder::FbxImporter::CreateDescriptor());
g_componentDescriptors.push_back(FbxSceneImporter::FbxImportRequestHandler::CreateDescriptor());
// Node and attribute importers
g_componentDescriptors.push_back(AssImpBitangentStreamImporter::CreateDescriptor());
@@ -125,7 +114,6 @@ namespace AZ
extern "C" AZ_DLL_EXPORT void InitializeDynamicModule(void* env)
{
AZ::Environment::Attach(static_cast<AZ::EnvironmentInstance>(env));
AZ::SceneAPI::FbxSceneBuilder::Initialize();
}
extern "C" AZ_DLL_EXPORT void Reflect(AZ::SerializeContext* context)
{
@@ -10,12 +10,16 @@
*
*/
#include <AssetProcessor/AssetBuilderSDK/AssetBuilderSDK/AssetBuilderSDK.h>
#include <AzCore/Serialization/EditContextConstants.inl>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <AzCore/StringFunc/StringFunc.h>
#include <SceneAPI/FbxSceneBuilder/FbxImportRequestHandler.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Events/CallProcessorBus.h>
#include <SceneAPI/SceneCore/Events/ImportEventContext.h>
#include <SceneAPI/FbxSceneBuilder/FbxImportRequestHandler.h>
namespace AZ
{
@@ -23,10 +27,25 @@ namespace AZ
{
namespace FbxSceneImporter
{
const char* FbxImportRequestHandler::s_extension = ".fbx";
void SceneImporterSettings::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context); serializeContext)
{
serializeContext->Class<SceneImporterSettings>()
->Version(1)
->Field("SupportedFileTypeExtensions", &SceneImporterSettings::m_supportedFileTypeExtensions);
}
}
void FbxImportRequestHandler::Activate()
{
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
settingsRegistry->GetObject(m_settings, "/O3DE/SceneAPI/AssetImporter");
}
BusConnect();
}
@@ -37,21 +56,29 @@ namespace AZ
void FbxImportRequestHandler::Reflect(ReflectContext* context)
{
SceneImporterSettings::Reflect(context);
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<FbxImportRequestHandler, SceneCore::BehaviorComponent>()->Version(1);
serializeContext->Class<FbxImportRequestHandler, AZ::Component>()->Version(1)->Attribute(
AZ::Edit::Attributes::SystemComponentTags,
AZStd::vector<AZ::Crc32>({AssetBuilderSDK::ComponentTags::AssetBuilder}));
}
}
void FbxImportRequestHandler::GetSupportedFileExtensions(AZStd::unordered_set<AZStd::string>& extensions)
{
extensions.insert(s_extension);
extensions.insert(m_settings.m_supportedFileTypeExtensions.begin(), m_settings.m_supportedFileTypeExtensions.end());
}
Events::LoadingResult FbxImportRequestHandler::LoadAsset(Containers::Scene& scene, const AZStd::string& path, const Uuid& guid, [[maybe_unused]] RequestingApplication requester)
{
if (!AzFramework::StringFunc::Path::IsExtension(path.c_str(), s_extension))
AZStd::string extension;
StringFunc::Path::GetExtension(path.c_str(), extension);
if (!m_settings.m_supportedFileTypeExtensions.contains(extension))
{
return Events::LoadingResult::Ignored;
}
@@ -73,6 +100,11 @@ namespace AZ
return Events::LoadingResult::AssetFailure;
}
}
void FbxImportRequestHandler::GetProvidedServices(ComponentDescriptor::DependencyArrayType& provided)
{
provided.emplace_back(AZ_CRC_CE("AssetImportRequestHandler"));
}
} // namespace Import
} // namespace SceneAPI
} // namespace AZ
@@ -21,12 +21,21 @@ namespace AZ
{
namespace FbxSceneImporter
{
struct SceneImporterSettings
{
AZ_TYPE_INFO(SceneImporterSettings, "{8BB6C7AD-BF99-44DC-9DA1-E7AD3F03DC10}");
static void Reflect(AZ::ReflectContext* context);
AZStd::unordered_set<AZStd::string> m_supportedFileTypeExtensions;
};
class FbxImportRequestHandler
: public SceneCore::BehaviorComponent
: public AZ::Component
, public Events::AssetImportRequestBus::Handler
{
public:
AZ_COMPONENT(FbxImportRequestHandler, "{9F4B189C-0A96-4F44-A5F0-E087FF1561F8}", SceneCore::BehaviorComponent);
AZ_COMPONENT(FbxImportRequestHandler, "{9F4B189C-0A96-4F44-A5F0-E087FF1561F8}");
~FbxImportRequestHandler() override = default;
@@ -38,8 +47,13 @@ namespace AZ
Events::LoadingResult LoadAsset(Containers::Scene& scene, const AZStd::string& path, const Uuid& guid,
RequestingApplication requester) override;
static void GetProvidedServices(ComponentDescriptor::DependencyArrayType& provided);
private:
static const char* s_extension;
SceneImporterSettings m_settings;
static constexpr const char* SettingsFilename = "AssetImporterSettings.json";
};
} // namespace FbxSceneImporter
} // namespace SceneAPI
@@ -260,7 +260,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Animation");
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
// Add check for animation layers at the scene level.
@@ -387,11 +387,10 @@ namespace AZ
}
Events::ProcessingResultCombiner combinedAnimationResult;
for (AZ::u32 meshIndex = 0; meshIndex < currentNode->mNumMeshes; ++meshIndex)
if (context.m_sourceNode.ContainsMesh())
{
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[meshIndex]];
if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(mesh->mName.C_Str());
const aiMesh* firstMesh = scene->mMeshes[currentNode->mMeshes[0]];
if (NodeToChannelToMorphAnim::iterator channelsForMeshName = meshMorphAnimations.find(firstMesh->mName.C_Str());
channelsForMeshName != meshMorphAnimations.end())
{
const auto [nodeIterName, channels] = *channelsForMeshName;
@@ -399,7 +398,7 @@ namespace AZ
{
const auto& [animation, morphAnimation] = animAndMorphAnim;
combinedAnimationResult += ImportBlendShapeAnimation(
context, animation, morphAnimation, mesh);
context, animation, morphAnimation, firstMesh);
}
}
}
@@ -413,32 +412,39 @@ namespace AZ
if (boneAnimations.empty() && !meshMorphAnimations.empty())
{
const aiAnimation* animation = scene->mAnimations[0];
// Morph animations need a regular animation on the node, as well.
// If there is no bone animation on the current node, then generate one here.
AZStd::shared_ptr<SceneData::GraphData::AnimationData> createdAnimationData =
AZStd::make_shared<SceneData::GraphData::AnimationData>();
const size_t numKeyframes = animation->mDuration + 1; // +1 because we start at 0 and the last keyframe is at mDuration instead of mDuration-1
createdAnimationData->ReserveKeyFrames(numKeyframes);
const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond;
createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames);
// Set every frame of the animation to the start location of the node.
aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
context.m_sourceSceneSystem.ConvertUnit(localTransform);
for (AZ::u32 time = 0; time <= animation->mDuration; ++time)
for (AZ::u32 channelIndex = 0; channelIndex < animation->mNumMorphMeshChannels; ++channelIndex)
{
createdAnimationData->AddKeyFrame(localTransform);
const aiMeshMorphAnim* nodeAnim = animation->mMorphMeshChannels[channelIndex];
// Morph animations need a regular animation on the node, as well.
// If there is no bone animation on the current node, then generate one here.
AZStd::shared_ptr<SceneData::GraphData::AnimationData> createdAnimationData =
AZStd::make_shared<SceneData::GraphData::AnimationData>();
const size_t numKeyframes = GetNumKeyFrames(
nodeAnim->mNumKeys,
animation->mDuration,
animation->mTicksPerSecond);
createdAnimationData->ReserveKeyFrames(numKeyframes);
const double timeStepBetweenFrames = 1.0 / animation->mTicksPerSecond;
createdAnimationData->SetTimeStepBetweenFrames(timeStepBetweenFrames);
// Set every frame of the animation to the start location of the node.
aiMatrix4x4 combinedTransform = GetConcatenatedLocalTransform(currentNode);
DataTypes::MatrixType localTransform = AssImpSDKWrapper::AssImpTypeConverter::ToTransform(combinedTransform);
context.m_sourceSceneSystem.SwapTransformForUpAxis(localTransform);
context.m_sourceSceneSystem.ConvertUnit(localTransform);
for (AZ::u32 time = 0; time <= numKeyframes; ++time)
{
createdAnimationData->AddKeyFrame(localTransform);
}
const AZStd::string stubBoneAnimForMorphName(AZStd::string::format("%s%s", nodeName.c_str(), nodeAnim->mName.C_Str()));
Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
context.m_currentGraphPosition, stubBoneAnimForMorphName.c_str(), AZStd::move(createdAnimationData));
context.m_scene.GetGraph().MakeEndPoint(addNode);
}
Containers::SceneGraph::NodeIndex addNode = context.m_scene.GetGraph().AddChild(
context.m_currentGraphPosition, nodeName.c_str(), AZStd::move(createdAnimationData));
context.m_scene.GetGraph().MakeEndPoint(addNode);
return combinedAnimationResult.GetResult();
}
decltype(boneAnimations) parentFillerAnimations;
@@ -446,8 +452,8 @@ namespace AZ
// Go through all the animations and make sure we create animations for bones who's parents don't have an animation
for (auto&& anim : boneAnimations)
{
aiNode* node = scene->mRootNode->FindNode(anim.first.c_str());
aiNode* parent = node->mParent;
const aiNode* node = scene->mRootNode->FindNode(anim.first.c_str());
const aiNode* parent = node->mParent;
while (parent && parent != scene->mRootNode)
{
@@ -598,7 +604,8 @@ namespace AZ
// Keyframes generated for every single frame of the animation.
typedef AZStd::map<int, AZStd::vector<KeyData>> ValueToKeyDataMap;
ValueToKeyDataMap valueToKeyDataMap;
// Key time can be less than zero, normalize to have zero be the lowest time.
double keyOffset = 0;
for (int keyIdx = 0; keyIdx < meshMorphAnim->mNumKeys; keyIdx++)
{
aiMeshMorphKey& key = meshMorphAnim->mKeys[keyIdx];
@@ -609,6 +616,10 @@ namespace AZ
valueToKeyDataMap[currentValue].insert(
AZStd::upper_bound(valueToKeyDataMap[currentValue].begin(), valueToKeyDataMap[currentValue].end(),thisKey),
thisKey);
if (key.mTime < keyOffset)
{
keyOffset = key.mTime;
}
}
}
@@ -631,7 +642,7 @@ namespace AZ
const double time = GetTimeForFrame(frame, animation->mTicksPerSecond);
float weight = 0;
if (!SampleKeyFrame(weight, keys, keys.size(), time, keyIdx))
if (!SampleKeyFrame(weight, keys, keys.size(), time + keyOffset, keyIdx))
{
return Events::ProcessingResult::Failure;
}
@@ -25,7 +25,6 @@
#include <assimp/scene.h>
#include <assimp/mesh.h>
namespace AZ
{
namespace SceneAPI
@@ -44,7 +43,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AssImpBitangentStreamImporter, SceneCore::LoadingComponent>()->Version(2); // LYN-2576
serializeContext->Class<AssImpBitangentStreamImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-3250
}
}
@@ -55,62 +54,79 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
if (!meshDataResult.IsSuccess())
const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex)
{
return meshDataResult.GetError();
}
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
return scene->mMeshes[meshIndex]->HasTangentsAndBitangents();
};
size_t vertexCount = parentMeshData->GetVertexCount();
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes)
{
AZ_Error(Utilities::ErrorWindow, false,
"Tried to construct bitangent stream attribute for invalid or non-mesh parent data, mesh index is invalid");
return Events::ProcessingResult::Failure;
}
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
if (!mesh->HasTangentsAndBitangents())
// If there are no bitangents on any meshes, there's nothing to import in this function.
const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
if (!anyMeshHasTangentsAndBitangents)
{
return Events::ProcessingResult::Ignored;
}
// AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material.
// This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes,
// and the engine has code to do this later.
const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
if (!allMeshesHaveTangentsAndBitangents)
{
const char* mixedBitangentsError =
"Node with name %s has meshes with and without bitangents. "
"Placeholder incorrect bitangents will be generated to allow the data to process, "
"but the source art needs to be fixed to correct this. Either apply bitangents to all meshes on this node, "
"or remove all bitangents from all meshes on this node.";
AZ_Error(
Utilities::ErrorWindow, false, mixedBitangentsError, currentNode->mName.C_Str());
}
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
AZStd::shared_ptr<SceneData::GraphData::MeshVertexBitangentData> bitangentStream =
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexBitangentData>();
// AssImp only has one bitangentStream per mesh.
bitangentStream->SetBitangentSetIndex(0);
bitangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx);
bitangentStream->ReserveContainerSpace(vertexCount);
for (int v = 0; v < mesh->mNumVertices; ++v)
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
const Vector3 bitangent(
AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v]));
bitangentStream->AppendBitangent(bitangent);
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
for (int v = 0; v < mesh->mNumVertices; ++v)
{
if (!mesh->HasTangentsAndBitangents())
{
// This node has mixed meshes with and without bitangents.
// An error was already thrown above. Output stub bitangents so
// the mesh can still be output in some form, even if the data isn't correct.
// The bitangent count needs to match the vertex count on the associated mesh node.
bitangentStream->AppendBitangent(Vector3::CreateAxisY());
}
else
{
const Vector3 bitangent(
AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mBitangents[v]));
bitangentStream->AppendBitangent(bitangent);
}
}
}
AZStd::string nodeName(AZStd::string::format("%s",m_defaultNodeName));
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName);
Events::ProcessingResult bitangentResults;
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, nodeName.c_str());
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, bitangentStream, newIndex, m_defaultNodeName);
bitangentResults = Events::Process(dataPopulated);
if (bitangentResults != Events::ProcessingResult::Failure)
{
bitangentResults = AddAttributeDataNodeWithContexts(dataPopulated);
}
return bitangentResults;
}
@@ -74,37 +74,51 @@ namespace AZ
{
return meshDataResult.GetError();
}
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
int parentMeshIndex = parentMeshData->GetSdkMeshIndex();
Events::ProcessingResultCombiner combinedBlendShapeResult;
// 1. Loop through meshes & anims
// Create storage: Anim to meshes
// 2. Loop through anims & meshes
// Create an anim mesh for each anim, with meshes re-combined.
// AssImp separates meshes that have multiple materials.
// This code re-combines them to match previous FBX SDK behavior,
// so they can be separated by engine code instead.
AZStd::map<AZStd::string_view, AZStd::vector<AZStd::pair<int, int>>> animToMeshToAnimMeshIndices;
for (int nodeMeshIdx = 0; nodeMeshIdx < numMesh; nodeMeshIdx++)
{
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[nodeMeshIdx];
const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
// Each mesh gets its own node in the scene graph, so only generate
// morph targets for the current mesh.
if (parentMeshIndex != nodeMeshIdx || !aiMesh->mNumAnimMeshes)
{
continue;
}
for (int animIdx = 0; animIdx < aiMesh->mNumAnimMeshes; animIdx++)
{
AZStd::shared_ptr<SceneData::GraphData::BlendShapeData> blendShapeData =
AZStd::make_shared<SceneData::GraphData::BlendShapeData>();
aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[animIdx];
AZStd::string nodeName(aiAnimMesh->mName.C_Str());
size_t dotIndex = nodeName.rfind('.');
if (dotIndex != AZStd::string::npos)
{
nodeName.erase(0, dotIndex + 1);
}
RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
AZ_TraceContext("Blend shape name", nodeName);
animToMeshToAnimMeshIndices[aiAnimMesh->mName.C_Str()].emplace_back(nodeMeshIdx, animIdx);
}
}
for (const auto& animToMeshIndex : animToMeshToAnimMeshIndices)
{
AZStd::shared_ptr<SceneData::GraphData::BlendShapeData> blendShapeData =
AZStd::make_shared<SceneData::GraphData::BlendShapeData>();
// Some DCC tools, like Maya, include a full path separated by '.' in the node names.
// For example, "cone_skin_blendShapeNode.cone_squash"
// Downstream processing doesn't want anything but the last part of that node name,
// so find the last '.' and remove anything before it.
AZStd::string nodeName(animToMeshIndex.first);
size_t dotIndex = nodeName.rfind('.');
if (dotIndex != AZStd::string::npos)
{
nodeName.erase(0, dotIndex + 1);
}
int vertexOffset = 0;
RenamedNodesMap::SanitizeNodeName(nodeName, context.m_scene.GetGraph(), context.m_currentGraphPosition, "BlendShape");
AZ_TraceContext("Blend shape name", nodeName);
for (const auto& meshIndex : animToMeshIndex.second)
{
int sceneMeshIdx = context.m_sourceNode.GetAssImpNode()->mMeshes[meshIndex.first];
const aiMesh* aiMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[sceneMeshIdx];
const aiAnimMesh* aiAnimMesh = aiMesh->mAnimMeshes[meshIndex.second];
AZStd::bitset<SceneData::GraphData::BlendShapeData::MaxNumUVSets> uvSetUsedFlags;
for (AZ::u8 uvSetIndex = 0; uvSetIndex < SceneData::GraphData::BlendShapeData::MaxNumUVSets; ++uvSetIndex)
@@ -128,7 +142,7 @@ namespace AZ
context.m_sourceSceneSystem.ConvertUnit(vertex);
blendShapeData->AddPosition(vertex);
blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx);
blendShapeData->SetVertexIndexToControlPointIndexMap(vertIdx + vertexOffset, vertIdx + vertexOffset);
// Add normals
if (aiAnimMesh->HasNormals())
@@ -191,33 +205,36 @@ namespace AZ
}
for (int idx = 0; idx < face.mNumIndices; ++idx)
{
blendFace.vertexIndex[idx] = face.mIndices[idx];
blendFace.vertexIndex[idx] = face.mIndices[idx] + vertexOffset;
}
blendShapeData->AddFace(blendFace);
}
vertexOffset += aiMesh->mNumVertices;
// Report problem if no vertex or face converted to MeshData
if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
{
AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str());
return Events::ProcessingResult::Failure;
}
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
Events::ProcessingResult blendShapeResult;
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName);
blendShapeResult = Events::Process(dataPopulated);
if (blendShapeResult != Events::ProcessingResult::Failure)
{
blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated);
}
combinedBlendShapeResult += blendShapeResult;
}
// Report problem if no vertex or face converted to MeshData
if (blendShapeData->GetVertexCount() <= 0 || blendShapeData->GetFaceCount() <= 0)
{
AZ_Error(Utilities::ErrorWindow, false, "Missing geometry data in blendshape node %s.", nodeName.c_str());
return Events::ProcessingResult::Failure;
}
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
Events::ProcessingResult blendShapeResult;
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, blendShapeData, newIndex, nodeName);
blendShapeResult = Events::Process(dataPopulated);
if (blendShapeResult != Events::ProcessingResult::Failure)
{
blendShapeResult = AddAttributeDataNodeWithContexts(dataPopulated);
}
combinedBlendShapeResult += blendShapeResult;
}
return combinedBlendShapeResult.GetResult();
@@ -46,8 +46,8 @@ namespace AZ
}
void EnumBonesInNode(
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, aiNode*>& mainBoneList,
AZStd::unordered_map<AZStd::string, aiBone*>& boneLookup)
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, const aiNode*>& mainBoneList,
AZStd::unordered_map<AZStd::string, const aiBone*>& boneLookup)
{
/* From AssImp Documentation
a) Create a map or a similar container to store which nodes are necessary for the skeleton. Pre-initialise it for all nodes with a "no".
@@ -62,14 +62,14 @@ namespace AZ
for (unsigned meshIndex = 0; meshIndex < node->mNumMeshes; ++meshIndex)
{
aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]];
const aiMesh* mesh = scene->mMeshes[node->mMeshes[meshIndex]];
for (unsigned boneIndex = 0; boneIndex < mesh->mNumBones; ++boneIndex)
{
aiBone* bone = mesh->mBones[boneIndex];
const aiBone* bone = mesh->mBones[boneIndex];
aiNode* boneNode = scene->mRootNode->FindNode(bone->mName);
aiNode* boneParent = boneNode->mParent;
const aiNode* boneNode = scene->mRootNode->FindNode(bone->mName);
const aiNode* boneParent = boneNode->mParent;
mainBoneList[bone->mName.C_Str()] = boneNode;
boneLookup[bone->mName.C_Str()] = bone;
@@ -85,8 +85,8 @@ namespace AZ
}
void EnumChildren(
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, aiNode*>& mainBoneList,
AZStd::unordered_map<AZStd::string, aiBone*>& boneLookup)
const aiScene* scene, const aiNode* node, AZStd::unordered_map<AZStd::string, const aiNode*>& mainBoneList,
AZStd::unordered_map<AZStd::string, const aiBone*>& boneLookup)
{
EnumBonesInNode(scene, node, mainBoneList, boneLookup);
@@ -102,7 +102,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Bone");
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (IsPivotNode(currentNode->mName))
@@ -118,8 +118,8 @@ namespace AZ
}
else
{
AZStd::unordered_map<AZStd::string, aiNode*> mainBoneList;
AZStd::unordered_map<AZStd::string, aiBone*> boneLookup;
AZStd::unordered_map<AZStd::string, const aiNode*> mainBoneList;
AZStd::unordered_map<AZStd::string, const aiBone*> boneLookup;
EnumChildren(scene, scene->mRootNode, mainBoneList, boneLookup);
if (mainBoneList.find(currentNode->mName.C_Str()) != mainBoneList.end())
@@ -172,7 +172,7 @@ namespace AZ
}
aiMatrix4x4 transform = currentNode->mTransformation;
aiNode* parent = currentNode->mParent;
const aiNode* parent = currentNode->mParent;
while (parent)
{
@@ -11,6 +11,7 @@
*/
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzToolsFramework/Debug/TraceContext.h>
#include <SceneAPI/FbxSceneBuilder/Importers/AssImpColorStreamImporter.h>
@@ -44,7 +45,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AssImpColorStreamImporter, SceneCore::LoadingComponent>()->Version(2); // LYN-2576
serializeContext->Class<AssImpColorStreamImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-3250
}
}
@@ -55,43 +56,64 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
if (!meshDataResult.IsSuccess())
{
return meshDataResult.GetError();
}
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
// This node has at least one mesh, verify that the color channel counts are the same for all meshes.
const int expectedColorChannels = scene->mMeshes[currentNode->mMeshes[0]]->GetNumColorChannels();
const bool allMeshesHaveSameNumberOfColorChannels =
AZStd::all_of(currentNode->mMeshes + 1, currentNode->mMeshes + currentNode->mNumMeshes, [scene, expectedColorChannels](const unsigned int meshIndex)
{
return scene->mMeshes[meshIndex]->GetNumColorChannels() == expectedColorChannels;
});
size_t vertexCount = parentMeshData->GetVertexCount();
AZ_Error(
Utilities::ErrorWindow,
allMeshesHaveSameNumberOfColorChannels,
"Color channel counts for node %s has meshes with different color channel counts. "
"The color channel count for the first mesh will be used, and placeholder incorrect color values "
"will be generated to allow the data to process, but the source art needs to be fixed to correct this. "
"All meshes on this node should have the same number of color channels.",
currentNode->mName.C_Str());
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
if (sdkMeshIndex < 0)
if (expectedColorChannels == 0)
{
AZ_Error(Utilities::ErrorWindow, false,
"Tried to construct color stream attribute for invalid or non-mesh parent data, mesh index is missing");
return Events::ProcessingResult::Failure;
return Events::ProcessingResult::Ignored;
}
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
Events::ProcessingResultCombiner combinedVertexColorResults;
for (int colorSetIndex = 0; colorSetIndex < mesh->GetNumColorChannels(); ++colorSetIndex)
for (int colorSetIndex = 0; colorSetIndex < expectedColorChannels; ++colorSetIndex)
{
AZStd::shared_ptr<SceneData::GraphData::MeshVertexColorData> vertexColors =
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexColorData>();
vertexColors->ReserveContainerSpace(vertexCount);
for (int v = 0; v < mesh->mNumVertices; ++v)
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
AZ::SceneAPI::DataTypes::Color vertexColor(
AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v]));
vertexColors->AppendColor(vertexColor);
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
for (int v = 0; v < mesh->mNumVertices; ++v)
{
if (colorSetIndex < mesh->GetNumColorChannels())
{
AZ::SceneAPI::DataTypes::Color vertexColor(
AssImpSDKWrapper::AssImpTypeConverter::ToColor(mesh->mColors[colorSetIndex][v]));
vertexColors->AppendColor(vertexColor);
}
else
{
// An error was already emitted if this mesh has less color channels
// than other meshes on the parent node. Append an arbitrary color value, fully opaque black,
// so the mesh can still be processed.
// It's better to let the engine load a partially valid mesh than to completely fail.
vertexColors->AppendColor(AZ::SceneAPI::DataTypes::Color(0.0f,0.0f,0.0f,1.0f));
}
}
}
AZStd::string nodeName(AZStd::string::format("%s%d",m_defaultNodeName,colorSetIndex));
AZStd::string nodeName(AZStd::string::format("%s%d", m_defaultNodeName, colorSetIndex));
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
@@ -106,9 +128,7 @@ namespace AZ
combinedVertexColorResults += colorMapResults;
}
return combinedVertexColorResults.GetResult();
}
} // namespace FbxSceneBuilder
@@ -69,7 +69,7 @@ namespace AZ
aiMatrix4x4 GetConcatenatedLocalTransform(const aiNode* currentNode)
{
aiNode* parent = currentNode->mParent;
const aiNode* parent = currentNode->mParent;
aiMatrix4x4 combinedTransform = currentNode->mTransformation;
while (parent)
@@ -62,7 +62,7 @@ namespace AZ
for (int idx = 0; idx < context.m_sourceNode.m_assImpNode->mNumMeshes; ++idx)
{
int meshIndex = context.m_sourceNode.m_assImpNode->mMeshes[idx];
aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex];
const aiMesh* assImpMesh = context.m_sourceScene.GetAssImpScene()->mMeshes[meshIndex];
AZ_Assert(assImpMesh, "Asset Importer Mesh should not be null.");
int materialIndex = assImpMesh->mMaterialIndex;
AZ_TraceContext("Material Index", materialIndex);
@@ -45,7 +45,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Mesh");
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (!context.m_sourceNode.ContainsMesh() || IsSkinnedMesh(*currentNode, *scene))
@@ -45,7 +45,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Skin");
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (!context.m_sourceNode.ContainsMesh() || !IsSkinnedMesh(*currentNode, *scene))
@@ -51,7 +51,7 @@ namespace AZ
{
AZ_TraceContext("Importer", "Skin Weights");
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if(currentNode->mNumMeshes <= 0)
@@ -59,35 +59,21 @@ namespace AZ
return Events::ProcessingResult::Ignored;
}
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
if (!meshDataResult.IsSuccess())
{
return meshDataResult.GetError();
}
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
int parentMeshIndex = parentMeshData->GetSdkMeshIndex();
Events::ProcessingResultCombiner combinedSkinWeightsResult;
// Don't create this until a bone with weights is encountered
Containers::SceneGraph::NodeIndex weightsIndexForMesh;
AZStd::string skinWeightName;
AZStd::shared_ptr<SceneData::GraphData::SkinWeightData> skinWeightData;
const uint64_t totalVertices = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
int vertexCount = 0;
for(unsigned nodeMeshIndex = 0; nodeMeshIndex < currentNode->mNumMeshes; ++nodeMeshIndex)
{
if (nodeMeshIndex != parentMeshIndex)
{
// Only generate skinning data for the parent mesh.
// Each AssImp mesh is assigned to a unique node,
// so the skinning data should be generated as a child node
// for the associated parent mesh.
continue;
}
int sceneMeshIndex = currentNode->mMeshes[nodeMeshIndex];
const aiMesh* mesh = scene->mMeshes[sceneMeshIndex];
// Don't create this until a bone with weights is encountered
Containers::SceneGraph::NodeIndex weightsIndexForMesh;
AZStd::string skinWeightName;
AZStd::shared_ptr<SceneData::GraphData::SkinWeightData> skinWeightData;
for(unsigned b = 0; b < mesh->mNumBones; ++b)
{
const aiBone* bone = mesh->mBones[b];
@@ -100,7 +86,6 @@ namespace AZ
if (!weightsIndexForMesh.IsValid())
{
skinWeightName = s_skinWeightName;
skinWeightName += AZStd::to_string(nodeMeshIndex);
RenamedNodesMap::SanitizeNodeName(skinWeightName, context.m_scene.GetGraph(), context.m_currentGraphPosition);
weightsIndexForMesh =
@@ -116,23 +101,25 @@ namespace AZ
}
Pending pending;
pending.m_bone = bone;
pending.m_numVertices = mesh->mNumVertices;
pending.m_numVertices = totalVertices;
pending.m_skinWeightData = skinWeightData;
pending.m_vertOffset = vertexCount;
m_pendingSkinWeights.push_back(pending);
}
Events::ProcessingResult skinWeightsResult;
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName);
skinWeightsResult = Events::Process(dataPopulated);
if (skinWeightsResult != Events::ProcessingResult::Failure)
{
skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated);
}
combinedSkinWeightsResult += skinWeightsResult;
vertexCount += mesh->mNumVertices;
}
Events::ProcessingResult skinWeightsResult;
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, skinWeightData, weightsIndexForMesh, skinWeightName);
skinWeightsResult = Events::Process(dataPopulated);
if (skinWeightsResult != Events::ProcessingResult::Failure)
{
skinWeightsResult = AddAttributeDataNodeWithContexts(dataPopulated);
}
combinedSkinWeightsResult += skinWeightsResult;
return combinedSkinWeightsResult.GetResult();
}
@@ -153,7 +140,7 @@ namespace AZ
link.boneId = boneId;
link.weight = it.m_bone->mWeights[weight].mWeight;
it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId, link);
it.m_skinWeightData->AddAndSortLink(it.m_bone->mWeights[weight].mVertexId + it.m_vertOffset, link);
}
}
const auto result = m_pendingSkinWeights.empty() ? Events::ProcessingResult::Ignored : Events::ProcessingResult::Success;
@@ -61,6 +61,7 @@ namespace AZ
{
const aiBone* m_bone = nullptr;
unsigned m_numVertices = 0;
unsigned m_vertOffset = 0;
AZStd::shared_ptr<SceneData::GraphData::SkinWeightData> m_skinWeightData;
};
@@ -11,6 +11,7 @@
*/
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzToolsFramework/Debug/TraceContext.h>
#include <SceneAPI/FbxSceneBuilder/Importers/AssImpTangentStreamImporter.h>
@@ -44,7 +45,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AssImpTangentStreamImporter, SceneCore::LoadingComponent>()->Version(2); // LYN-2576
serializeContext->Class<AssImpTangentStreamImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-3250
}
}
@@ -55,62 +56,79 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
if (!meshDataResult.IsSuccess())
const auto meshHasTangentsAndBitangents = [&scene](const unsigned int meshIndex)
{
return meshDataResult.GetError();
}
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
return scene->mMeshes[meshIndex]->HasTangentsAndBitangents();
};
size_t vertexCount = parentMeshData->GetVertexCount();
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
if (sdkMeshIndex < 0 || sdkMeshIndex >= currentNode->mNumMeshes)
{
AZ_Error(Utilities::ErrorWindow, false,
"Tried to construct tangent stream attribute for invalid or non-mesh parent data, mesh index is invalid");
return Events::ProcessingResult::Failure;
}
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
if (!mesh->HasTangentsAndBitangents())
// If there are no tangents on any meshes, there's nothing to import in this function.
const bool anyMeshHasTangentsAndBitangents = AZStd::any_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
if (!anyMeshHasTangentsAndBitangents)
{
return Events::ProcessingResult::Ignored;
}
// AssImp nodes with multiple meshes on them occur when AssImp split a mesh on material.
// This logic recombines those meshes to minimize the changes needed to replace FBX SDK with AssImp, FBX SDK did not separate meshes,
// and the engine has code to do this later.
const bool allMeshesHaveTangentsAndBitangents = AZStd::all_of(currentNode->mMeshes, currentNode->mMeshes + currentNode->mNumMeshes, meshHasTangentsAndBitangents);
if (!allMeshesHaveTangentsAndBitangents)
{
const char* mixedTangentsError =
"Node with name %s has meshes with and without tangents. "
"Placeholder incorrect tangents will be generated to allow the data to process, "
"but the source art needs to be fixed to correct this. Either apply tangents to all meshes on this node, "
"or remove all tangents from all meshes on this node.";
AZ_Error(
Utilities::ErrorWindow, false, mixedTangentsError, currentNode->mName.C_Str());
}
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
AZStd::shared_ptr<SceneData::GraphData::MeshVertexTangentData> tangentStream =
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexTangentData>();
// AssImp only has one tangentStream per mesh.
tangentStream->SetTangentSetIndex(0);
tangentStream->SetTangentSpace(AZ::SceneAPI::DataTypes::TangentSpace::FromFbx);
tangentStream->ReserveContainerSpace(vertexCount);
for (int v = 0; v < mesh->mNumVertices; ++v)
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
// Vector4's constructor that takes in a vector3 sets w to 1.0f automatically.
const Vector4 tangent(AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v]));
tangentStream->AppendTangent(tangent);
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
for (int v = 0; v < mesh->mNumVertices; ++v)
{
if (!mesh->HasTangentsAndBitangents())
{
// This node has mixed meshes with and without tangents.
// An error was already thrown above. Output stub tangents so
// the mesh can still be output in some form, even if the data isn't correct.
// The tangent count needs to match the vertex count on the associated mesh node.
tangentStream->AppendTangent(Vector4(0.f, 1.f, 0.f, 1.f));
}
else
{
const Vector4 tangent(
AssImpSDKWrapper::AssImpTypeConverter::ToVector3(mesh->mTangents[v]));
tangentStream->AppendTangent(tangent);
}
}
}
AZStd::string nodeName(AZStd::string::format("%s", m_defaultNodeName));
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, nodeName.c_str());
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, m_defaultNodeName);
Events::ProcessingResult tangentResults;
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, nodeName.c_str());
AssImpSceneAttributeDataPopulatedContext dataPopulated(context, tangentStream, newIndex, m_defaultNodeName);
tangentResults = Events::Process(dataPopulated);
if (tangentResults != Events::ProcessingResult::Failure)
{
tangentResults = AddAttributeDataNodeWithContexts(dataPopulated);
}
return tangentResults;
}
@@ -50,7 +50,7 @@ namespace AZ
Events::ProcessingResult AssImpTransformImporter::ImportTransform(AssImpSceneNodeAppendedContext& context)
{
AZ_TraceContext("Importer", "transform");
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
if (currentNode == scene->mRootNode || IsPivotNode(currentNode->mName))
@@ -12,17 +12,19 @@
#include <AzCore/Math/Vector2.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/containers/array.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzToolsFramework/Debug/TraceContext.h>
#include <SceneAPI/FbxSceneBuilder/ImportContexts/AssImpImportContexts.h>
#include <SceneAPI/FbxSceneBuilder/Importers/AssImpUvMapImporter.h>
#include <SceneAPI/FbxSceneBuilder/Importers/ImporterUtilities.h>
#include <SceneAPI/FbxSceneBuilder/Importers/Utilities/AssImpMeshImporterUtilities.h>
#include <SceneAPI/SDKWrapper/AssImpNodeWrapper.h>
#include <SceneAPI/SDKWrapper/AssImpSceneWrapper.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
#include <SceneAPI/SceneData/GraphData/MeshData.h>
#include <SceneAPI/SceneData/GraphData/MeshVertexUVData.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
#include <SceneAPI/SDKWrapper/AssImpNodeWrapper.h>
#include <SceneAPI/SDKWrapper/AssImpSceneWrapper.h>
#include <assimp/scene.h>
#include <assimp/mesh.h>
@@ -45,7 +47,7 @@ namespace AZ
SerializeContext* serializeContext = azrtti_cast<SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<AssImpUvMapImporter, SceneCore::LoadingComponent>()->Version(3); // LYN-2506
serializeContext->Class<AssImpUvMapImporter, SceneCore::LoadingComponent>()->Version(4); // LYN-3250
}
}
@@ -56,28 +58,53 @@ namespace AZ
{
return Events::ProcessingResult::Ignored;
}
aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiNode* currentNode = context.m_sourceNode.GetAssImpNode();
const aiScene* scene = context.m_sourceScene.GetAssImpScene();
GetMeshDataFromParentResult meshDataResult(GetMeshDataFromParent(context));
if (!meshDataResult.IsSuccess())
// AssImp separates meshes that have multiple materials.
// This code re-combines them to match previous FBX SDK behavior,
// so they can be separated by engine code instead.
bool foundTextureCoordinates = false;
AZStd::array<int, AI_MAX_NUMBER_OF_TEXTURECOORDS> meshesPerTextureCoordinateIndex = {};
for (int localMeshIndex = 0; localMeshIndex < currentNode->mNumMeshes; ++localMeshIndex)
{
return meshDataResult.GetError();
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[localMeshIndex]];
for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
{
if (!mesh->mTextureCoords[texCoordIndex])
{
continue;
}
++meshesPerTextureCoordinateIndex[texCoordIndex];
foundTextureCoordinates = true;
}
}
const SceneData::GraphData::MeshData* const parentMeshData(meshDataResult.GetValue());
size_t vertexCount = parentMeshData->GetVertexCount();
if (!foundTextureCoordinates)
{
return Events::ProcessingResult::Ignored;
}
int sdkMeshIndex = parentMeshData->GetSdkMeshIndex();
AZ_Assert(sdkMeshIndex >= 0,
"Tried to construct uv stream attribute for invalid or non-mesh parent data, mesh index is missing");
const uint64_t vertexCount = GetVertexCountForAllMeshesOnNode(*currentNode, *scene);
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
{
int meshesWithIndex = meshesPerTextureCoordinateIndex[texCoordIndex];
AZ_Error(
Utilities::ErrorWindow,
meshesWithIndex == 0 || meshesWithIndex == currentNode->mNumMeshes,
"Texture coordinate index %d for node %s is not on all meshes on this node. "
"Placeholder arbitrary texture values will be generated to allow the data to process, but the source art "
"needs to be fixed to correct this. All meshes on this node should have the same number of texture coordinate channels.",
texCoordIndex,
currentNode->mName.C_Str());
}
Events::ProcessingResultCombiner combinedUvMapResults;
for (int texCoordIndex = 0; texCoordIndex < AI_MAX_NUMBER_OF_TEXTURECOORDS; ++texCoordIndex)
for (int texCoordIndex = 0; texCoordIndex < meshesPerTextureCoordinateIndex.size(); ++texCoordIndex)
{
if (!mesh->mTextureCoords[texCoordIndex])
// No meshes have this texture coordinate index, skip it.
if (meshesPerTextureCoordinateIndex[texCoordIndex] == 0)
{
continue;
}
@@ -85,24 +112,55 @@ namespace AZ
AZStd::shared_ptr<SceneData::GraphData::MeshVertexUVData> uvMap =
AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexUVData>();
uvMap->ReserveContainerSpace(vertexCount);
bool customNameFound = false;
AZStd::string name(AZStd::string::format("%s%d", m_defaultNodeName, texCoordIndex));
if (mesh->mTextureCoordsNames[texCoordIndex].length)
for (int sdkMeshIndex = 0; sdkMeshIndex < currentNode->mNumMeshes; ++sdkMeshIndex)
{
name = mesh->mTextureCoordsNames[texCoordIndex].C_Str();
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[sdkMeshIndex]];
if(mesh->mTextureCoords[texCoordIndex])
{
if (mesh->mTextureCoordsNames[texCoordIndex].length > 0)
{
if (!customNameFound)
{
name = mesh->mTextureCoordsNames[texCoordIndex].C_Str();
customNameFound = true;
}
else
{
AZ_Warning(Utilities::WarningWindow,
strcmp(name.c_str(), mesh->mTextureCoordsNames[texCoordIndex].C_Str()) == 0,
"Node %s has conflicting mesh coordinate names at index %d, %s and %s. Using %s.",
currentNode->mName.C_Str(),
texCoordIndex,
name.c_str(),
mesh->mTextureCoordsNames[texCoordIndex].C_Str(),
name.c_str());
}
}
}
for (int v = 0; v < mesh->mNumVertices; ++v)
{
if (mesh->mTextureCoords[texCoordIndex])
{
AZ::Vector2 vertexUV(
mesh->mTextureCoords[texCoordIndex][v].x,
// The engine's V coordinate is reverse of how it's stored in the FBX file.
1.0f - mesh->mTextureCoords[texCoordIndex][v].y);
uvMap->AppendUV(vertexUV);
}
else
{
// An error was already emitted if the UV channels for all meshes on this node do not match.
// Append an arbitrary UV value so that the mesh can still be processed.
// It's better to let the engine load a partially valid mesh than to completely fail.
uvMap->AppendUV(AZ::Vector2::CreateZero());
}
}
}
uvMap->SetCustomName(name.c_str());
for (int v = 0; v < mesh->mNumVertices; ++v)
{
AZ::Vector2 vertexUV(
mesh->mTextureCoords[texCoordIndex][v].x,
// The engine's V coordinate is reverse of how it's stored in the FBX file.
1.0f - mesh->mTextureCoords[texCoordIndex][v].y);
uvMap->AppendUV(vertexUV);
}
Containers::SceneGraph::NodeIndex newIndex =
context.m_scene.GetGraph().AddChild(context.m_currentGraphPosition, name.c_str());
@@ -116,6 +174,7 @@ namespace AZ
}
combinedUvMapResults += uvMapResults;
}
return combinedUvMapResults.GetResult();
@@ -13,6 +13,7 @@
#include <assimp/mesh.h>
#include <assimp/scene.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzCore/std/numeric.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <SceneAPI/FbxSceneBuilder/FbxSceneSystem.h>
#include <SceneAPI/FbxSceneBuilder/ImportContexts/AssImpImportContexts.h>
@@ -24,7 +25,7 @@
namespace AZ::SceneAPI::FbxSceneBuilder
{
bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
const AZStd::function<AZStd::shared_ptr<SceneData::GraphData::MeshData>()>& makeMeshFunc)
{
AZStd::unordered_map<int, int> assImpMatIndexToLYIndex;
@@ -34,17 +35,18 @@ namespace AZ::SceneAPI::FbxSceneBuilder
{
return false;
}
auto newMesh = makeMeshFunc();
newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters());
newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters());
// AssImp separates meshes that have multiple materials.
// This code re-combines them to match previous FBX SDK behavior,
// so they can be separated by engine code instead.
int vertOffset = 0;
for (int m = 0; m < currentNode->mNumMeshes; ++m)
{
auto newMesh = makeMeshFunc();
newMesh->SetUnitSizeInMeters(sceneSystem.GetUnitSizeInMeters());
newMesh->SetOriginalUnitSizeInMeters(sceneSystem.GetOriginalUnitSizeInMeters());
newMesh->SetSdkMeshIndex(m);
aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]];
const aiMesh* mesh = scene->mMeshes[currentNode->mMeshes[m]];
// Lumberyard materials are created in order based on mesh references in the scene
if (assImpMatIndexToLYIndex.find(mesh->mMaterialIndex) == assImpMatIndexToLYIndex.end())
@@ -59,7 +61,7 @@ namespace AZ::SceneAPI::FbxSceneBuilder
sceneSystem.SwapVec3ForUpAxis(vertex);
sceneSystem.ConvertUnit(vertex);
newMesh->AddPosition(vertex);
newMesh->SetVertexIndexToControlPointIndexMap(vertIdx, vertIdx);
newMesh->SetVertexIndexToControlPointIndexMap(vertIdx + vertOffset, vertIdx + vertOffset);
if (mesh->HasNormals())
{
@@ -86,14 +88,15 @@ namespace AZ::SceneAPI::FbxSceneBuilder
}
for (int idx = 0; idx < face.mNumIndices; ++idx)
{
meshFace.vertexIndex[idx] = face.mIndices[idx];
meshFace.vertexIndex[idx] = face.mIndices[idx] + vertOffset;
}
newMesh->AddFace(meshFace, assImpMatIndexToLYIndex[mesh->mMaterialIndex]);
}
vertOffset += mesh->mNumVertices;
meshes.push_back(newMesh);
}
meshes.push_back(newMesh);
return true;
}
@@ -127,4 +130,13 @@ namespace AZ::SceneAPI::FbxSceneBuilder
azrtti_cast<const SceneData::GraphData::MeshData* const>(parentData);
return AZ::Success(parentMeshData);
}
uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene)
{
return AZStd::accumulate(node.mMeshes, node.mMeshes + node.mNumMeshes, uint64_t{ 0u },
[&scene](auto runningTotal, unsigned int meshIndex)
{
return runningTotal + scene.mMeshes[meshIndex]->mNumVertices;
});
}
}
@@ -44,11 +44,16 @@ namespace AZ
namespace FbxSceneBuilder
{
bool BuildSceneMeshFromAssImpMesh(aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
bool BuildSceneMeshFromAssImpMesh(const aiNode* currentNode, const aiScene* scene, const FbxSceneSystem& sceneSystem, AZStd::vector<AZStd::shared_ptr<DataTypes::IGraphObject>>& meshes,
const AZStd::function<AZStd::shared_ptr<SceneData::GraphData::MeshData>()>& makeMeshFunc);
typedef AZ::Outcome<const SceneData::GraphData::MeshData* const, Events::ProcessingResult> GetMeshDataFromParentResult;
GetMeshDataFromParentResult GetMeshDataFromParent(AssImpSceneNodeAppendedContext& context);
// If a node in the original scene file has a mesh with multiple materials on it, the associated AssImp
// node will have multiple meshes on it, broken apart per material. This returns the total number
// of vertices on all meshes on the given node.
uint64_t GetVertexCountForAllMeshesOnNode(const aiNode& node, const aiScene& scene);
}
}
}
@@ -27,6 +27,7 @@ namespace AZ
Containers::SceneGraph::NodeIndex parentNode, const char* defaultName)
{
AZ_TraceContext("Node name", name);
const AZStd::string originalNodeName(name);
bool isNameUpdated = false;
// Nodes can't have an empty name, except of the root, otherwise nodes can't be referenced.
@@ -56,7 +57,7 @@ namespace AZ
// can't reference the same parent in that case. This is to make sure the node can be quickly found as
// the full path will be unique. To fix any issues, an index is appended.
size_t index = 1;
size_t offset = name.length();
const size_t offset = name.length();
while (graph.Find(parentNode, name).IsValid())
{
// Remove the previously tried extension.
@@ -71,7 +72,8 @@ namespace AZ
if (isNameUpdated)
{
AZ_TraceContext("New node name", name);
AZ_TracePrintf(Utilities::WarningWindow, "The name of the node was invalid or conflicting and was updated.");
AZ_TracePrintf(Utilities::WarningWindow, "The name of the node '%s' was invalid or conflicting and was updated to '%s'.",
originalNodeName.c_str(), name.c_str());
}
return isNameUpdated;
@@ -38,12 +38,14 @@ namespace AZ
{
AZ_TracePrintf(SceneAPI::Utilities::LogWindow, "AssImpSceneWrapper::LoadSceneFromFile %s", fileName);
AZ_TraceContext("Filename", fileName);
// aiProcess_JoinIdenticalVertices is not enabled because O3DE has a mesh optimizer that also does this,
// this flag is disabled to keep AssImp output similar to FBX SDK to reduce downstream bugs for the initial AssImp release.
// There's currently a minimum of properties and flags set to maximize compatibility with the existing node graph.
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false);
m_importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, false);
m_sceneFileName = fileName;
m_assImpScene = m_importer.ReadFile(fileName,
aiProcess_Triangulate //Triangulates all faces of all meshes
| aiProcess_JoinIdenticalVertices //Identifies and joins identical vertex data sets for the imported meshes
| aiProcess_LimitBoneWeights //Limits the number of bones that can affect a vertex to a maximum value
//dropping the least important and re-normalizing
| aiProcess_GenNormals); //Generate normals for meshes
@@ -19,6 +19,16 @@ namespace AZ::SceneAPI::Utilities
m_output += AZStd::string::format("\t%s: %s\n", name, data);
}
void DebugOutput::WriteArray(const char* name, const unsigned int* data, int size)
{
m_output += AZStd::string::format("\t%s: ", name);
for (int index = 0; index < size; ++index)
{
m_output += AZStd::string::format("%d, ", data[index]);
}
m_output += AZStd::string::format("\n");
}
void DebugOutput::Write(const char* name, const AZStd::string& data)
{
Write(name, data.c_str());
@@ -29,6 +29,7 @@ namespace AZ::SceneAPI::Utilities
void Write(const char* name, const AZStd::vector<AZStd::vector<T>>& data);
SCENE_CORE_API void Write(const char* name, const char* data);
SCENE_CORE_API void WriteArray(const char* name, const unsigned int* data, int size);
SCENE_CORE_API void Write(const char* name, const AZStd::string& data);
SCENE_CORE_API void Write(const char* name, double data);
SCENE_CORE_API void Write(const char* name, uint64_t data);
@@ -285,8 +285,26 @@ namespace AZ
void BlendShapeData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const
{
output.Write("Positions", m_positions);
int index = 0;
for (const auto& position : m_positions)
{
output.Write(AZStd::string::format("\t%d", index).c_str(), position);
++index;
}
index = 0;
output.Write("Normals", m_normals);
for (const auto& normal : m_normals)
{
output.Write(AZStd::string::format("\t%d", index).c_str(), normal);
++index;
}
index = 0;
output.Write("Faces", m_faces);
for (const auto& face : m_faces)
{
output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3);
++index;
}
}
} // GraphData
} // SceneData
@@ -45,7 +45,6 @@ namespace AZ
behaviorContext->Class<MeshData>()
->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Common)
->Attribute(AZ::Script::Attributes::Module, "scene")
->Method("GetSdkMeshIndex", &MeshData::GetSdkMeshIndex)
->Method("GetControlPointIndex", &MeshData::GetControlPointIndex)
->Method("GetUsedControlPointCount", &MeshData::GetUsedControlPointCount)
->Method("GetUsedPointIndexForControlPoint", &MeshData::GetUsedPointIndexForControlPoint)
@@ -77,10 +76,6 @@ namespace AZ
void MeshData::CloneAttributesFrom(const IGraphObject* sourceObject)
{
IMeshData::CloneAttributesFrom(sourceObject);
if (const auto* typedSource = azrtti_cast<const MeshData*>(sourceObject))
{
SetSdkMeshIndex(typedSource->GetSdkMeshIndex());
}
}
void MeshData::AddPosition(const AZ::Vector3& position)
@@ -111,15 +106,6 @@ namespace AZ
m_faceMaterialIds.push_back(faceMaterialId);
}
void MeshData::SetSdkMeshIndex(int sdkMeshIndex)
{
m_sdkMeshIndex = sdkMeshIndex;
}
int MeshData::GetSdkMeshIndex() const
{
return m_sdkMeshIndex;
}
void MeshData::SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex)
{
m_vertexIndexToControlPointIndexMap[vertexIndex] = controlPointIndex;
@@ -206,8 +192,26 @@ namespace AZ
void MeshData::GetDebugOutput(SceneAPI::Utilities::DebugOutput& output) const
{
output.Write("Positions", m_positions);
int index = 0;
for (const auto& position : m_positions)
{
output.Write(AZStd::string::format("\t%d", index).c_str(), position);
++index;
}
index = 0;
output.Write("Normals", m_normals);
for (const auto& normal : m_normals)
{
output.Write(AZStd::string::format("\t%d", index).c_str(), normal);
++index;
}
index = 0;
output.Write("FaceList", m_faceList);
for (const auto& face : m_faceList)
{
output.WriteArray(AZStd::string::format("\t%d", index).c_str(), face.vertexIndex, 3);
++index;
}
output.Write("FaceMaterialIds", m_faceMaterialIds);
}
}
@@ -49,9 +49,6 @@ namespace AZ
SCENE_DATA_API void AddFace(const AZ::SceneAPI::DataTypes::IMeshData::Face& face,
unsigned int faceMaterialId = AZ::SceneAPI::DataTypes::IMeshData::s_invalidMaterialId);
SCENE_DATA_API void SetSdkMeshIndex(int sdkMeshIndex);
SCENE_DATA_API int GetSdkMeshIndex() const;
SCENE_DATA_API void SetVertexIndexToControlPointIndexMap(int vertexIndex, int controlPointIndex);
SCENE_DATA_API size_t GetUsedControlPointCount() const override;
SCENE_DATA_API int GetControlPointIndex(int vertexIndex) const override;
@@ -80,8 +77,6 @@ namespace AZ
AZStd::unordered_map<int, int> m_vertexIndexToControlPointIndexMap;
AZStd::unordered_map<int, int> m_controlPointToUsedVertexIndexMap;
int m_sdkMeshIndex = -1;
};
}
}
@@ -57,7 +57,6 @@ namespace AZ
meshData->AddNormal(Vector3{0.1f, 0.2f, 0.3f});
meshData->AddNormal(Vector3{0.4f, 0.5f, 0.6f});
meshData->SetOriginalUnitSizeInMeters(10.0f);
meshData->SetSdkMeshIndex(1337);
meshData->SetUnitSizeInMeters(0.5f);
meshData->SetVertexIndexToControlPointIndexMap(0, 10);
meshData->SetVertexIndexToControlPointIndexMap(1, 11);
@@ -252,7 +251,6 @@ namespace AZ
ExpectExecute("TestExpectFloatEquals(meshData:GetNormal(1).z, 0.6)");
ExpectExecute("TestExpectFloatEquals(meshData:GetOriginalUnitSizeInMeters(), 10.0)");
ExpectExecute("TestExpectFloatEquals(meshData:GetUnitSizeInMeters(), 0.5)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetSdkMeshIndex(), 1337)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetUsedControlPointCount(), 4)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(0), 10)");
ExpectExecute("TestExpectIntegerEquals(meshData:GetControlPointIndex(1), 11)");