diff --git a/Code/CryEngine/CrySystem/DebugCallStack.cpp b/Code/CryEngine/CrySystem/DebugCallStack.cpp new file mode 100644 index 0000000000..2a219ce674 --- /dev/null +++ b/Code/CryEngine/CrySystem/DebugCallStack.cpp @@ -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 +#include +#include "System.h" + +#include +#include + +#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 + +#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(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(""); + 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& 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 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::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(m_pSystem); + if (pSystem && pSystem->GetUserCallback()) + { + return pSystem->GetUserCallback()->OnBackupDocument(); + } + + return false; +} + +bool DebugCallStack::SaveCurrentLevel() +{ + CSystem* pSystem = static_cast(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 diff --git a/Code/CryEngine/CrySystem/DebugCallStack.h b/Code/CryEngine/CrySystem/DebugCallStack.h new file mode 100644 index 0000000000..c37e6ba0d4 --- /dev/null +++ b/Code/CryEngine/CrySystem/DebugCallStack.h @@ -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& functions); + + void SetUserDialogEnable(const bool bUserDialogEnable); + + typedef std::map 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 diff --git a/Code/CryEngine/CrySystem/DllMain.cpp b/Code/CryEngine/CrySystem/DllMain.cpp index 7fd620835b..53593821d9 100644 --- a/Code/CryEngine/CrySystem/DllMain.cpp +++ b/Code/CryEngine/CrySystem/DllMain.cpp @@ -14,6 +14,7 @@ #include "CrySystem_precompiled.h" #include "System.h" #include +#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("ExceptionHandlerIsSet"); + const bool handlerIsSet = (envVar && *envVar); + if (!handlerIsSet) + { + ((DebugCallStack*)IDebugCallStack::instance())->installErrorHandler(pSystem); + } +#endif + bool retVal = false; { AZ::Debug::StartupLogSinkReporter initLogSink; diff --git a/Code/CryEngine/CrySystem/IDebugCallStack.cpp b/Code/CryEngine/CrySystem/IDebugCallStack.cpp new file mode 100644 index 0000000000..c14dd2b0da --- /dev/null +++ b/Code/CryEngine/CrySystem/IDebugCallStack.cpp @@ -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 +#include +#include +#include +//#if !defined(LINUX) + +#include + +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(<ime); + tm* today = localtime(<ime); + + 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) diff --git a/Code/CryEngine/CrySystem/IDebugCallStack.h b/Code/CryEngine/CrySystem/IDebugCallStack.h new file mode 100644 index 0000000000..f181b73913 --- /dev/null +++ b/Code/CryEngine/CrySystem/IDebugCallStack.h @@ -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 diff --git a/Code/CryEngine/CrySystem/System.h b/Code/CryEngine/CrySystem/System.h index 631d84d934..b91b1ba059 100644 --- a/Code/CryEngine/CrySystem/System.h +++ b/Code/CryEngine/CrySystem/System.h @@ -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; diff --git a/Code/CryEngine/CrySystem/SystemInit.cpp b/Code/CryEngine/CrySystem/SystemInit.cpp index a2c21ea04c..25d6b9c601 100644 --- a/Code/CryEngine/CrySystem/SystemInit.cpp +++ b/Code/CryEngine/CrySystem/SystemInit.cpp @@ -121,6 +121,10 @@ # include #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, diff --git a/Code/CryEngine/CrySystem/SystemWin32.cpp b/Code/CryEngine/CrySystem/SystemWin32.cpp index c974365bfc..eb6aa17532 100644 --- a/Code/CryEngine/CrySystem/SystemWin32.cpp +++ b/Code/CryEngine/CrySystem/SystemWin32.cpp @@ -46,6 +46,8 @@ #include #endif +#include "IDebugCallStack.h" + #if defined(APPLE) || defined(LINUX) #include #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 } diff --git a/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp b/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp new file mode 100644 index 0000000000..feaffd42aa --- /dev/null +++ b/Code/CryEngine/CrySystem/WindowsErrorReporting.cpp @@ -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 +#include +#include "errorrep.h" +#include "ISystem.h" + +#include + +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 diff --git a/Code/CryEngine/CrySystem/crysystem_files.cmake b/Code/CryEngine/CrySystem/crysystem_files.cmake index 6a56339b85..84250de95b 100644 --- a/Code/CryEngine/CrySystem/crysystem_files.cmake +++ b/Code/CryEngine/CrySystem/crysystem_files.cmake @@ -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 ) diff --git a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h index c5cdbbf331..11f4531124 100644 --- a/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h +++ b/Code/Framework/AzCore/AzCore/Asset/AssetCommon.h @@ -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 + Asset::Asset(const AssetId& id, AssetData* assetData, AssetLoadBehavior loadBehavior) + : m_assetId(id) + , m_assetType(azrtti_typeid()) + , m_loadBehavior(loadBehavior) + { + AZ_Assert(!assetData->m_assetId.IsValid(), "Asset data already has an ID set."); + assetData->m_assetId = id; + SetData(assetData); + } + //========================================================================= template Asset::Asset(const AssetId& id, const AZ::Data::AssetType& type, const AZStd::string& hint) diff --git a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp index 78020cb09d..70000dc9a8 100644 --- a/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/TcpTransport/TcpSocket.cpp @@ -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(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; } diff --git a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp index 870545e6c8..5676d48150 100644 --- a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpNetworkInterface.cpp @@ -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; } diff --git a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp index e642c87623..cbb5f8e6c0 100644 --- a/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/UdpTransport/UdpSocket.cpp @@ -82,7 +82,7 @@ namespace AzNetworking if (::bind(static_cast(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; } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h index 19c236f509..8412361657 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipInterface.h @@ -12,6 +12,7 @@ #pragma once +#include #include #include #include @@ -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>& 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; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp index 02243a8c77..da529d9349 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.cpp @@ -314,6 +314,11 @@ namespace AzToolsFramework return *m_rootInstance; } + const AZStd::vector>& PrefabEditorEntityOwnershipService::GetPlayInEditorAssetData() + { + return m_playInEditorData.m_assets; + } + void PrefabEditorEntityOwnershipService::OnEntityRemoved(AZ::EntityId entityId) { AzFramework::SliceEntityRequestBus::MultiHandler::BusDisconnect(entityId); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h index 48e07df091..3be9b95df0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Entity/PrefabEditorEntityOwnershipService.h @@ -195,6 +195,8 @@ namespace AzToolsFramework AZ::IO::PathView filePath, Prefab::InstanceOptionalReference instanceToParentUnder) override; Prefab::InstanceOptionalReference GetRootPrefabInstance() override; + + const AZStd::vector>& GetPlayInEditorAssetData() override; ////////////////////////////////////////////////////////////////////////// void OnEntityRemoved(AZ::EntityId entityId); diff --git a/Gems/Multiplayer/Code/CMakeLists.txt b/Gems/Multiplayer/Code/CMakeLists.txt index 7a4eaeb014..019f341d0c 100644 --- a/Gems/Multiplayer/Code/CMakeLists.txt +++ b/Gems/Multiplayer/Code/CMakeLists.txt @@ -96,12 +96,12 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) PRIVATE Gem::Multiplayer.Tools.Static ) - + ly_add_target( - NAME Multiplayer.Editor.Static STATIC + NAME Multiplayer.Editor GEM_MODULE NAMESPACE Gem FILES_CMAKE - multiplayer_editor_files.cmake + multiplayer_editor_shared_files.cmake COMPILE_DEFINITIONS PUBLIC MULTIPLAYER_EDITOR @@ -113,7 +113,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) PUBLIC Include BUILD_DEPENDENCIES - PUBLIC + PRIVATE Legacy::CryCommon Legacy::Editor.Headers AZ::AzCore @@ -121,23 +121,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS) AZ::AzNetworking AZ::AzToolsFramework Gem::Multiplayer.Static - ) - - ly_add_target( - NAME Multiplayer.Editor GEM_MODULE - NAMESPACE Gem - FILES_CMAKE - multiplayer_editor_shared_files.cmake - INCLUDE_DIRECTORIES - PRIVATE - . - Source - ${pal_source_dir} - PUBLIC - Include - BUILD_DEPENDENCIES - PRIVATE - Gem::Multiplayer.Editor.Static + Gem::Multiplayer.Tools ) endif() diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayerTools.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayerTools.h new file mode 100644 index 0000000000..c621808f7a --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayerTools.h @@ -0,0 +1,39 @@ +/* +* 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 + +namespace Multiplayer +{ + //! IMultiplayer provides insight into the Multiplayer session and its Agents + class IMultiplayerTools + { + public: + // NetworkPrefabProcessor is the only class that should be setting process network prefab status + friend class NetworkPrefabProcessor; + + AZ_RTTI(IMultiplayerTools, "{E8A80EAB-29CB-4E3B-A0B2-FFCB37060FB0}"); + + virtual ~IMultiplayerTools() = default; + + //! Returns if network prefab processing has created currently active or pending spawnables + //! @return If network prefab processing has created currently active or pending spawnables + virtual bool DidProcessNetworkPrefabs() = 0; + + private: + //! Sets if network prefab processing has created currently active or pending spawnables + //! @param didProcessNetPrefabs if network prefab processing has created currently active or pending spawnables + virtual void SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) = 0; + }; +} diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h new file mode 100644 index 0000000000..b82fab91be --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerConstants.h @@ -0,0 +1,32 @@ +/* +* 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 +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + constexpr AZStd::string_view MPNetworkInterfaceName("MultiplayerNetworkInterface"); + constexpr AZStd::string_view MPEditorInterfaceName("MultiplayerEditorNetworkInterface"); + + constexpr AZStd::string_view LocalHost("127.0.0.1"); + constexpr uint16_t DefaultServerPort = 30090; + constexpr uint16_t DefaultServerEditorPort = 30091; + +} + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 2f934979b1..a1e68aa708 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -59,4 +59,5 @@ + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml new file mode 100644 index 0000000000..8f55ecd2b8 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 10a0d17e73..612601883c 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -23,7 +23,7 @@ namespace Multiplayer { AZ_CVAR(AZ::TimeMs, cl_InputRateMs, AZ::TimeMs{ 33 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Rate at which to sample and process client inputs"); AZ_CVAR(AZ::TimeMs, cl_MaxRewindHistoryMs, AZ::TimeMs{ 2000 }, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of milliseconds to keep for server correction rewind and replay"); -#ifndef _RELEASE +#ifndef AZ_RELEASE_BUILD AZ_CVAR(float, cl_DebugHackTimeMultiplier, 1.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Scalar value used to simulate clock hacking cheats for validating bank time system and anticheat"); #endif @@ -477,7 +477,7 @@ namespace Multiplayer const double inputRate = static_cast(static_cast(cl_InputRateMs)) / 1000.0; const double maxRewindHistory = static_cast(static_cast(cl_MaxRewindHistoryMs)) / 1000.0; -#ifndef _RELEASE +#ifndef AZ_RELEASE_BUILD m_moveAccumulator += deltaTime * cl_DebugHackTimeMultiplier; #else m_moveAccumulator += deltaTime; diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp new file mode 100644 index 0000000000..f684e1f12f --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -0,0 +1,187 @@ +/* + * 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 +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Multiplayer +{ + using namespace AzNetworking; + + AZ_CVAR(bool, editorsv_isDedicated, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether to init as a server expecting data from an Editor. Do not modify unless you're sure of what you're doing."); + + MultiplayerEditorConnection::MultiplayerEditorConnection() + : m_byteStream(&m_buffer) + { + m_networkEditorInterface = AZ::Interface::Get()->CreateNetworkInterface( + AZ::Name(MPEditorInterfaceName), ProtocolType::Tcp, TrustZone::ExternalClientToServer, *this); + if (editorsv_isDedicated) + { + uint16_t editorServerPort = DefaultServerEditorPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("editorsv_port", editorServerPort); + } + AZ_Assert(m_networkEditorInterface, "MP Editor Network Interface was unregistered before Editor Server could start listening."); + m_networkEditorInterface->Listen(editorServerPort); + } + } + + bool MultiplayerEditorConnection::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerInit& packet + ) + { + // Editor Server Init is intended for non-release targets + if (!packet.GetLastUpdate()) + { + // More packets are expected, flush this to the buffer + m_byteStream.Write(TcpPacketEncodingBuffer::GetCapacity(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + } + else + { + // This is the last expected packet, flush it to the buffer + m_byteStream.Write(packet.GetAssetData().GetSize(), reinterpret_cast(packet.ModifyAssetData().GetBuffer())); + + // Read all assets out of the buffer + m_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + AZStd::vector> assetData; + while (m_byteStream.GetCurPos() < m_byteStream.GetLength()) + { + AZ::Data::AssetId assetId; + uint32_t hintSize; + AZStd::string assetHint; + m_byteStream.Read(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + m_byteStream.Read(sizeof(uint32_t), reinterpret_cast(&hintSize)); + assetHint.resize(hintSize); + m_byteStream.Read(hintSize, assetHint.data()); + + size_t assetSize = m_byteStream.GetCurPos(); + AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(m_byteStream, nullptr); + assetSize = m_byteStream.GetCurPos() - assetSize; + AZ::Data::Asset asset = AZ::Data::Asset(assetId, assetDatum, AZ::Data::AssetLoadBehavior::NoLoad); + asset.SetHint(assetHint); + + AZ::Data::AssetInfo assetInfo; + assetInfo.m_assetId = asset.GetId(); + assetInfo.m_assetType = asset.GetType(); + assetInfo.m_relativePath = asset.GetHint(); + assetInfo.m_sizeBytes = assetSize; + + // Register Asset to AssetManager + AZ::Data::AssetManager::Instance().AssignAssetData(asset); + AZ::Data::AssetCatalogRequestBus::Broadcast(&AZ::Data::AssetCatalogRequests::RegisterAsset, asset.GetId(), assetInfo); + + assetData.push_back(asset); + } + + // Now that we've deserialized, clear the byte stream + m_byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + m_byteStream.Truncate(); + + // Load the level via the root spawnable that was registered + const AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; + AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + + // Setup the normal multiplayer connection + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + uint16_t serverPort = DefaultServerPort; + if (auto console = AZ::Interface::Get(); console) + { + console->GetCvarValue("sv_port", serverPort); + } + networkInterface->Listen(serverPort); + + AZLOG_INFO("Editor Server completed asset receive, responding to Editor..."); + return connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady()); + } + + return true; + } + + bool MultiplayerEditorConnection::HandleRequest + ( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerReady& packet + ) + { + if (connection->GetConnectionRole() == ConnectionRole::Connector) + { + // Receiving this packet means Editor sync is done, disconnect + connection->Disconnect(AzNetworking::DisconnectReason::TerminatedByClient, AzNetworking::TerminationEndpoint::Local); + + if (auto console = AZ::Interface::Get(); console) + { + AZ::CVarFixedString remoteAddress; + uint16_t remotePort; + if (console->GetCvarValue("editorsv_serveraddr", remoteAddress) != AZ::GetValueResult::ConsoleVarNotFound && + console->GetCvarValue("editorsv_port", remotePort) != AZ::GetValueResult::ConsoleVarNotFound) + { + // Connect the Editor to the editor server for Multiplayer simulation + AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); + INetworkInterface* networkInterface = + AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); + + const IpAddress ipAddress(remoteAddress.c_str(), remotePort, networkInterface->GetType()); + networkInterface->Connect(ipAddress); + } + } + } + return true; + } + + ConnectResult MultiplayerEditorConnection::ValidateConnect + ( + [[maybe_unused]] const IpAddress& remoteAddress, + [[maybe_unused]] const IPacketHeader& packetHeader, + [[maybe_unused]] ISerializer& serializer + ) + { + return ConnectResult::Accepted; + } + + void MultiplayerEditorConnection::OnConnect([[maybe_unused]] AzNetworking::IConnection* connection) + { + ; + } + + bool MultiplayerEditorConnection::OnPacketReceived(AzNetworking::IConnection* connection, const IPacketHeader& packetHeader, ISerializer& serializer) + { + return MultiplayerEditorPackets::DispatchPacket(connection, packetHeader, serializer, *this); + } + + void MultiplayerEditorConnection::OnPacketLost([[maybe_unused]] IConnection* connection, [[maybe_unused]] PacketId packetId) + { + ; + } + + void MultiplayerEditorConnection::OnDisconnect([[maybe_unused]] AzNetworking::IConnection* connection, [[maybe_unused]] DisconnectReason reason, [[maybe_unused]] TerminationEndpoint endpoint) + { + ; + } +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h new file mode 100644 index 0000000000..d803a60744 --- /dev/null +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h @@ -0,0 +1,58 @@ +/* +* 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 + +#include +#include +#include +#include +#include +#include +#include + +namespace AzNetworking +{ + class INetworkInterface; +} + +namespace Multiplayer +{ + //! MultiplayerEditorConnection is a connection listener to synchronize the Editor and a local server it launches + class MultiplayerEditorConnection final + : public AzNetworking::IConnectionListener + { + public: + MultiplayerEditorConnection(); + ~MultiplayerEditorConnection() = default; + + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerInit& packet); + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerReady& packet); + + //! IConnectionListener interface + //! @{ + AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; + void OnConnect(AzNetworking::IConnection* connection) override; + bool OnPacketReceived(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; + void OnPacketLost(AzNetworking::IConnection* connection, AzNetworking::PacketId packetId) override; + void OnDisconnect(AzNetworking::IConnection* connection, AzNetworking::DisconnectReason reason, AzNetworking::TerminationEndpoint endpoint) override; + //! @} + + private: + + AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; + AZStd::vector m_buffer; + AZ::IO::ByteContainerStream> m_byteStream; + }; +} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp deleted file mode 100644 index 470aa61cd0..0000000000 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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 - -namespace Multiplayer -{ - MultiplayerEditorDispatcher::MultiplayerEditorDispatcher() - { - ; - } -} diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h deleted file mode 100644 index c1058dc8a0..0000000000 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorDispatcher.h +++ /dev/null @@ -1,36 +0,0 @@ -/* -* 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 - -#include -#include -#include -#include - -#include - - -namespace Multiplayer -{ - //! MultiplayerEditorDispatcher is responsible for dispatching delta from the Editor to an Editor launched local server - class MultiplayerEditorDispatcher final - { - public: - MultiplayerEditorDispatcher(); - ~MultiplayerEditorDispatcher() = default; - - private: - }; -} diff --git a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp similarity index 86% rename from Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp rename to Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp index 97c5e4d105..2fe0aabe5f 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.cpp @@ -10,13 +10,13 @@ * */ -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include -#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/MultiplayerEditorGem.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.h similarity index 100% rename from Gems/Multiplayer/Code/Source/MultiplayerEditorGem.h rename to Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorGem.h diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index aec3e7870f..829fe7e495 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -10,12 +10,22 @@ * */ -#include -#include +#include +#include +#include + +#include +#include +#include + +#include #include #include +#include #include #include +#include +#include namespace Multiplayer { @@ -23,8 +33,12 @@ namespace Multiplayer AZ_CVAR(bool, editorsv_enabled, false, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether Editor launching a local server to connect to is supported"); + AZ_CVAR(bool, editorsv_launch, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, + "Whether Editor should launch a server when the server address is localhost"); AZ_CVAR(AZ::CVarFixedString, editorsv_process, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The server executable that should be run. Empty to use the current project's ServerLauncher"); + AZ_CVAR(AZ::CVarFixedString, editorsv_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the server to connect to"); + AZ_CVAR(uint16_t, editorsv_port, DefaultServerEditorPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that the multiplayer editor gem will bind to for traffic"); void MultiplayerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { @@ -57,12 +71,14 @@ namespace Multiplayer void MultiplayerEditorSystemComponent::Activate() { + AzFramework::GameEntityContextEventBus::Handler::BusConnect(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); } void MultiplayerEditorSystemComponent::Deactivate() { AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); + AzFramework::GameEntityContextEventBus::Handler::BusDisconnect(); } void MultiplayerEditorSystemComponent::NotifyRegisterViews() @@ -77,50 +93,6 @@ namespace Multiplayer { switch (event) { - case eNotify_OnBeginGameMode: - { - AZ::TickBus::Handler::BusConnect(); - - if (editorsv_enabled) - { - // Assemble the server's path - AZ::CVarFixedString serverProcess = editorsv_process; - if (serverProcess.empty()) - { - // If enabled but no process name is supplied, try this project's ServerLauncher - serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; - } - - AZ::IO::FixedMaxPathString serverPath = AZ::Utils::GetExecutableDirectory(); - if (!serverProcess.contains(AZ_TRAIT_OS_PATH_SEPARATOR)) - { - // If only the process name is specified, append that as well - serverPath.append(AZ_TRAIT_OS_PATH_SEPARATOR + serverProcess); - } - else - { - // If any path was already specified, then simply assign - serverPath = serverProcess; - } - - if (!serverProcess.ends_with(AZ_TRAIT_OS_EXECUTABLE_EXTENSION)) - { - // Add this platform's exe extension if it's not specified - serverPath.append(AZ_TRAIT_OS_EXECUTABLE_EXTENSION); - } - - // Start the configured server if it's available - AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; - processLaunchInfo.m_commandlineParameters = - AZStd::string::format("\"%s\"", serverPath.c_str()); - processLaunchInfo.m_showWindow = true; - processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; - - m_serverProcess = AzFramework::ProcessWatcher::LaunchProcess( - processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); - } - break; - } case eNotify_OnQuit: AZ_Warning("Multiplayer Editor", m_editor != nullptr, "Multiplayer Editor received On Quit without an Editor pointer."); if (m_editor) @@ -130,25 +102,132 @@ namespace Multiplayer } [[fallthrough]]; case eNotify_OnEndGameMode: - AZ::TickBus::Handler::BusDisconnect(); // Kill the configured server if it's active if (m_serverProcess) { m_serverProcess->TerminateProcess(0); m_serverProcess = nullptr; } + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPEditorInterfaceName)); + if (editorNetworkInterface) + { + editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByClient); + } break; } } - void MultiplayerEditorSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + AzFramework::ProcessWatcher* LaunchEditorServer() { + // Assemble the server's path + AZ::CVarFixedString serverProcess = editorsv_process; + AZ::IO::FixedMaxPath serverPath; + if (serverProcess.empty()) + { + // If enabled but no process name is supplied, try this project's ServerLauncher + serverProcess = AZ::Utils::GetProjectName() + ".ServerLauncher"; + serverPath = AZ::Utils::GetExecutableDirectory(); + serverPath /= serverProcess + AZ_TRAIT_OS_EXECUTABLE_EXTENSION; + } + else + { + serverPath = serverProcess; + } + // Start the configured server if it's available + AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo; + processLaunchInfo.m_commandlineParameters = AZStd::string::format("\"%s\" --editorsv_isDedicated true", serverPath.c_str()); + processLaunchInfo.m_showWindow = true; + processLaunchInfo.m_processPriority = AzFramework::ProcessPriority::PROCESSPRIORITY_NORMAL; + + // Launch the Server and give it a few seconds to boot up + AzFramework::ProcessWatcher* outProcess = AzFramework::ProcessWatcher::LaunchProcess( + processLaunchInfo, AzFramework::ProcessCommunicationType::COMMUNICATOR_TYPE_NONE); + if (outProcess) + { + AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(15000)); + } + + return outProcess; } - int MultiplayerEditorSystemComponent::GetTickOrder() + void MultiplayerEditorSystemComponent::OnGameEntitiesStarted() { - // Tick immediately after the network system component - return AZ::TICK_PLACEMENT + 1; + auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); + if (!prefabEditorEntityOwnershipInterface) + { + AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable"); + } + + // BeginGameMode and Prefab Processing have completed at this point + IMultiplayerTools* mpTools = AZ::Interface::Get(); + if (editorsv_enabled && mpTools != nullptr && mpTools->DidProcessNetworkPrefabs()) + { + const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); + + AZStd::vector buffer; + AZ::IO::ByteContainerStream byteStream(&buffer); + + // Serialize Asset information and AssetData into a potentially large buffer + for (const auto& asset : assetData) + { + AZ::Data::AssetId assetId = asset.GetId(); + AZStd::string assetHint = asset.GetHint(); + uint32_t hintSize = aznumeric_cast(assetHint.size()); + + byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + byteStream.Write(sizeof(uint32_t), reinterpret_cast(&hintSize)); + byteStream.Write(assetHint.size(), assetHint.data()); + AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); + } + + const AZ::CVarFixedString remoteAddress = editorsv_serveraddr; + if (editorsv_launch && LocalHost == remoteAddress) + { + m_serverProcess = LaunchEditorServer(); + } + + // Now that the server has launched, attempt to connect the NetworkInterface + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPEditorInterfaceName)); + AZ_Assert(editorNetworkInterface, "MP Editor Network Interface was unregistered before Editor could connect."); + m_editorConnId = editorNetworkInterface->Connect( + AzNetworking::IpAddress(remoteAddress.c_str(), editorsv_port, AzNetworking::ProtocolType::Tcp)); + + if (m_editorConnId == AzNetworking::InvalidConnectionId) + { + AZ_Warning( + "MultiplayerEditor", false, + "Could not connect to server targeted by Editor. If using a local server, check that it's built and editorsv_launch is true."); + return; + } + + // Read the buffer into EditorServerInit packets until we've flushed the whole thing + byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + + while (byteStream.GetCurPos() < byteStream.GetLength()) + { + MultiplayerEditorPackets::EditorServerInit packet; + AzNetworking::TcpPacketEncodingBuffer& outBuffer = packet.ModifyAssetData(); + + // Size the packet's buffer appropriately + size_t readSize = TcpPacketEncodingBuffer::GetCapacity(); + size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); + if (byteStreamSize < readSize) + { + readSize = byteStreamSize; + } + + outBuffer.Resize(readSize); + byteStream.Read(readSize, outBuffer.GetBuffer()); + + // If we've run out of buffer, mark that we're done + if (byteStream.GetCurPos() == byteStream.GetLength()) + { + packet.SetLastUpdate(true); + } + editorNetworkInterface->SendReliablePacket(m_editorConnId, packet); + } + } + } } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index 8c18a2e57a..81b138c675 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -14,15 +14,16 @@ #include +#include + #include #include #include #include - +#include #include #include - namespace AzNetworking { class INetworkInterface; @@ -33,7 +34,7 @@ namespace Multiplayer //! Multiplayer system component wraps the bridging logic between the game and transport layer. class MultiplayerEditorSystemComponent final : public AZ::Component - , private AZ::TickBus::Handler + , private AzFramework::GameEntityContextEventBus::Handler , private AzToolsFramework::EditorEvents::Bus::Handler , private IEditorNotifyListener { @@ -59,17 +60,19 @@ namespace Multiplayer void NotifyRegisterViews() override; //! @} - private: - - //! AZ::TickBus::Handler overrides. + private: + //! EditorEvents::Handler overrides //! @{ - void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; - int GetTickOrder() override; - //! @} - //! void OnEditorNotifyEvent(EEditorNotifyEvent event) override; + //! @} + + //! GameEntityContextEventBus::Handler overrides + //! @{ + void OnGameEntitiesStarted() override; + //! @} IEditor* m_editor = nullptr; AzFramework::ProcessWatcher* m_serverProcess = nullptr; + AzNetworking::ConnectionId m_editorConnId; }; } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 8eb5bf7b0c..aa6fe6e72c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -10,22 +10,27 @@ * */ -#include -#include -#include -#include -#include -#include -#include +#include #include -#include + +#include +#include +#include +#include +#include +#include +#include + #include +#include #include #include #include #include #include +#include #include +#include namespace AZ::ConsoleTypeHelpers { @@ -59,11 +64,8 @@ namespace Multiplayer { using namespace AzNetworking; - static const AZStd::string_view s_networkInterfaceName("MultiplayerNetworkInterface"); - static constexpr uint16_t DefaultServerPort = 30090; - AZ_CVAR(uint16_t, cl_clientport, 0, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port to bind to for game traffic when connecting to a remote host, a value of 0 will select any available port"); - AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, "127.0.0.1", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); + AZ_CVAR(AZ::CVarFixedString, cl_serveraddr, AZ::CVarFixedString(LocalHost), nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The address of the remote server or host to connect to"); AZ_CVAR(AZ::CVarFixedString, cl_serverpassword, "", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Optional server password"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); @@ -140,7 +142,7 @@ namespace Multiplayer void MultiplayerSystemComponent::Activate() { AZ::TickBus::Handler::BusConnect(); - m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(s_networkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); + m_networkInterface = AZ::Interface::Get()->CreateNetworkInterface(AZ::Name(MPNetworkInterfaceName), sv_protocol, TrustZone::ExternalClientToServer, *this); m_consoleCommandHandler.Connect(AZ::Interface::Get()->GetConsoleCommandInvokedEvent()); AZ::Interface::Register(this); @@ -664,7 +666,7 @@ namespace Multiplayer { Multiplayer::MultiplayerAgentType serverType = sv_isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer; AZ::Interface::Get()->InitializeMultiplayer(serverType); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); networkInterface->Listen(sv_port); } AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to"); @@ -672,7 +674,7 @@ namespace Multiplayer void connect([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Client); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); if (arguments.size() < 1) { @@ -702,7 +704,7 @@ namespace Multiplayer void disconnect([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::Uninitialized); - INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(s_networkInterfaceName)); + INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MPNetworkInterfaceName)); auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; networkInterface->GetConnectionSet().VisitConnections(visitor); } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 6bedd0599b..1410a82a3c 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -12,17 +12,20 @@ #pragma once +#include +#include +#include +#include +#include + #include #include #include #include +#include #include #include #include -#include -#include -#include -#include namespace AzNetworking { @@ -72,7 +75,7 @@ namespace Multiplayer bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::NotifyClientMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::EntityMigration& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerPackets::ReadyForEntityUpdates& packet); - + //! IConnectionListener interface //! @{ AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; @@ -109,6 +112,7 @@ namespace Multiplayer AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); AzNetworking::INetworkInterface* m_networkInterface = nullptr; + AzNetworking::INetworkInterface* m_networkEditorInterface = nullptr; AZ::ConsoleCommandInvokedEvent::Handler m_consoleCommandHandler; AZ::ThreadSafeDeque m_cvarCommands; @@ -124,5 +128,9 @@ namespace Multiplayer AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = InvalidHostFrameId; + +#if !defined(AZ_RELEASE_BUILD) + MultiplayerEditorConnection m_editorConnectionListener; +#endif }; } diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp index 5a223d6214..3646dd52a4 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.cpp @@ -10,40 +10,40 @@ * */ -#include -#include +#include +#include #include + #include #include namespace Multiplayer { - //! Multiplayer Tools system component provides serialize context reflection for tools-only systems. - class MultiplayerToolsSystemComponent final - : public AZ::Component + + void MultiplayerToolsSystemComponent::Reflect(AZ::ReflectContext* context) { - public: - AZ_COMPONENT(MultiplayerToolsSystemComponent, "{65AF5342-0ECE-423B-B646-AF55A122F72B}"); + NetworkPrefabProcessor::Reflect(context); + } - static void Reflect(AZ::ReflectContext* context) - { - NetworkPrefabProcessor::Reflect(context); - } + void MultiplayerToolsSystemComponent::Activate() + { + AZ::Interface::Register(this); + } - MultiplayerToolsSystemComponent() = default; - ~MultiplayerToolsSystemComponent() override = default; + void MultiplayerToolsSystemComponent::Deactivate() + { + AZ::Interface::Unregister(this); + } - /// AZ::Component overrides. - void Activate() override - { + bool MultiplayerToolsSystemComponent::DidProcessNetworkPrefabs() + { + return m_didProcessNetPrefabs; + } - } - - void Deactivate() override - { - - } - }; + void MultiplayerToolsSystemComponent::SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) + { + m_didProcessNetPrefabs = didProcessNetPrefabs; + } MultiplayerToolsModule::MultiplayerToolsModule() : AZ::Module() diff --git a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h index 823bd63a1d..b05e2aa5fb 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerToolsModule.h @@ -12,10 +12,36 @@ #pragma once +#include #include +#include namespace Multiplayer { + class MultiplayerToolsSystemComponent final + : public AZ::Component + , public IMultiplayerTools + { + public: + AZ_COMPONENT(MultiplayerToolsSystemComponent, "{65AF5342-0ECE-423B-B646-AF55A122F72B}"); + + static void Reflect(AZ::ReflectContext* context); + + MultiplayerToolsSystemComponent() = default; + ~MultiplayerToolsSystemComponent() override = default; + + /// AZ::Component overrides. + void Activate() override; + void Deactivate() override; + + bool DidProcessNetworkPrefabs() override; + + private: + void SetDidProcessNetworkPrefabs(bool didProcessNetPrefabs) override; + + bool m_didProcessNetPrefabs = false; + }; + class MultiplayerToolsModule : public AZ::Module { diff --git a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp index 93136ab261..0bbfe17801 100644 --- a/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp +++ b/Gems/Multiplayer/Code/Source/Pipeline/NetworkPrefabProcessor.cpp @@ -10,7 +10,11 @@ * */ -#include +#include +#include +#include +#include +#include #include #include @@ -18,9 +22,6 @@ #include #include #include -#include -#include -#include namespace Multiplayer { @@ -29,9 +30,20 @@ namespace Multiplayer void NetworkPrefabProcessor::Process(PrefabProcessorContext& context) { + IMultiplayerTools* mpTools = AZ::Interface::Get(); + if (mpTools) + { + mpTools->SetDidProcessNetworkPrefabs(false); + } + context.ListPrefabs([&context](AZStd::string_view prefabName, PrefabDom& prefab) { ProcessPrefab(context, prefabName, prefab); }); + + if (mpTools && !context.GetProcessedObjects().empty()) + { + mpTools->SetDidProcessNetworkPrefabs(true); + } } void NetworkPrefabProcessor::Reflect(AZ::ReflectContext* context) diff --git a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp index 2e0b9c0759..6ace4db592 100644 --- a/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp +++ b/Gems/Multiplayer/Code/Tests/MultiplayerSystemTests.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ namespace UnitTest SetupAllocator(); AZ::NameDictionary::Create(); m_spawnableComponent = new AzFramework::SpawnableSystemComponent(); + m_netComponent = new AzNetworking::NetworkingSystemComponent(); m_mpComponent = new Multiplayer::MultiplayerSystemComponent(); m_initHandler = Multiplayer::SessionInitEvent::Handler([this](AzNetworking::INetworkInterface* value) { TestInitEvent(value); }); @@ -43,6 +45,7 @@ namespace UnitTest void TearDown() override { delete m_mpComponent; + delete m_netComponent; delete m_spawnableComponent; AZ::NameDictionary::Destroy(); TeardownAllocator(); @@ -71,6 +74,7 @@ namespace UnitTest Multiplayer::SessionShutdownEvent::Handler m_shutdownHandler; Multiplayer::ConnectionAcquiredEvent::Handler m_connAcquiredHandler; + AzNetworking::NetworkingSystemComponent* m_netComponent = nullptr; Multiplayer::MultiplayerSystemComponent* m_mpComponent = nullptr; AzFramework::SpawnableSystemComponent* m_spawnableComponent = nullptr; }; diff --git a/Gems/Multiplayer/Code/multiplayer_editor_files.cmake b/Gems/Multiplayer/Code/multiplayer_editor_files.cmake deleted file mode 100644 index ce3e3227e0..0000000000 --- a/Gems/Multiplayer/Code/multiplayer_editor_files.cmake +++ /dev/null @@ -1,15 +0,0 @@ -# -# 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. -# - -set(FILES - Source/Editor/MultiplayerEditorDispatcher.cpp - Source/Editor/MultiplayerEditorDispatcher.h -) diff --git a/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake b/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake index 2d5611d4b6..3fb76061b8 100644 --- a/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_editor_shared_files.cmake @@ -12,8 +12,8 @@ set(FILES Source/MultiplayerGem.cpp Source/MultiplayerGem.h - Source/MultiplayerEditorGem.cpp - Source/MultiplayerEditorGem.h + Source/Editor/MultiplayerEditorGem.cpp + Source/Editor/MultiplayerEditorGem.h Source/Editor/MultiplayerEditorSystemComponent.cpp Source/Editor/MultiplayerEditorSystemComponent.h ) diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index addd4ea810..eb856a48db 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -11,6 +11,8 @@ set(FILES Include/Multiplayer/IMultiplayer.h + Include/Multiplayer/IMultiplayerTools.h + Include/Multiplayer/MultiplayerConstants.h Include/Multiplayer/MultiplayerStats.h Include/Multiplayer/MultiplayerTypes.h Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -46,6 +48,7 @@ set(FILES Source/AutoGen/AutoComponentTypes_Source.jinja Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml Source/AutoGen/Multiplayer.AutoPackets.xml + Source/AutoGen/MultiplayerEditor.AutoPackets.xml Source/AutoGen/NetworkTransformComponent.AutoComponent.xml Source/Components/LocalPredictionPlayerInputComponent.cpp Source/Components/MultiplayerComponent.cpp @@ -59,6 +62,8 @@ set(FILES Source/ConnectionData/ServerToClientConnectionData.cpp Source/ConnectionData/ServerToClientConnectionData.h Source/ConnectionData/ServerToClientConnectionData.inl + Source/Editor/MultiplayerEditorConnection.cpp + Source/Editor/MultiplayerEditorConnection.h Source/EntityDomains/FullOwnershipEntityDomain.cpp Source/EntityDomains/FullOwnershipEntityDomain.h Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp diff --git a/Gems/Multiplayer/Code/multiplayer_tools_files.cmake b/Gems/Multiplayer/Code/multiplayer_tools_files.cmake index 1be02fd999..3fef954ba6 100644 --- a/Gems/Multiplayer/Code/multiplayer_tools_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_tools_files.cmake @@ -10,6 +10,7 @@ # set(FILES + Include/Multiplayer/IMultiplayerTools.h Source/Multiplayer_precompiled.cpp Source/Multiplayer_precompiled.h Source/Pipeline/NetworkPrefabProcessor.cpp