From 6488c9076a1bf5606bd2174b3fe5a299d4556d42 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Thu, 31 Jul 2025 23:53:29 +0200 Subject: [PATCH 01/83] bugfix(asciistring): Fix thread unsafe reference counting of AsciiString (#1395) --- GeneralsMD/Code/Main/WinMain.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index a600d90618b..55ed50d744d 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -774,6 +774,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // default in a DevStudio project // + TheAsciiStringCriticalSection = &critSec1; TheUnicodeStringCriticalSection = &critSec2; TheDmaCriticalSection = &critSec3; TheMemoryPoolCriticalSection = &critSec4; From 7603545a3e249ad0c05649b6ce6a5bd75defaa25 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:53:57 -0400 Subject: [PATCH 02/83] - Use data dir for crash dumps instead of game dir (Fix for EA client locking the folder...) - Missing break --- .../GameNetwork/GeneralsOnline/OnlineServices_Init.cpp | 8 +++++++- .../GeneralsOnline/OnlineServices_RoomsInterface.cpp | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 244618413dc..d137ba095bb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -384,9 +384,15 @@ void NGMP_OnlineServicesManager::Tick() void NGMP_OnlineServicesManager::InitSentry() { + std::string strDumpPath = std::format("{}/GeneralsOnlineCrashData/", TheGlobalData->getPath_UserData().str()); + if (!std::filesystem::exists(strDumpPath)) + { + std::filesystem::create_directory(strDumpPath); + } + sentry_options_t* options = sentry_options_new(); sentry_options_set_dsn(options, "{REPLACE_SENTRY_DSN}"); - sentry_options_set_database_path(options, ".sentry-native"); + sentry_options_set_database_path(options, strDumpPath.c_str()); sentry_options_set_release(options, "generalsonline-client@0.1"); #if _DEBUG diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 9bee7416b17..979f66aeeb4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp @@ -326,6 +326,7 @@ void WebSocket::Tick() NGMP_OnlineServicesManager::GetInstance()->GetLobbyInterface()->m_callbackStartGamePacket(); } } + break; case EWebSocketMessageID::NETWORK_SIGNAL: { From e89b48e0d9ad713e6fef13142668eeb3f7a2147f Mon Sep 17 00:00:00 2001 From: ArcticDolphin <5984296+tintinhamans@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:37:15 +0200 Subject: [PATCH 03/83] ci(build): Ensure runner uses windows-2022 image for build jobs (#1405) --- .github/workflows/build-toolchain.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-toolchain.yml b/.github/workflows/build-toolchain.yml index 4ce439e0258..86a28770180 100644 --- a/.github/workflows/build-toolchain.yml +++ b/.github/workflows/build-toolchain.yml @@ -29,7 +29,7 @@ on: jobs: build: name: Preset ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} - runs-on: windows-latest + runs-on: windows-2022 timeout-minutes: 40 steps: - name: Checkout Code @@ -66,7 +66,7 @@ jobs: $fileHash = (Get-FileHash -Path VS6_VisualStudio6.7z -Algorithm SHA256).Hash Write-Host "Downloaded file SHA256: $fileHash" Write-Host "Expected file SHA256: $env:EXPECTED_HASH" - if ($hash -ne $env:EXPECTED_HASH) { + if ($fileHash -ne $env:EXPECTED_HASH) { Write-Error "Hash verification failed! File may be corrupted or tampered with." exit 1 } From e06f5982301613b307466b675eabafe46ae2c0c3 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:39:26 +0200 Subject: [PATCH 04/83] bugfix(gametext): Prevent crashing in WorldBuilder, MapCacheBuilder by moving updateWindowTitle() out of GameText class (#1396) --- .../GameEngine/Source/Common/GameEngine.cpp | 72 ++++++++++++++++++ .../GameEngine/Source/GameClient/GameText.cpp | 76 ------------------- .../GameEngine/Source/Common/GameEngine.cpp | 72 ++++++++++++++++++ .../GameEngine/Source/GameClient/GameText.cpp | 76 ------------------- 4 files changed, 144 insertions(+), 152 deletions(-) diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 228277a8fcc..ec2276b8790 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -83,6 +83,7 @@ #include "GameLogic/ScriptEngine.h" #include "GameLogic/SidesList.h" +#include "GameClient/ClientInstance.h" #include "GameClient/Display.h" #include "GameClient/FXList.h" #include "GameClient/GameClient.h" @@ -171,6 +172,76 @@ extern CComModule _Module; //------------------------------------------------------------------------------------------------- static void updateTGAtoDDS(); +//------------------------------------------------------------------------------------------------- +static void updateWindowTitle() +{ + // TheSuperHackers @tweak Now prints product and version information in the Window title. + + DEBUG_ASSERTCRASH(TheVersion != NULL, ("TheVersion is NULL")); + DEBUG_ASSERTCRASH(TheGameText != NULL, ("TheGameText is NULL")); + + UnicodeString title; + + if (rts::ClientInstance::getInstanceId() > 1u) + { + UnicodeString str; + str.format(L"Instance:%.2u", rts::ClientInstance::getInstanceId()); + title.concat(str); + } + + UnicodeString productString = TheVersion->getUnicodeProductString(); + + if (!productString.isEmpty()) + { + if (!title.isEmpty()) + title.concat(L" "); + title.concat(productString); + } + +#if RTS_GENERALS + const WideChar* defaultGameTitle = L"Command and Conquer Generals"; +#elif RTS_ZEROHOUR + const WideChar* defaultGameTitle = L"Command and Conquer Generals Zero Hour"; +#endif + UnicodeString gameTitle = TheGameText->FETCH_OR_SUBSTITUTE("GUI:Command&ConquerGenerals", defaultGameTitle); + + if (!gameTitle.isEmpty()) + { + UnicodeString gameTitleFinal; + UnicodeString gameVersion = TheVersion->getUnicodeVersion(); + + if (productString.isEmpty()) + { + gameTitleFinal = gameTitle; + } + else + { + UnicodeString gameTitleFormat = TheGameText->FETCH_OR_SUBSTITUTE("Version:GameTitle", L"for %ls"); + gameTitleFinal.format(gameTitleFormat.str(), gameTitle.str()); + } + + if (!title.isEmpty()) + title.concat(L" "); + title.concat(gameTitleFinal.str()); + title.concat(L" "); + title.concat(gameVersion.str()); + } + + if (!title.isEmpty()) + { + AsciiString titleA; + titleA.translate(title); //get ASCII version for Win 9x + + extern HWND ApplicationHWnd; ///< our application window handle + if (ApplicationHWnd) { + //Set it twice because Win 9x does not support SetWindowTextW. + ::SetWindowText(ApplicationHWnd, titleA.str()); + ::SetWindowTextW(ApplicationHWnd, title.str()); + } + } +} + +//------------------------------------------------------------------------------------------------- Int GameEngine::getFramesPerSecondLimit( void ) { return m_maxFPS; @@ -337,6 +408,7 @@ void GameEngine::init() initSubsystem(TheDeepCRCSanityCheck, "TheDeepCRCSanityCheck", MSGNEW("GameEngineSubystem") DeepCRCSanityCheck, NULL, NULL, NULL, NULL); #endif // DEBUG_CRC initSubsystem(TheGameText, "TheGameText", CreateGameTextInterface(), NULL); + updateWindowTitle(); initSubsystem(TheScienceStore,"TheScienceStore", MSGNEW("GameEngineSubsystem") ScienceStore(), &xferCRC, "Data\\INI\\Default\\Science.ini", "Data\\INI\\Science.ini"); initSubsystem(TheMultiplayerSettings,"TheMultiplayerSettings", MSGNEW("GameEngineSubsystem") MultiplayerSettings(), &xferCRC, "Data\\INI\\Default\\Multiplayer.ini", "Data\\INI\\Multiplayer.ini"); initSubsystem(TheTerrainTypes,"TheTerrainTypes", MSGNEW("GameEngineSubsystem") TerrainTypeCollection(), &xferCRC, "Data\\INI\\Default\\Terrain.ini", "Data\\INI\\Terrain.ini"); diff --git a/Generals/Code/GameEngine/Source/GameClient/GameText.cpp b/Generals/Code/GameEngine/Source/GameClient/GameText.cpp index d3e91019cc9..1749e3a8435 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GameText.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GameText.cpp @@ -48,7 +48,6 @@ #include "GameClient/GameText.h" #include "Common/Language.h" #include "Common/Registry.h" -#include "GameClient/ClientInstance.h" #include "GameClient/LanguageFilter.h" #include "Common/Debug.h" #include "Common/UnicodeString.h" @@ -198,8 +197,6 @@ class GameTextManager : public GameTextInterface Bool parseMapStringFile( const char *filename ); Bool readLine( char *buffer, Int max, File *file ); Char readChar( File *file ); - - void updateWindowTitle(); }; static int _cdecl compareLUT ( const void *, const void*); @@ -373,8 +370,6 @@ void GameTextManager::init( void ) qsort( m_stringLUT, m_textCount, sizeof(StringLookUp), compareLUT ); - updateWindowTitle(); - } //============================================================================ @@ -1468,77 +1463,6 @@ Char GameTextManager::readChar( File *file ) return 0; } -//============================================================================ -// GameTextManager::updateWindowTitle -//============================================================================ - -void GameTextManager::updateWindowTitle() -{ - // TheSuperHackers @tweak Now prints product and version information in the Window title. - - DEBUG_ASSERTCRASH(TheVersion != NULL, ("TheVersion is NULL")); - - UnicodeString title; - - if (rts::ClientInstance::getInstanceId() > 1u) - { - UnicodeString str; - str.format(L"Instance:%.2u", rts::ClientInstance::getInstanceId()); - title.concat(str); - } - - UnicodeString productString = TheVersion->getUnicodeProductString(); - - if (!productString.isEmpty()) - { - if (!title.isEmpty()) - title.concat(L" "); - title.concat(productString); - } - -#if RTS_GENERALS - const WideChar* defaultGameTitle = L"Command and Conquer Generals"; -#elif RTS_ZEROHOUR - const WideChar* defaultGameTitle = L"Command and Conquer Generals Zero Hour"; -#endif - UnicodeString gameTitle = FETCH_OR_SUBSTITUTE("GUI:Command&ConquerGenerals", defaultGameTitle); - - if (!gameTitle.isEmpty()) - { - UnicodeString gameTitleFinal; - UnicodeString gameVersion = TheVersion->getUnicodeVersion(); - - if (productString.isEmpty()) - { - gameTitleFinal = gameTitle; - } - else - { - UnicodeString gameTitleFormat = FETCH_OR_SUBSTITUTE("Version:GameTitle", L"for %ls"); - gameTitleFinal.format(gameTitleFormat.str(), gameTitle.str()); - } - - if (!title.isEmpty()) - title.concat(L" "); - title.concat(gameTitleFinal.str()); - title.concat(L" "); - title.concat(gameVersion.str()); - } - - if (!title.isEmpty()) - { - AsciiString titleA; - titleA.translate(title); //get ASCII version for Win 9x - - extern HWND ApplicationHWnd; ///< our application window handle - if (ApplicationHWnd) { - //Set it twice because Win 9x does not support SetWindowTextW. - ::SetWindowText(ApplicationHWnd, titleA.str()); - ::SetWindowTextW(ApplicationHWnd, title.str()); - } - } -} - //============================================================================ // compareLUT //============================================================================ diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 317d24e852e..ea3140a0cdf 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -85,6 +85,7 @@ #include "GameLogic/ScriptEngine.h" #include "GameLogic/SidesList.h" +#include "GameClient/ClientInstance.h" #include "GameClient/Display.h" #include "GameClient/FXList.h" #include "GameClient/GameClient.h" @@ -169,6 +170,76 @@ extern CComModule _Module; //------------------------------------------------------------------------------------------------- static void updateTGAtoDDS(); +//------------------------------------------------------------------------------------------------- +static void updateWindowTitle() +{ + // TheSuperHackers @tweak Now prints product and version information in the Window title. + + DEBUG_ASSERTCRASH(TheVersion != NULL, ("TheVersion is NULL")); + DEBUG_ASSERTCRASH(TheGameText != NULL, ("TheGameText is NULL")); + + UnicodeString title; + + if (rts::ClientInstance::getInstanceId() > 1u) + { + UnicodeString str; + str.format(L"Instance:%.2u", rts::ClientInstance::getInstanceId()); + title.concat(str); + } + + UnicodeString productString = TheVersion->getUnicodeProductString(); + + if (!productString.isEmpty()) + { + if (!title.isEmpty()) + title.concat(L" "); + title.concat(productString); + } + +#if RTS_GENERALS + const WideChar* defaultGameTitle = L"Command and Conquer Generals"; +#elif RTS_ZEROHOUR + const WideChar* defaultGameTitle = L"Command and Conquer Generals Zero Hour"; +#endif + UnicodeString gameTitle = TheGameText->FETCH_OR_SUBSTITUTE("GUI:Command&ConquerGenerals", defaultGameTitle); + + if (!gameTitle.isEmpty()) + { + UnicodeString gameTitleFinal; + UnicodeString gameVersion = TheVersion->getUnicodeVersion(); + + if (productString.isEmpty()) + { + gameTitleFinal = gameTitle; + } + else + { + UnicodeString gameTitleFormat = TheGameText->FETCH_OR_SUBSTITUTE("Version:GameTitle", L"for %ls"); + gameTitleFinal.format(gameTitleFormat.str(), gameTitle.str()); + } + + if (!title.isEmpty()) + title.concat(L" "); + title.concat(gameTitleFinal.str()); + title.concat(L" "); + title.concat(gameVersion.str()); + } + + if (!title.isEmpty()) + { + AsciiString titleA; + titleA.translate(title); //get ASCII version for Win 9x + + extern HWND ApplicationHWnd; ///< our application window handle + if (ApplicationHWnd) { + //Set it twice because Win 9x does not support SetWindowTextW. + ::SetWindowText(ApplicationHWnd, titleA.str()); + ::SetWindowTextW(ApplicationHWnd, title.str()); + } + } +} + +//------------------------------------------------------------------------------------------------- Int GameEngine::getFramesPerSecondLimit( void ) { return m_maxFPS; @@ -414,6 +485,7 @@ void GameEngine::init() initSubsystem(TheDeepCRCSanityCheck, "TheDeepCRCSanityCheck", MSGNEW("GameEngineSubystem") DeepCRCSanityCheck, NULL, NULL, NULL, NULL); #endif // DEBUG_CRC initSubsystem(TheGameText, "TheGameText", CreateGameTextInterface(), NULL); + updateWindowTitle(); #ifdef DUMP_PERF_STATS/////////////////////////////////////////////////////////////////////////// GetPrecisionTimer(&endTime64);////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp index c8edd55664e..01dcb91a2c0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameText.cpp @@ -48,7 +48,6 @@ #include "GameClient/GameText.h" #include "Common/Language.h" #include "Common/Registry.h" -#include "GameClient/ClientInstance.h" #include "GameClient/LanguageFilter.h" #include "Common/Debug.h" #include "Common/UnicodeString.h" @@ -198,8 +197,6 @@ class GameTextManager : public GameTextInterface Bool parseMapStringFile( const char *filename ); Bool readLine( char *buffer, Int max, File *file ); Char readChar( File *file ); - - void updateWindowTitle(); }; static int _cdecl compareLUT ( const void *, const void*); @@ -373,8 +370,6 @@ void GameTextManager::init( void ) qsort( m_stringLUT, m_textCount, sizeof(StringLookUp), compareLUT ); - updateWindowTitle(); - } //============================================================================ @@ -1468,77 +1463,6 @@ Char GameTextManager::readChar( File *file ) return 0; } -//============================================================================ -// GameTextManager::updateWindowTitle -//============================================================================ - -void GameTextManager::updateWindowTitle() -{ - // TheSuperHackers @tweak Now prints product and version information in the Window title. - - DEBUG_ASSERTCRASH(TheVersion != NULL, ("TheVersion is NULL")); - - UnicodeString title; - - if (rts::ClientInstance::getInstanceId() > 1u) - { - UnicodeString str; - str.format(L"Instance:%.2u", rts::ClientInstance::getInstanceId()); - title.concat(str); - } - - UnicodeString productString = TheVersion->getUnicodeProductString(); - - if (!productString.isEmpty()) - { - if (!title.isEmpty()) - title.concat(L" "); - title.concat(productString); - } - -#if RTS_GENERALS - const WideChar* defaultGameTitle = L"Command and Conquer Generals"; -#elif RTS_ZEROHOUR - const WideChar* defaultGameTitle = L"Command and Conquer Generals Zero Hour"; -#endif - UnicodeString gameTitle = FETCH_OR_SUBSTITUTE("GUI:Command&ConquerGenerals", defaultGameTitle); - - if (!gameTitle.isEmpty()) - { - UnicodeString gameTitleFinal; - UnicodeString gameVersion = TheVersion->getUnicodeVersion(); - - if (productString.isEmpty()) - { - gameTitleFinal = gameTitle; - } - else - { - UnicodeString gameTitleFormat = FETCH_OR_SUBSTITUTE("Version:GameTitle", L"for %ls"); - gameTitleFinal.format(gameTitleFormat.str(), gameTitle.str()); - } - - if (!title.isEmpty()) - title.concat(L" "); - title.concat(gameTitleFinal.str()); - title.concat(L" "); - title.concat(gameVersion.str()); - } - - if (!title.isEmpty()) - { - AsciiString titleA; - titleA.translate(title); //get ASCII version for Win 9x - - extern HWND ApplicationHWnd; ///< our application window handle - if (ApplicationHWnd) { - //Set it twice because Win 9x does not support SetWindowTextW. - ::SetWindowText(ApplicationHWnd, titleA.str()); - ::SetWindowTextW(ApplicationHWnd, title.str()); - } - } -} - //============================================================================ // compareLUT //============================================================================ From 31f3a5c6040896100c445acd7373f6235d4123cd Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:40:30 +0200 Subject: [PATCH 05/83] fix(debug): Fix Release debug logging in tools (#1397) --- Core/Tools/ImagePacker/Source/WinMain.cpp | 5 +---- Core/Tools/MapCacheBuilder/Source/WinMain.cpp | 5 +---- Generals/Code/Tools/GUIEdit/Source/WinMain.cpp | 5 +---- Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp | 7 +++---- GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp | 5 +---- GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp | 6 ++---- 6 files changed, 9 insertions(+), 24 deletions(-) diff --git a/Core/Tools/ImagePacker/Source/WinMain.cpp b/Core/Tools/ImagePacker/Source/WinMain.cpp index 755978bd5ac..205d8768ea8 100644 --- a/Core/Tools/ImagePacker/Source/WinMain.cpp +++ b/Core/Tools/ImagePacker/Source/WinMain.cpp @@ -84,8 +84,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, Int nCmdShow ) { - // start the log - DEBUG_INIT(DEBUG_FLAGS_DEFAULT); + // initialize the memory manager early initMemoryManager(); // save application instance @@ -114,9 +113,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, delete TheImagePacker; TheImagePacker = NULL; - // close the log shutdownMemoryManager(); - DEBUG_SHUTDOWN(); // all done return 0; diff --git a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp index b1f4c488fa5..b85825f4d7d 100644 --- a/Core/Tools/MapCacheBuilder/Source/WinMain.cpp +++ b/Core/Tools/MapCacheBuilder/Source/WinMain.cpp @@ -212,8 +212,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, Int nCmdShow ) { - // start the log - DEBUG_INIT(DEBUG_FLAGS_DEFAULT); + // initialize the memory manager early initMemoryManager(); try @@ -325,9 +324,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, DEBUG_CRASH(("Munkee munkee!")); } - // close the log shutdownMemoryManager(); - DEBUG_SHUTDOWN(); // all done return 0; diff --git a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp index c8fae1cf18e..5b251bb58cc 100644 --- a/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp +++ b/Generals/Code/Tools/GUIEdit/Source/WinMain.cpp @@ -199,8 +199,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, } ::SetCurrentDirectory(buffer); - // start the log - DEBUG_INIT(DEBUG_FLAGS_DEFAULT); + // initialize the memory manager early initMemoryManager(); // register a class for our window with the OS @@ -275,9 +274,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, delete TheEditor; TheEditor = NULL; - // close the log shutdownMemoryManager(); - DEBUG_SHUTDOWN(); return msg.wParam; diff --git a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 8c853cea4b1..83086d52f66 100644 --- a/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/Generals/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -274,13 +274,13 @@ BOOL CWorldBuilderApp::InitInstance() // initialization _set_se_translator( DumpExceptionInfo ); // Hook that allows stack trace. - // start the log - DEBUG_INIT(DEBUG_FLAGS_DEFAULT); + // initialize the memory manager early + initMemoryManager(); + DEBUG_LOG(("starting Worldbuilder.")); #ifdef RTS_DEBUG DEBUG_LOG(("RTS_DEBUG defined.")); #endif - initMemoryManager(); #ifdef MEMORYPOOL_CHECKPOINTING gFirstCP = TheMemoryPoolFactory->debugSetCheckpoint(); #endif @@ -644,7 +644,6 @@ int CWorldBuilderApp::ExitInstance() TheMemoryPoolFactory->debugMemoryReport(REPORT_FACTORYINFO | REPORT_CP_LEAKS | REPORT_CP_STACKTRACE, gFirstCP, lastCP); #endif shutdownMemoryManager(); - DEBUG_SHUTDOWN(); return CWinApp::ExitInstance(); } diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp index cbe8569b0c2..8ecdc3de45e 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp +++ b/GeneralsMD/Code/Tools/GUIEdit/Source/WinMain.cpp @@ -199,8 +199,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, } ::SetCurrentDirectory(buffer); - // start the log - DEBUG_INIT(DEBUG_FLAGS_DEFAULT); + // initialize the memory manager early initMemoryManager(); // register a class for our window with the OS @@ -275,9 +274,7 @@ Int APIENTRY WinMain(HINSTANCE hInstance, delete TheEditor; TheEditor = NULL; - // close the log shutdownMemoryManager(); - DEBUG_SHUTDOWN(); return msg.wParam; diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp index 95c3f677fcd..1f44f7fe15f 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/WorldBuilder.cpp @@ -280,8 +280,8 @@ BOOL CWorldBuilderApp::InitInstance() // initialization _set_se_translator( DumpExceptionInfo ); // Hook that allows stack trace. - // start the log - DEBUG_INIT(DEBUG_FLAGS_DEFAULT); + // initialize the memory manager early + initMemoryManager(); #ifdef DEBUG_LOGGING // Turn on console output jba [3/20/2003] @@ -293,7 +293,6 @@ BOOL CWorldBuilderApp::InitInstance() #ifdef RTS_DEBUG DEBUG_LOG(("RTS_DEBUG defined.")); #endif - initMemoryManager(); #ifdef MEMORYPOOL_CHECKPOINTING gFirstCP = TheMemoryPoolFactory->debugSetCheckpoint(); #endif @@ -667,7 +666,6 @@ int CWorldBuilderApp::ExitInstance() TheMemoryPoolFactory->debugMemoryReport(REPORT_POOLINFO | REPORT_POOL_OVERFLOW | REPORT_SIMPLE_LEAKS, 0, 0); #endif shutdownMemoryManager(); - DEBUG_SHUTDOWN(); return CWinApp::ExitInstance(); } From d04210442f0658c78e1d8b8ed7ad80df2bc6179d Mon Sep 17 00:00:00 2001 From: OmniBlade Date: Sat, 2 Aug 2025 11:45:57 +0100 Subject: [PATCH 06/83] fix(cpudetect): Fix version and memory detection (#1393) Memory detection was incorrect on systems with > 4GB. Version info detection was incorrect on Windows 8 and later and if manifested for an earlier version than currently running on. --- .../Source/WWVegas/WWLib/cpudetect.cpp | 60 ++++++++++++++++++- .../Source/WWVegas/WWLib/cpudetect.h | 21 ++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/Core/Libraries/Source/WWVegas/WWLib/cpudetect.cpp b/Core/Libraries/Source/WWVegas/WWLib/cpudetect.cpp index e3e98753341..c3ef582e389 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/cpudetect.cpp +++ b/Core/Libraries/Source/WWVegas/WWLib/cpudetect.cpp @@ -78,12 +78,21 @@ unsigned CPUDetectClass::L1InstructionCacheSetAssociative; unsigned CPUDetectClass::L1InstructionTraceCacheSize; unsigned CPUDetectClass::L1InstructionTraceCacheSetAssociative; +#if defined(_MSC_VER) && _MSC_VER < 1300 unsigned CPUDetectClass::TotalPhysicalMemory; unsigned CPUDetectClass::AvailablePhysicalMemory; unsigned CPUDetectClass::TotalPageMemory; unsigned CPUDetectClass::AvailablePageMemory; unsigned CPUDetectClass::TotalVirtualMemory; unsigned CPUDetectClass::AvailableVirtualMemory; +#else +unsigned long long CPUDetectClass::TotalPhysicalMemory; +unsigned long long CPUDetectClass::AvailablePhysicalMemory; +unsigned long long CPUDetectClass::TotalPageMemory; +unsigned long long CPUDetectClass::AvailablePageMemory; +unsigned long long CPUDetectClass::TotalVirtualMemory; +unsigned long long CPUDetectClass::AvailableVirtualMemory; +#endif unsigned CPUDetectClass::OSVersionNumberMajor; unsigned CPUDetectClass::OSVersionNumberMinor; @@ -885,6 +894,7 @@ void CPUDetectClass::Init_Memory() { #ifdef WIN32 +#if defined(_MSC_VER) && _MSC_VER < 1300 MEMORYSTATUS mem; GlobalMemoryStatus(&mem); @@ -894,14 +904,32 @@ void CPUDetectClass::Init_Memory() AvailablePageMemory = mem.dwAvailPageFile; TotalVirtualMemory = mem.dwTotalVirtual; AvailableVirtualMemory = mem.dwAvailVirtual; -#elif defined(_UNIX) +#else + MEMORYSTATUSEX mem; + mem.dwLength = sizeof(mem); + GlobalMemoryStatusEx(&mem); + + TotalPhysicalMemory = mem.ullTotalPhys; + AvailablePhysicalMemory = mem.ullAvailPhys; + TotalPageMemory = mem.ullTotalPageFile; + AvailablePageMemory = mem.ullAvailPageFile; + TotalVirtualMemory = mem.ullTotalVirtual; + AvailableVirtualMemory = mem.ullAvailVirtual; +#endif // defined(_MSC_VER) && _MSC_VER < 1300 + +#else #warning FIX Init_Memory() -#endif +#endif // WIN32 } void CPUDetectClass::Init_OS() { #ifdef WIN32 + +// TheSuperHackers @fix OmniBlade 30/07/2025 +// GetVersionEx only returns the version of Windows it was manifested for since Windows 8. +// RtlGetVersion returns the correct information at least at the time of writing. +#if defined(_MSC_VER) && _MSC_VER < 1300 OSVERSIONINFO os; os.dwOSVersionInfoSize = sizeof(os); GetVersionEx(&os); @@ -911,7 +939,33 @@ void CPUDetectClass::Init_OS() OSVersionBuildNumber = os.dwBuildNumber; OSVersionPlatformId = os.dwPlatformId; OSVersionExtraInfo = os.szCSDVersion; -#elif defined(_UNIX) +#else + typedef LONG(WINAPI * RtlGetVersionPtr)(PRTL_OSVERSIONINFOW); + HMODULE ntdll = LoadLibraryExA("ntdll", NULL, 0); + if (ntdll != nullptr) { + RtlGetVersionPtr RtlGetVersion = (RtlGetVersionPtr)::GetProcAddress(ntdll, "RtlGetVersion"); + + if (RtlGetVersion != nullptr) { + RTL_OSVERSIONINFOW os = {0}; + os.dwOSVersionInfoSize = sizeof(os); + RtlGetVersion(&os); + OSVersionNumberMajor = os.dwMajorVersion; + OSVersionNumberMinor = os.dwMinorVersion; + OSVersionBuildNumber = os.dwBuildNumber; + OSVersionPlatformId = os.dwPlatformId; + OSVersionExtraInfo = os.szCSDVersion; + return; + } + } + + // GetVersionEx will return this info if no manifest is present so seems a safe fallback. + OSVersionNumberMajor = 6; + OSVersionNumberMinor = 2; + OSVersionBuildNumber = 0; + OSVersionPlatformId = 2; + OSVersionExtraInfo = ""; +#endif // defined(_MSC_VER) && _MSC_VER < 1300 +#else #warning FIX Init_OS() #endif } diff --git a/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h b/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h index b1a7d87e497..9d88979c0fb 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h +++ b/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h @@ -194,12 +194,22 @@ class CPUDetectClass static unsigned Get_L1_Instruction_Trace_Cache_Set_Associative() { return L1InstructionTraceCacheSetAssociative; } // System memory + +#if defined(_MSC_VER) && _MSC_VER < 1300 static unsigned Get_Total_Physical_Memory() { return TotalPhysicalMemory; } static unsigned Get_Available_Physical_Memory() { return AvailablePhysicalMemory; } static unsigned Get_Total_Page_File_Size() { return TotalPageMemory; } static unsigned Get_Available_Page_File_Size() { return AvailablePageMemory; } static unsigned Get_Total_Virtual_Memory() { return TotalVirtualMemory; } static unsigned Get_Available_Virtual_Memory() { return AvailableVirtualMemory; } +#else + static unsigned long long Get_Total_Physical_Memory() { return TotalPhysicalMemory; } + static unsigned long long Get_Available_Physical_Memory() { return AvailablePhysicalMemory; } + static unsigned long long Get_Total_Page_File_Size() { return TotalPageMemory; } + static unsigned long long Get_Available_Page_File_Size() { return AvailablePageMemory; } + static unsigned long long Get_Total_Virtual_Memory() { return TotalVirtualMemory; } + static unsigned long long Get_Available_Virtual_Memory() { return AvailableVirtualMemory; } +#endif static unsigned Get_Processor_Type() { return ProcessorType; } @@ -278,14 +288,21 @@ class CPUDetectClass // L1 instruction trace cache information static unsigned L1InstructionTraceCacheSize; static unsigned L1InstructionTraceCacheSetAssociative; - +#if defined(_MSC_VER) && _MSC_VER < 1300 static unsigned TotalPhysicalMemory; static unsigned AvailablePhysicalMemory; static unsigned TotalPageMemory; static unsigned AvailablePageMemory; static unsigned TotalVirtualMemory; static unsigned AvailableVirtualMemory; - +#else + static unsigned long long TotalPhysicalMemory; + static unsigned long long AvailablePhysicalMemory; + static unsigned long long TotalPageMemory; + static unsigned long long AvailablePageMemory; + static unsigned long long TotalVirtualMemory; + static unsigned long long AvailableVirtualMemory; +#endif static unsigned OSVersionNumberMajor; static unsigned OSVersionNumberMinor; static unsigned OSVersionBuildNumber; From 88b545ec69782eac953bce340dfbb14bc8100fb6 Mon Sep 17 00:00:00 2001 From: Stubbjax Date: Sat, 2 Aug 2025 20:49:44 +1000 Subject: [PATCH 07/83] bugfix(controls): Fix and improve next idle worker selection with contained workers (#1338) --- Core/GameEngine/Include/Common/STLUtils.h | 15 ++++++ .../GameEngine/Include/GameClient/InGameUI.h | 11 ++-- .../GameEngine/Include/GameLogic/GameLogic.h | 1 + .../GameEngine/Source/GameClient/InGameUI.cpp | 51 ++++++++++++------- .../GameEngine/Include/GameClient/InGameUI.h | 11 ++-- .../GameEngine/Source/GameClient/InGameUI.cpp | 51 ++++++++++++------- 6 files changed, 98 insertions(+), 42 deletions(-) diff --git a/Core/GameEngine/Include/Common/STLUtils.h b/Core/GameEngine/Include/Common/STLUtils.h index eef1a3a1195..4fae9beef39 100644 --- a/Core/GameEngine/Include/Common/STLUtils.h +++ b/Core/GameEngine/Include/Common/STLUtils.h @@ -20,6 +20,7 @@ #include #include +#include namespace stl { @@ -58,4 +59,18 @@ bool find_and_erase_unordered(Container& container, const typename Container::va return false; } +// Push back value into vector-like container if it does not yet contain that value. +template +bool push_back_unique(Container& container, const typename Container::value_type& value) +{ + typename Container::iterator it = std::find(container.begin(), container.end(), value); + if (it == container.end()) + { + container.push_back(value); + return true; + } + + return false; +} + } // namespace stl diff --git a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h index e73afdd6bf6..4f8e37cb94f 100644 --- a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h @@ -309,7 +309,12 @@ class InGameUI : public SubsystemInterface, public Snapshot { friend class Drawable; // for selection/deselection transactions - + +protected: + + typedef std::list ObjectList; + typedef std::list::iterator ObjectListIt; + public: // *************************************************************************************** enum SelectionRules @@ -544,6 +549,7 @@ friend class Drawable; // for selection/deselection transactions virtual void addIdleWorker( Object *obj ); virtual void removeIdleWorker( Object *obj, Int playerNumber ); virtual void selectNextIdleWorker( void ); + static std::vector getUniqueIdleWorkers(const ObjectList& idleWorkers); virtual void recreateControlBar( void ); virtual void refreshCustomUiResources( void ); @@ -650,9 +656,6 @@ friend class Drawable; // for selection/deselection transactions Color color; ///< what color should we display the military subtitles }; - typedef std::list ObjectList; - typedef std::list::iterator ObjectListIt; - // ---------------------------------------------------------------------------------------------- // Protected Methods ---------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------- diff --git a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h index 1ce79b3df91..dcc6efbd523 100644 --- a/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h +++ b/Generals/Code/GameEngine/Include/GameLogic/GameLogic.h @@ -93,6 +93,7 @@ typedef void (*GameLogicFuncPtr)( Object *obj, void *userData ); typedef std::hash_map, rts::equal_to > ObjectPtrHash; typedef ObjectPtrHash::const_iterator ObjectPtrIter; +typedef std::vector ObjectPtrVector; // ------------------------------------------------------------------------------------------------ /** diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index 69bd81e08b0..77de792aec6 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -5424,42 +5424,41 @@ void InGameUI::selectNextIdleWorker( void ) if(getSelectCount() == 0 || getSelectCount() > 1) { selectThisObject = *m_idleWorkers[index].begin(); + // If our idle worker is contained by anything, we need to select the container instead. + while (selectThisObject->getContainedBy()) + selectThisObject = selectThisObject->getContainedBy(); } else { - Drawable *selectedDrawable = TheInGameUI->getFirstSelectedDrawable(); - - ObjectListIt it = m_idleWorkers[index].begin(); - while(it != m_idleWorkers[index].end()) + Drawable *selectedDrawable = TheInGameUI->getFirstSelectedDrawable(); + // TheSuperHackers @tweak Stubbjax 22/07/2025 Idle worker iteration now correctly identifies and + // iterates contained idle workers. Previous iteration logic would not go past contained workers, + // and was not guaranteed to select top-level containers. + ObjectPtrVector uniqueIdleWorkers = getUniqueIdleWorkers(m_idleWorkers[index]); + + ObjectPtrVector::iterator it = uniqueIdleWorkers.begin(); + while(it != uniqueIdleWorkers.end()) { Object *itObj = *it; if(itObj == selectedDrawable->getObject()) { ++it; - if(it != m_idleWorkers[index].end()) + if(it != uniqueIdleWorkers.end()) selectThisObject = *it; else - selectThisObject = *m_idleWorkers[index].begin(); + selectThisObject = *uniqueIdleWorkers.begin(); break; } ++it; } // if we had something selected that wasn't a worker, we'll get here - if(!selectThisObject) - selectThisObject = *m_idleWorkers[index].begin(); - + if (!selectThisObject) + selectThisObject = uniqueIdleWorkers.front(); } DEBUG_ASSERTCRASH(selectThisObject, ("InGameUI::selectNextIdleWorker Could not select the next IDLE worker")); if(selectThisObject) { - - //If our idle worker is contained by anything, we need to select the container instead. - Object *containedBy = selectThisObject->getContainedBy(); - if( containedBy ) - { - selectThisObject = containedBy; - } - + DEBUG_ASSERTCRASH(selectThisObject->getContainedBy() == NULL, ("InGameUI::selectNextIdleWorker Selected idle object should not be contained")); deselectAllDrawables(); GameMessage *teamMsg = TheMessageStream->appendMessage( GameMessage::MSG_CREATE_SELECTED_GROUP ); @@ -5484,6 +5483,24 @@ void InGameUI::selectNextIdleWorker( void ) } } +// Finds unique selectables to avoid selecting the same or a previous container if multiple idle workers are contained. +ObjectPtrVector InGameUI::getUniqueIdleWorkers(const ObjectList& idleWorkers) +{ + ObjectPtrVector uniqueIdleWorkers; + uniqueIdleWorkers.reserve(idleWorkers.size()); + + for (ObjectList::const_iterator it = idleWorkers.begin(); it != idleWorkers.end(); ++it) + { + Object* itObj = *it; + while (itObj->getContainedBy()) + itObj = itObj->getContainedBy(); + + stl::push_back_unique(uniqueIdleWorkers, itObj); + } + + return uniqueIdleWorkers; +} + Int InGameUI::getIdleWorkerCount( void ) { Int index = ThePlayerList->getLocalPlayer()->getPlayerIndex(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index 2f3be01188f..237cc09cb4c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -323,7 +323,12 @@ class InGameUI : public SubsystemInterface, public Snapshot { friend class Drawable; // for selection/deselection transactions - + +protected: + + typedef std::list ObjectList; + typedef std::list::iterator ObjectListIt; + public: // *************************************************************************************** enum SelectionRules @@ -561,6 +566,7 @@ friend class Drawable; // for selection/deselection transactions virtual void addIdleWorker( Object *obj ); virtual void removeIdleWorker( Object *obj, Int playerNumber ); virtual void selectNextIdleWorker( void ); + static std::vector getUniqueIdleWorkers(const ObjectList& idleWorkers); virtual void recreateControlBar( void ); virtual void refreshCustomUiResources( void ); @@ -670,9 +676,6 @@ friend class Drawable; // for selection/deselection transactions Color color; ///< what color should we display the military subtitles }; - typedef std::list ObjectList; - typedef std::list::iterator ObjectListIt; - // ---------------------------------------------------------------------------------------------- // Protected Methods ---------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 6ba4f777d5f..0801d31b05c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -5596,42 +5596,41 @@ void InGameUI::selectNextIdleWorker( void ) if(getSelectCount() == 0 || getSelectCount() > 1) { selectThisObject = *m_idleWorkers[index].begin(); + // If our idle worker is contained by anything, we need to select the container instead. + while (selectThisObject->getContainedBy()) + selectThisObject = selectThisObject->getContainedBy(); } else { - Drawable *selectedDrawable = TheInGameUI->getFirstSelectedDrawable(); - - ObjectListIt it = m_idleWorkers[index].begin(); - while(it != m_idleWorkers[index].end()) + Drawable *selectedDrawable = TheInGameUI->getFirstSelectedDrawable(); + // TheSuperHackers @tweak Stubbjax 22/07/2025 Idle worker iteration now correctly identifies and + // iterates contained idle workers. Previous iteration logic would not go past contained workers, + // and was not guaranteed to select top-level containers. + ObjectPtrVector uniqueIdleWorkers = getUniqueIdleWorkers(m_idleWorkers[index]); + + ObjectPtrVector::iterator it = uniqueIdleWorkers.begin(); + while(it != uniqueIdleWorkers.end()) { Object *itObj = *it; if(itObj == selectedDrawable->getObject()) { ++it; - if(it != m_idleWorkers[index].end()) + if(it != uniqueIdleWorkers.end()) selectThisObject = *it; else - selectThisObject = *m_idleWorkers[index].begin(); + selectThisObject = *uniqueIdleWorkers.begin(); break; } ++it; } // if we had something selected that wasn't a worker, we'll get here - if(!selectThisObject) - selectThisObject = *m_idleWorkers[index].begin(); - + if (!selectThisObject) + selectThisObject = uniqueIdleWorkers.front(); } DEBUG_ASSERTCRASH(selectThisObject, ("InGameUI::selectNextIdleWorker Could not select the next IDLE worker")); if(selectThisObject) { - - //If our idle worker is contained by anything, we need to select the container instead. - Object *containedBy = selectThisObject->getContainedBy(); - if( containedBy ) - { - selectThisObject = containedBy; - } - + DEBUG_ASSERTCRASH(selectThisObject->getContainedBy() == NULL, ("InGameUI::selectNextIdleWorker Selected idle object should not be contained")); deselectAllDrawables(); GameMessage *teamMsg = TheMessageStream->appendMessage( GameMessage::MSG_CREATE_SELECTED_GROUP ); @@ -5656,6 +5655,24 @@ void InGameUI::selectNextIdleWorker( void ) } } +// Finds unique selectables to avoid selecting the same or a previous container if multiple idle workers are contained. +ObjectPtrVector InGameUI::getUniqueIdleWorkers(const ObjectList& idleWorkers) +{ + ObjectPtrVector uniqueIdleWorkers; + uniqueIdleWorkers.reserve(idleWorkers.size()); + + for (ObjectList::const_iterator it = idleWorkers.begin(); it != idleWorkers.end(); ++it) + { + Object* itObj = *it; + while (itObj->getContainedBy()) + itObj = itObj->getContainedBy(); + + stl::push_back_unique(uniqueIdleWorkers, itObj); + } + + return uniqueIdleWorkers; +} + Int InGameUI::getIdleWorkerCount( void ) { Int index = ThePlayerList->getLocalPlayer()->getPlayerIndex(); From 3b5d29804c13f8402e836e0449c14723f2180fbe Mon Sep 17 00:00:00 2001 From: Caball009 <82909616+Caball009@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:50:46 +0200 Subject: [PATCH 08/83] refactor(specialpower): Clean up ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower (#1402) --- .../Update/ParticleUplinkCannonUpdate.cpp | 11 ++++-- .../Update/ParticleUplinkCannonUpdate.cpp | 37 +++++++++---------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp index 435642cbc0f..b4608b386cf 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp @@ -275,6 +275,8 @@ Bool ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower(const SpecialPow if( !BitIsSet( commandOptions, COMMAND_FIRED_BY_SCRIPT ) ) { + DEBUG_ASSERTCRASH(targetPos, ("Particle Cannon target data must not be NULL")); + //All human players have manual control and must "drive" the beam around! m_startAttackFrame = TheGameLogic->getFrame(); m_laserStatus = LASERSTATUS_NONE; @@ -285,14 +287,17 @@ Bool ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower(const SpecialPow } else { + DEBUG_ASSERTCRASH(targetPos, ("Particle Cannon target data must not be NULL")); + //All computer controlled players have automatic control -- the "S" curve. UnsignedInt now = TheGameLogic->getFrame(); - m_initialTargetPosition.set( targetPos ); - m_startAttackFrame = max( now, (UnsignedInt)1 ); - m_laserStatus = LASERSTATUS_NONE; + m_initialTargetPosition.set( targetPos ); + m_startAttackFrame = max( now, (UnsignedInt)1 ); + m_laserStatus = LASERSTATUS_NONE; setLogicalStatus( STATUS_READY_TO_FIRE ); m_specialPowerModule->setReadyFrame( now ); } + m_startDecayFrame = m_startAttackFrame + data->m_totalFiringFrames; SpecialPowerModuleInterface *spmInterface = getObject()->getSpecialPowerModule( specialPowerTemplate ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp index b5dd8fc6f33..e7747358301 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp @@ -274,10 +274,10 @@ Bool ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower(const SpecialPow return FALSE; } -// getObject()->getControllingPlayer()->getAcademyStats()->recordSpecialPowerUsed( specialPowerTemplate ); - if( !BitIsSet( commandOptions, COMMAND_FIRED_BY_SCRIPT ) ) { + DEBUG_ASSERTCRASH(targetPos, ("Particle Cannon target data must not be NULL")); + //All human players have manual control and must "drive" the beam around! m_startAttackFrame = TheGameLogic->getFrame(); m_laserStatus = LASERSTATUS_NONE; @@ -293,28 +293,21 @@ Bool ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower(const SpecialPow { //Script fired with a specific waypoint path, so set things up to follow the waypoint! UnsignedInt now = TheGameLogic->getFrame(); - + Coord3D pos; - if( way ) - { - pos.set( way->getLocation() ); - } - else if( targetObj ) - { - pos.set( targetObj->getPosition() ); - } - m_startAttackFrame = max( now, (UnsignedInt)1 ); + pos.set( way->getLocation() ); + + m_startAttackFrame = max( now, (UnsignedInt)1 ); #if !RETAIL_COMPATIBLE_CRC m_manualTargetMode = FALSE; #endif m_scriptedWaypointMode = TRUE; - m_laserStatus = LASERSTATUS_NONE; + m_laserStatus = LASERSTATUS_NONE; setLogicalStatus( STATUS_READY_TO_FIRE ); m_specialPowerModule->setReadyFrame( now ); - m_initialTargetPosition.set( &pos ); + m_initialTargetPosition.set( &pos ); m_currentTargetPosition.set( &pos ); - m_nextDestWaypointID = way->getID(); Int linkCount = way->getNumLinks(); Int which = GameLogicRandomValue( 0, linkCount-1 ); Waypoint *next = way->getLink( which ); @@ -323,10 +316,15 @@ Bool ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower(const SpecialPow m_nextDestWaypointID = next->getID(); m_overrideTargetDestination.set( next->getLocation() ); } - + else + { + m_nextDestWaypointID = way->getID(); + } } else { + DEBUG_ASSERTCRASH(targetPos || targetObj, ("Particle Cannon target data must not be NULL")); + //All computer controlled players have automatic control -- the "S" curve. UnsignedInt now = TheGameLogic->getFrame(); @@ -343,12 +341,13 @@ Bool ParticleUplinkCannonUpdate::initiateIntentToDoSpecialPower(const SpecialPow m_manualTargetMode = FALSE; m_scriptedWaypointMode = FALSE; #endif - m_initialTargetPosition.set( &pos ); - m_startAttackFrame = max( now, (UnsignedInt)1 ); - m_laserStatus = LASERSTATUS_NONE; + m_initialTargetPosition.set( &pos ); + m_startAttackFrame = max( now, (UnsignedInt)1 ); + m_laserStatus = LASERSTATUS_NONE; setLogicalStatus( STATUS_READY_TO_FIRE ); m_specialPowerModule->setReadyFrame( now ); } + m_startDecayFrame = m_startAttackFrame + data->m_totalFiringFrames; SpecialPowerModuleInterface *spmInterface = getObject()->getSpecialPowerModule( specialPowerTemplate ); From 4a9bc10cd8c8f4890324f7f9c95642bf2a77f6b8 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 2 Aug 2025 12:51:39 +0200 Subject: [PATCH 09/83] docs(file): Fix incorrect comment in File class flags (#1403) --- Core/GameEngine/Include/Common/file.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Core/GameEngine/Include/Common/file.h b/Core/GameEngine/Include/Common/file.h index 4c4da7cea92..38714a7105d 100644 --- a/Core/GameEngine/Include/Common/file.h +++ b/Core/GameEngine/Include/Common/file.h @@ -55,8 +55,6 @@ #include "Lib/BaseType.h" #include "Common/AsciiString.h" #include "Common/GameMemory.h" -// include FileSystem.h as it will be used alot with File.h -//#include "Common/FileSystem.h" //---------------------------------------------------------------------------- // Forward References @@ -101,7 +99,7 @@ class File : public MemoryPoolObject CREATE = 0x00000008, ///< Create file if it does not exist TRUNCATE = 0x00000010, ///< Delete all data in file when opened - // NOTE: accesses file as text data if neither TEXT and BINARY are set + // NOTE: accesses file as binary data if neither TEXT and BINARY are set TEXT = 0x00000020, ///< Access file as text data BINARY = 0x00000040, ///< Access file as binary data From 1d64febaa9526d52da5c9c51b8f4e80741658cb5 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Sat, 2 Aug 2025 20:17:41 -0400 Subject: [PATCH 10/83] - 60hz fixes for nuke cannon and nuke missile - Latest tweaks from test branch --- .../GameEngine/Include/GameLogic/AIPathfind.h | 1983 +-- .../GameLogic/Module/DeployStyleAIUpdate.h | 4 +- .../GameNetwork/GeneralsOnline/NetworkMesh.h | 2 + .../Include/GameNetwork/NetworkDefs.h | 2 +- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 14 +- .../Source/GameLogic/AI/AIPathfind.cpp | 860 +- .../Update/AIUpdate/DeployStyleAIUpdate.cpp | 21 + .../Object/Update/NeutronMissileUpdate.cpp | 29 + .../Source/GameNetwork/ConnectionManager.cpp | 47 +- .../GameNetwork/GeneralsOnline/NGMPGame.cpp | 7 +- .../GeneralsOnline/NetworkMesh.cpp | 9 + .../GeneralsOnline/NextGenTransport.cpp | 3 + .../GameNetworkingSockets.dll | Bin 1068544 -> 1068032 bytes .../Source/GameNetwork/NetCommandMsg.cpp | 2135 +-- .../Source/GameNetwork/NetPacket.cpp | 11950 ++++++++-------- .../GameEngine/Source/GameNetwork/Network.cpp | 28 +- .../Source/GameNetwork/NetworkUtil.cpp | 2 +- 17 files changed, 8912 insertions(+), 8184 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPathfind.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPathfind.h index 1d5a5d650e2..be8ea6ea04e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPathfind.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/AIPathfind.h @@ -1,985 +1,998 @@ -/* -** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// AIPathfind.h -// AI pathfinding system -// Author: Michael S. Booth, October 2001 - -#pragma once - -#ifndef _PATHFIND_H_ -#define _PATHFIND_H_ - -#include "Common/GameType.h" -#include "Common/GameMemory.h" -#include "Common/Snapshot.h" -//#include "GameLogic/Locomotor.h" // no, do not include this, unless you like long recompiles -#include "GameLogic/LocomotorSet.h" -#include "GameLogic/GameLogic.h" - -class Bridge; -class Object; -class Weapon; -class PathfindZoneManager; - -// How close is close enough when moving. - -#define PATHFIND_CLOSE_ENOUGH 1.0f -#define PATH_MAX_PRIORITY 0x7FFFFFFF - -#define INFANTRY_MOVES_THROUGH_INFANTRY - - - typedef UnsignedShort zoneStorageType; - - -//---------------------------------------------------------------------------------------------------------- - -/** - * PathNodes are used to create a final Path to return from the - * pathfinder. Note that these are not used during the A* search. - */ -class PathNode : public MemoryPoolObject -{ -public: - PathNode(); - - Coord3D *getPosition( void ) { return &m_pos; } ///< return position of this node - const Coord3D *getPosition( void ) const { return &m_pos; } ///< return position of this node - void setPosition( const Coord3D *pos ) { m_pos = *pos; } ///< set the position of this path node - - const Coord3D *computeDirectionVector( void ); ///< compute direction to next node - - PathNode *getNext( void ) { return m_next; } ///< return next node in the path - PathNode *getPrevious( void ) { return m_prev; } ///< return previous node in the path - const PathNode *getNext( void ) const { return m_next; } ///< return next node in the path - const PathNode *getPrevious( void ) const { return m_prev; } ///< return previous node in the path - - PathfindLayerEnum getLayer( void ) const { return m_layer; } ///< return layer of this node. - void setLayer( PathfindLayerEnum layer ) { m_layer = layer; } ///< set the layer of this path node - - void setNextOptimized( PathNode *node ); - - PathNode *getNextOptimized(Coord2D* dir = NULL, Real* dist = NULL) ///< return next node in optimized path - { - if (dir) - *dir = m_nextOptiDirNorm2D; - if (dist) - *dist = m_nextOptiDist2D; - return m_nextOpti; - } - - const PathNode *getNextOptimized(Coord2D* dir = NULL, Real* dist = NULL) const ///< return next node in optimized path - { - if (dir) - *dir = m_nextOptiDirNorm2D; - if (dist) - *dist = m_nextOptiDist2D; - return m_nextOpti; - } - - void setCanOptimize(Bool canOpt) { m_canOptimize = canOpt;} - Bool getCanOptimize( void ) const { return m_canOptimize;} - - /// given a list, prepend this node, return new list - PathNode *prependToList( PathNode *list ); - - /// given a list, append this node, return new list. slow implementation. - /// @todo optimize this - PathNode *appendToList( PathNode *list ); - - /// given a node, append to this node - void append( PathNode *list ); - -public: - mutable Int m_id; // Used in Path::xfer() to save & recreate the path list. - -private: - MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( PathNode, "PathNodePool" ); ///< @todo Set real numbers for mem alloc - - PathNode* m_nextOpti; ///< next node in the optimized path - PathNode* m_next; ///< next node in the path - PathNode* m_prev; ///< previous node in the path - Coord3D m_pos; ///< position of node in space - PathfindLayerEnum m_layer; ///< Layer for this section. - Bool m_canOptimize; ///< True if this cell can be optimized out. - - Real m_nextOptiDist2D; ///< if nextOpti is nonnull, the dist to it. - Coord2D m_nextOptiDirNorm2D; ///< if nextOpti is nonnull, normalized dir vec towards it. - -}; - -// this doesn't actually seem to be a particularly useful win, -// performance-wise, so I didn't enable it. (srj) -#define NO_CPOP_STARTS_FROM_PREV_SEG - -struct ClosestPointOnPathInfo -{ - Real distAlongPath; - Coord3D posOnPath; - PathfindLayerEnum layer; -}; - -/** - * This class encapsulates a "path" returned by the Pathfinder. - */ -class Path : public MemoryPoolObject, public Snapshot -{ -public: - Path(); - - PathNode *getFirstNode( void ) { return m_path; } - PathNode *getLastNode( void ) { return m_pathTail; } - - void updateLastNode( const Coord3D *pos ); - - void prependNode( const Coord3D *pos, PathfindLayerEnum layer ); ///< Create a new node at the head of the path - void appendNode( const Coord3D *pos, PathfindLayerEnum layer ); ///< Create a new node at the end of the path - void setBlockedByAlly(Bool blocked) {m_blockedByAlly = blocked;} - Bool getBlockedByAlly(void) {return m_blockedByAlly;} - void optimize( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, Bool blocked ); ///< Optimize the path to discard redundant nodes - - void optimizeGroundPath( Bool crusher, Int diameter ); ///< Optimize the ground path to discard redundant nodes - - /// Given a location, return nearest location on path, and along-path dist to end as function result - void computePointOnPath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D& pos, ClosestPointOnPathInfo& out); - - /// Given a location, return nearest location on path, and along-path dist to end as function result - void peekCachedPointOnPath( Coord3D& pos ) const {pos = m_cpopOut.posOnPath;} - - /// Given a flight path, compute the distance to goal (0 if we are past it) & return the goal pos. - Real computeFlightDistToGoal( const Coord3D *pos, Coord3D& goalPos ); - - /// Given a location, return closest location on path, and along-path dist to end as function result - void markOptimized(void) {m_isOptimized = true;} - -protected: - // snapshot interface - virtual void crc( Xfer *xfer ); - virtual void xfer( Xfer *xfer ); - virtual void loadPostProcess(); - -protected: - enum {MAX_CPOP=20}; ///< Max times we will return the cached cpop. - MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( Path, "PathPool" ); ///< @todo Set real numbers for mem alloc - - PathNode* m_path; ///< The list of PathNode objects that define the path - PathNode* m_pathTail; - Bool m_isOptimized; ///< True if the path has been optimized - Bool m_blockedByAlly; ///< An ally needs to move off of this path. - // caching info for computePointOnPath. - Bool m_cpopValid; - Int m_cpopCountdown; ///< We only return the same cpop MAX_CPOP times. It is occasionally possible to get stuck. - Coord3D m_cpopIn; - ClosestPointOnPathInfo m_cpopOut; - const PathNode* m_cpopRecentStart; -}; - -//---------------------------------------------------------------------------------------------------------- - -// See GameType.h for -// enum {LAYER_INVALID = 0, LAYER_GROUND = 1, LAYER_TOP=2 }; - -// Fits in 4 bits for now -enum {MAX_WALL_PIECES = 128}; - -class PathfindCellInfo -{ - friend class PathfindCell; -public: - static void allocateCellInfos(void); - static void releaseCellInfos(void); - - static PathfindCellInfo * getACellInfo(PathfindCell *cell, const ICoord2D &pos); - static void releaseACellInfo(PathfindCellInfo *theInfo); - -protected: - static PathfindCellInfo *s_infoArray; - static PathfindCellInfo *s_firstFree; ///< - - - PathfindCellInfo *m_nextOpen, *m_prevOpen; ///< for A* "open" list, shared by closed list - - PathfindCellInfo *m_pathParent; ///< "parent" cell from pathfinder - PathfindCell *m_cell; ///< Cell this info belongs to currently. - - UnsignedShort m_totalCost, m_costSoFar; ///< cost estimates for A* search - - /// have to include cell's coordinates, since cells are often accessed via pointer only - ICoord2D m_pos; - - ObjectID m_goalUnitID; ///< The objectID of the ground unit whose goal this is. - ObjectID m_posUnitID; ///< The objectID of the ground unit that is occupying this cell. - ObjectID m_goalAircraftID; ///< The objectID of the aircraft whose goal this is. - - ObjectID m_obstacleID; ///< the object ID who overlaps this cell - - UnsignedInt m_isFree:1; - UnsignedInt m_blockedByAlly:1;///< True if this cell is blocked by an allied unit. - UnsignedInt m_obstacleIsFence:1;///< True if occupied by a fence. - UnsignedInt m_obstacleIsTransparent:1;///< True if obstacle is transparent (undefined if obstacleid is invalid) - /// @todo Do we need both mark values in this cell? Can't store a single value and compare it? - UnsignedInt m_open:1; ///< place for marking this cell as on the open list - UnsignedInt m_closed:1; ///< place for marking this cell as on the closed list -}; - -/** - * This represents one cell in the pathfinding grid. - * These cells categorize the world into idealized cellular states, - * and are also used for efficient A* pathfinding. - * @todo Optimize memory usage of pathfind grid. - */ -class PathfindCell -{ -public: - - enum CellType - { - CELL_CLEAR = 0x00, ///< clear, unobstructed ground - CELL_WATER = 0x01, ///< water area - CELL_CLIFF = 0x02, ///< steep altitude change - CELL_RUBBLE = 0x03, ///< Cell is occupied by rubble. - CELL_OBSTACLE = 0x04, ///< Occupied by a structure - CELL_BRIDGE_IMPASSABLE = 0x05, ///< Piece of a bridge that is impassable. - CELL_IMPASSABLE = 0x06 ///< Just plain impassable except for aircraft. - }; - - enum CellFlags - { - NO_UNITS = 0x00, ///< No units in this cell. - UNIT_GOAL = 0x01, ///< A unit is heading to this cell. - UNIT_PRESENT_MOVING = 0x02, ///< A unit is moving through this cell. - UNIT_PRESENT_FIXED = 0x03, ///< A unit is stationary in this cell. - UNIT_GOAL_OTHER_MOVING = 0x05 ///< A unit is moving through this cell, and another unit has this as it's goal. - }; - - /// reset the cell - void reset( ); - - PathfindCell(void); - ~PathfindCell(void); - - Bool setTypeAsObstacle( Object *obstacle, Bool isFence, const ICoord2D &pos ); ///< flag this cell as an obstacle, from the given one - Bool removeObstacle( Object *obstacle ); ///< unflag this cell as an obstacle, from the given one - void setType( CellType type ); ///< set the cell type - CellType getType( void ) const { return (CellType)m_type; } ///< get the cell type - CellFlags getFlags( void ) const { return (CellFlags)m_flags; } ///< get the cell type - Bool isAircraftGoal( void) const {return m_aircraftGoal != 0;} - - Bool isObstaclePresent( ObjectID objID ) const; ///< return true if the given object ID is registered as an obstacle in this cell - - Bool isObstacleTransparent( ) const{return m_info?m_info->m_obstacleIsTransparent:false; } ///< return true if the obstacle in the cell is KINDOF_CAN_SEE_THROUGHT_STRUCTURE - - Bool isObstacleFence( void ) const {return m_info?m_info->m_obstacleIsFence:false; }///< return true if the given obstacle in the cell is a fence. - - /// Return estimated cost from given cell to reach goal cell - UnsignedInt costToGoal( PathfindCell *goal ); - - UnsignedInt costToHierGoal( PathfindCell *goal ); - - UnsignedInt costSoFar( PathfindCell *parent ); - - /// put self on "open" list in ascending cost order, return new list - PathfindCell *putOnSortedOpenList( PathfindCell *list ); - - /// remove self from "open" list - PathfindCell *removeFromOpenList( PathfindCell *list ); - - /// put self on "closed" list, return new list - PathfindCell *putOnClosedList( PathfindCell *list ); - - /// remove self from "closed" list - PathfindCell *removeFromClosedList( PathfindCell *list ); - - /// remove all cells from closed list. - static Int releaseClosedList( PathfindCell *list ); - - /// remove all cells from closed list. - static Int releaseOpenList( PathfindCell *list ); - - inline PathfindCell *getNextOpen(void) {return m_info->m_nextOpen?m_info->m_nextOpen->m_cell:NULL;} - - inline UnsignedShort getXIndex(void) const {return m_info->m_pos.x;} - inline UnsignedShort getYIndex(void) const {return m_info->m_pos.y;} - - inline Bool isBlockedByAlly(void) const {return m_info->m_blockedByAlly;} - inline void setBlockedByAlly(Bool blocked) {m_info->m_blockedByAlly = (blocked!=0);} - - inline Bool getOpen(void) const {return m_info->m_open;} - inline Bool getClosed(void) const {return m_info->m_closed;} - inline UnsignedInt getCostSoFar(void) const {return m_info->m_costSoFar;} - inline UnsignedInt getTotalCost(void) const {return m_info->m_totalCost;} - - inline void setCostSoFar(UnsignedInt cost) { if( m_info ) m_info->m_costSoFar = cost;} - inline void setTotalCost(UnsignedInt cost) { if( m_info ) m_info->m_totalCost = cost;} - - void setParentCell(PathfindCell* parent); - void clearParentCell(void); - void setParentCellHierarchical(PathfindCell* parent); - inline PathfindCell* getParentCell(void) const {return m_info ? m_info->m_pathParent ? m_info->m_pathParent->m_cell : NULL : NULL;} - - Bool startPathfind( PathfindCell *goalCell ); - Bool getPinched(void) const {return m_pinched;} - void setPinched(Bool pinch) {m_pinched = pinch; } - - Bool allocateInfo(const ICoord2D &pos); - void releaseInfo(void); - Bool hasInfo(void) const {return m_info!=NULL;} - zoneStorageType getZone(void) const {return m_zone;} - void setZone(zoneStorageType zone) {m_zone = zone;} - void setGoalUnit(ObjectID unit, const ICoord2D &pos ); - void setGoalAircraft(ObjectID unit, const ICoord2D &pos ); - void setPosUnit(ObjectID unit, const ICoord2D &pos ); - inline ObjectID getGoalUnit(void) const {ObjectID id = m_info?m_info->m_goalUnitID:INVALID_ID; return id;} - inline ObjectID getGoalAircraft(void) const {ObjectID id = m_info?m_info->m_goalAircraftID:INVALID_ID; return id;} - inline ObjectID getPosUnit(void) const {ObjectID id = m_info?m_info->m_posUnitID:INVALID_ID; return id;} - - inline ObjectID getObstacleID(void) const {ObjectID id = m_info?m_info->m_obstacleID:INVALID_ID; return id;} - - void setLayer( PathfindLayerEnum layer ) { m_layer = layer; } ///< set the cell layer - PathfindLayerEnum getLayer( void ) const { return (PathfindLayerEnum)m_layer; } ///< get the cell layer - - void setConnectLayer( PathfindLayerEnum layer ) { m_connectsToLayer = layer; } ///< set the cell layer connect id - PathfindLayerEnum getConnectLayer( void ) const { return (PathfindLayerEnum)m_connectsToLayer; } ///< get the cell layer connect id - -private: - PathfindCellInfo *m_info; - zoneStorageType m_zone:14; ///< Zone. Each zone is a set of adjacent terrain type. If from & to in the same zone, you can successfully pathfind. If not, - // you still may be able to if you can cross multiple terrain types. - UnsignedShort m_aircraftGoal:1; //< This is an aircraft goal cell. - UnsignedShort m_pinched:1; //< This cell is surrounded by obstacle cells. - UnsignedByte m_type:4; ///< what type of cell terrain this is. - UnsignedByte m_flags:4; ///< what type of units are in or moving through this cell. - UnsignedByte m_connectsToLayer:4; ///< This cell can pathfind onto this layer, if > LAYER_TOP. - UnsignedByte m_layer:4; ///< Layer of this cell. -}; - -typedef PathfindCell *PathfindCellP; - - -// how close a unit has to be in z to interact with the layer. -#define LAYER_Z_CLOSE_ENOUGH_F 10.0f -/** - * This class represents a bridge in the map. This is effectively - * a sub-rectangle of the big pathfind map. - */ -class PathfindLayer -{ -public: - PathfindLayer(); - ~PathfindLayer(); -public: - void reset(void); - Bool init(Bridge *theBridge, PathfindLayerEnum layer); - void allocateCells(const IRegion2D *extent); - void allocateCellsForWallLayer(const IRegion2D *extent, ObjectID *wallPieces, Int numPieces); - void classifyCells(); - void classifyWallCells(ObjectID *wallPieces, Int numPieces); - Bool setDestroyed(Bool destroyed); - Bool isUnused(void); // True if it doesn't contain a bridge. - Bool isDestroyed(void) {return m_destroyed;} // True if it has been destroyed. - PathfindCell *getCell(Int x, Int y); - Int getZone(void) {return m_zone;} - void setZone(Int zone) {m_zone = zone;} - void applyZone(void); // Propagates m_zone to all cells. - void getStartCellIndex(ICoord2D *start) {*start = m_startCell;} - void getEndCellIndex(ICoord2D *end) {*end = m_endCell;} - - ObjectID getBridgeID(void); - Bool connectsZones(PathfindZoneManager *zm, const LocomotorSet& locomotorSet,Int zone1, Int zone2); - Bool isPointOnWall(ObjectID *wallPieces, Int numPieces, const Coord3D *pt); - -#if defined(RTS_DEBUG) - void doDebugIcons(void) ; -#endif -protected: - void classifyLayerMapCell( Int i, Int j , PathfindCell *cell, Bridge *theBridge); - void classifyWallMapCell( Int i, Int j, PathfindCell *cell , ObjectID *wallPieces, Int numPieces); - -private: - PathfindCell *m_blockOfMapCells; ///< Pathfinding map - contains iconic representation of the map - PathfindCell **m_layerCells; ///< Pathfinding map indexes - contains matrix indexing into the map. - Int m_width; // Number of cells in x - Int m_height; // Number of cells in y - Int m_xOrigin; // Index of first cell in x - Int m_yOrigin; // Index of first cell in y - ICoord2D m_startCell; // pathfind cell indexes for center cell on the from side. - ICoord2D m_endCell; // pathfind cell indexes for center cell on the to side. - - PathfindLayerEnum m_layer; - Int m_zone; // Whole bridge is in same zone. - Bridge *m_bridge; // Corresponding bridge in TerrainLogic. - Bool m_destroyed; - - -}; - - -#define PATHFIND_CELL_SIZE 10 -#define PATHFIND_CELL_SIZE_F 10.0f - -enum { PATHFIND_QUEUE_LEN=512}; - -struct TCheckMovementInfo; - -/** - * This class is a helper class for zone manager. It maintains information regarding the - * LocomotorSurfaceTypeMask equivalencies within a ZONE_BLOCK_SIZE x ZONE_BLOCK_SIZE area of - * cells. This is used in hierarchical pathfinding to find the best coarse path at the - * block level. - */ -class ZoneBlock -{ -public: - - ZoneBlock(); - ~ZoneBlock(); // not virtual, please don't override without making virtual. jba. - - void blockCalculateZones( PathfindCell **map, PathfindLayer layers[], const IRegion2D &bounds); ///< Does zone calculations. - zoneStorageType getEffectiveZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher, zoneStorageType zone) const; - - void clearMarkedPassable(void) {m_markedPassable = false;} - Bool isPassable(void) {return m_markedPassable;} - void setPassable(Bool pass) {m_markedPassable = pass;} - - Bool getInteractsWithBridge(void) const {return m_interactsWithBridge;} - void setInteractsWithBridge(Bool interacts) {m_interactsWithBridge = interacts;} - -protected: - void allocateZones(void); - void freeZones(void); - -protected: - ICoord2D m_cellOrigin; - - zoneStorageType m_firstZone; // First zone in this block. - UnsignedShort m_numZones; // Number of zones in this block. If == 1, there is only one zone, and - // no zone equivalency arrays will be allocated. - - - UnsignedShort m_zonesAllocated; - zoneStorageType *m_groundCliffZones; - zoneStorageType *m_groundWaterZones; - zoneStorageType *m_groundRubbleZones; - zoneStorageType *m_crusherZones; - Bool m_interactsWithBridge; - Bool m_markedPassable; -}; -typedef ZoneBlock *ZoneBlockP; - -/** - * This class manages the zones in the map. A zone is an area in the map that - * is one contiguous type of terrain (clear, cliff, water, building). If - * a unit is in a zone, and wants to move to another location, the destination - * zone has to be the same, or it can't get there. - * There are equivalency tables for meta-zones. For example, an amphibious craft can - * travel through water and clear cells. - */ -class PathfindZoneManager -{ -public: - enum {INITIAL_ZONES = 256}; - enum {ZONE_BLOCK_SIZE = 10}; // Zones are calculated in blocks of 20x20. This way, the raw zone numbers can be used to - enum {UNINITIALIZED_ZONE = 0}; - // compute hierarchically between the 20x20 blocks of cells. jba. - PathfindZoneManager(); - ~PathfindZoneManager(); - - void reset(void); - - Bool needToCalculateZones(void) const {return m_nextFrameToCalculateZones <= TheGameLogic->getFrame() ;} ///< Returns true if the zones need to be recalculated. - void markZonesDirty( Bool insert ) ; ///< Called when the zones need to be recalculated. - void updateZonesForModify( PathfindCell **map, PathfindLayer layers[], const IRegion2D &structureBounds, const IRegion2D &globalBounds ) ; ///< Called to recalculate an area when a structure has been removed. - void calculateZones( PathfindCell **map, PathfindLayer layers[], const IRegion2D &bounds); ///< Does zone calculations. - zoneStorageType getEffectiveZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher, zoneStorageType zone) const; - zoneStorageType getEffectiveTerrainZone(zoneStorageType zone) const; - - zoneStorageType getNextZone(void); - - void getExtent(ICoord2D &extent) const {extent = m_zoneBlockExtent;} - - /// return zone relative the the block zone that this cell resides in. - zoneStorageType getBlockZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher, Int cellX, Int cellY, PathfindCell **map) const; - void allocateBlocks(const IRegion2D &globalBounds); - - void clearPassableFlags(void); - Bool isPassable(Int cellX, Int cellY) const; - Bool clipIsPassable(Int cellX, Int cellY) const; - void setPassable(Int cellX, Int cellY, Bool passable); - - void setAllPassable(void); - - void setBridge(Int cellX, Int cellY, Bool bridge); - Bool interactsWithBridge(Int cellX, Int cellY) const; - -private: - void allocateZones(void); - void freeZones(void); - void freeBlocks(void); - -private: - ZoneBlock *m_blockOfZoneBlocks; ///< Zone blocks - Info for hierarchical pathfinding at a "blocky" level. - ZoneBlock **m_zoneBlocks; ///< Zone blocks as a matrix - contains matrix indexing into the map. - ICoord2D m_zoneBlockExtent; ///< Zone block extents. Not the same scale as the pathfind extents. - - UnsignedShort m_maxZone; ///< Max zone used. - UnsignedInt m_nextFrameToCalculateZones; ///< WHen should I recalculate, next?. - UnsignedShort m_zonesAllocated; - zoneStorageType *m_groundCliffZones; - zoneStorageType *m_groundWaterZones; - zoneStorageType *m_groundRubbleZones; - zoneStorageType *m_terrainZones; - zoneStorageType *m_crusherZones; - zoneStorageType *m_hierarchicalZones; -}; - -/** - * The pathfinding services interface provides access to the 3 expensive path find calls: - * findPath, findClosestPath, and findAttackPath. - * It is only available to units when their ai interface doPathfind method is called. - * This allows the pathfinder to spread out the pathfinding over a number of frames - * when a lot of units are trying to pathfind all at the same time. - */ -class PathfindServicesInterface { -public: - virtual Path *findPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, - const Coord3D *to )=0; ///< Find a short, valid path between given locations - /** Find a short, valid path to a location NEAR the to location. - This succeds when the destination is unreachable (like inside a building). - If the destination is unreachable, it will adjust the to point. */ - virtual Path *findClosestPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, - Coord3D *to, Bool blocked, Real pathCostMultiplier, Bool moveAllies )=0; - - /** Find a short, valid path to a location that obj can attack victim from. */ - virtual Path *findAttackPath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, - const Object *victim, const Coord3D* victimPos, const Weapon *weapon )=0; - - /** Patch to the exiting path from the current position, either because we became blocked, - or because we had to move off the path to avoid other units. */ - virtual Path *patchPath( const Object *obj, const LocomotorSet& locomotorSet, - Path *originalPath, Bool blocked ) = 0; - - /** Find a short, valid path to a location that is away from the repulsors. */ - virtual Path *findSafePath( const Object *obj, const LocomotorSet& locomotorSet, - const Coord3D *from, const Coord3D* repulsorPos1, const Coord3D* repulsorPos2, Real repulsorRadius ) = 0; - -}; - -/** - * The Pathfinding engine itself. - */ -class Pathfinder : PathfindServicesInterface, public Snapshot -{ -// The following routines are private, but available through the doPathfind callback to aiInterface. jba. -private: - virtual Path *findPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to); ///< Find a short, valid path between given locations - /** Find a short, valid path to a location NEAR the to location. - This succeds when the destination is unreachable (like inside a building). - If the destination is unreachable, it will adjust the to point. */ - virtual Path *findClosestPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, - Coord3D *to, Bool blocked, Real pathCostMultiplier, Bool moveAllies ); - - /** Find a short, valid path to a location that obj can attack victim from. */ - virtual Path *findAttackPath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, - const Object *victim, const Coord3D* victimPos, const Weapon *weapon ); - - /** Find a short, valid path to a location that is away from the repulsors. */ - virtual Path *findSafePath( const Object *obj, const LocomotorSet& locomotorSet, - const Coord3D *from, const Coord3D* repulsorPos1, const Coord3D* repulsorPos2, Real repulsorRadius ); - - /** Patch to the exiting path from the current position, either because we became blocked, - or because we had to move off the path to avoid other units. */ - virtual Path *patchPath( const Object *obj, const LocomotorSet& locomotorSet, - Path *originalPath, Bool blocked ); - -public: - Pathfinder( void ); - ~Pathfinder() ; - - void reset( void ); ///< Reset system in preparation for new map - - // --------------- inherited from Snapshot interface -------------- - void crc( Xfer *xfer ); - void xfer( Xfer *xfer ); - void loadPostProcess( void ); - - Bool clientSafeQuickDoesPathExist( const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to ); ///< Can we build any path at all between the locations (terrain & buildings check - fast) - Bool clientSafeQuickDoesPathExistForUI( const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to ); ///< Can we build any path at all between the locations (terrain onlyk - fast) - Bool slowDoesPathExist( Object *obj, const Coord3D *from, - const Coord3D *to, ObjectID ignoreObject=INVALID_ID ); ///< Can we build any path at all between the locations (terrain, buildings & units check - slower) - - Bool queueForPath(ObjectID id); ///< The object wants to request a pathfind, so put it on the list to process. - void processPathfindQueue(void); ///< Process some or all of the queued pathfinds. - void forceMapRecalculation( ); ///< Force pathfind map recomputation. If region is given, only that area is recomputed - - /** Returns an aircraft path to the goal. */ - Path *getAircraftPath( const Object *obj, const Coord3D *to); - Path *findGroundPath( const Coord3D *from, const Coord3D *to, Int pathRadius, - Bool crusher); ///< Find a short, valid path of the desired width on the ground. - - void addObjectToPathfindMap( class Object *obj ); ///< Classify the given object's cells in the map - void removeObjectFromPathfindMap( class Object *obj ); ///< De-classify the given object's cells in the map - - void removeUnitFromPathfindMap( Object *obj ); ///< De-classify the given mobile unit's cells in the map - void updateGoal( Object *obj, const Coord3D *newGoalPos, PathfindLayerEnum layer); ///< Update the given mobile unit's cells in the map - void updateAircraftGoal( Object *obj, const Coord3D *newGoalPos); ///< Update the given aircraft unit's cells in the map - void removeGoal( Object *obj); ///< Removes the given mobile unit's goal cells in the map - void updatePos( Object *obj, const Coord3D *newPos); ///< Update the given mobile unit's cells in the map - void removePos( Object *obj); ///< Removes the unit's position cells from the map - - Bool moveAllies(Object *obj, Path *path); - - // NOTE - The object MUST NOT MOVE between the call to createAWall... and removeWall... - // or BAD THINGS will happen. jba. - void createAWallFromMyFootprint( Object *obj ) {internal_classifyObjectFootprint(obj, true);} // Temporarily treat this object as an obstacle. - void removeWallFromMyFootprint( Object *obj ){internal_classifyObjectFootprint(obj, false);} // Undo createAWallFromMyFootprint. - - Path *getMoveAwayFromPath(Object *obj, Object *otherObj, Path *pathToAvoid, Object *otherObj2, Path *pathToAvoid2); - - void changeBridgeState( PathfindLayerEnum layer, Bool repaired ); - - Bool findBrokenBridge(const LocomotorSet &locomotorSet, const Coord3D *from, const Coord3D *to, ObjectID *bridgeID); - - void newMap(void); - - PathfindCell *getCell( PathfindLayerEnum layer, Int x, Int y ); ///< Return the cell at grid coords (x,y) - PathfindCell *getCell( PathfindLayerEnum layer, const Coord3D *pos ); ///< Given a position, return associated grid cell - PathfindCell *getClippedCell( PathfindLayerEnum layer, const Coord3D *pos ); ///< Given a position, return associated grid cell - void clip(Coord3D *from, Coord3D *to); - Bool worldToCell( const Coord3D *pos, ICoord2D *cell ); ///< Given a world position, return grid cell coordinate - - const ICoord2D *getExtent(void) const {return &m_extent.hi;} - - void setIgnoreObstacleID( ObjectID objID ); ///< if non-zero, the pathfinder will ignore the given obstacle - - Bool validMovementPosition( Bool isCrusher, LocomotorSurfaceTypeMask acceptableSurfaces, PathfindCell *toCell, PathfindCell *fromCell = NULL ); ///< Return true if given position is a valid movement location - Bool validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, Int x, Int y ); ///< Return true if given position is a valid movement location - Bool validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, const Coord3D *pos ); ///< Return true if given position is a valid movement location - Bool validMovementTerrain( PathfindLayerEnum layer, const Locomotor* locomotor, const Coord3D *pos ); ///< Return true if given position is a valid movement location - - Locomotor* chooseBestLocomotorForPosition(PathfindLayerEnum layer, LocomotorSet* locomotorSet, const Coord3D* pos ); - - Bool isViewBlockedByObstacle(const Object* obj, const Object* objOther); ///< Return true if the straight line between the given points contains any obstacle, and thus blocks vision - - Bool isAttackViewBlockedByObstacle(const Object* obj, const Coord3D& attackerPos, const Object* victim, const Coord3D& victimPos); ///< Return true if the straight line between the given points contains any obstacle, and thus blocks vision - - Bool isLinePassable( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, - PathfindLayerEnum layer, const Coord3D& startWorld, const Coord3D& endWorld, - Bool blocked, Bool allowPinched ); ///< Return true if the straight line between the given points is passable - - void moveAlliesAwayFromDestination( Object *obj,const Coord3D& destination); - - Bool isGroundPathPassable( Bool isCrusher, const Coord3D& startWorld, PathfindLayerEnum startLayer, - const Coord3D& endWorld, Int pathDiameter); ///< Return true if the straight line between the given points is passable - - // for debugging - const Coord3D *getDebugPathPosition( void ); - void setDebugPathPosition( const Coord3D *pos ); - Path *getDebugPath( void ); - void setDebugPath( Path *debugpath ); - - void cleanOpenAndClosedLists(void); - - // Adjusts the destination to a spot near dest that is not occupied by other units. - Bool adjustDestination(Object *obj, const LocomotorSet& locomotorSet, - Coord3D *dest, const Coord3D *groupDest=NULL); - - // Adjusts the destination to a spot near dest for landing that is not occupied by other units. - Bool adjustToLandingDestination(Object *obj, Coord3D *dest); - - // Adjusts the destination to a spot that can attack target that is not occupied by other units. - Bool adjustTargetDestination(const Object *obj, const Object *target, const Coord3D *targetPos, - const Weapon *weapon, Coord3D *dest); - - // Adjusts destination to a spot near dest that is possible to path to. - Bool adjustToPossibleDestination(Object *obj, const LocomotorSet& locomotorSet, Coord3D *dest); - - void snapPosition(Object *obj, Coord3D *pos); // Snaps the current position to it's grid location. - void snapClosestGoalPosition(Object *obj, Coord3D *pos); // Snaps the current position to a good goal position. - Bool goalPosition(Object *obj, Coord3D *pos); // Returns the goal position on the grid. - - PathfindLayerEnum addBridge(Bridge *theBridge); // Adds a bridge layer, and returns the layer id. - - void addWallPiece(Object *wallPiece); // Adds a wall piece. - void removeWallPiece(Object *wallPiece); // Removes a wall piece. - Real getWallHeight(void) {return m_wallHeight;} - Bool isPointOnWall(const Coord3D *pos); - - void updateLayer(Object *obj, PathfindLayerEnum layer); ///< Updates object's layer. - - static void classifyMapCell( Int x, Int y, PathfindCell *cell); ///< Classify the given map cell - Int clearCellForDiameter( Bool crusher, Int cellX, Int cellY, PathfindLayerEnum layer, Int pathDiameter ); ///< Return true if given position is a valid movement location - -protected: - virtual Path *internalFindPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to); ///< Find a short, valid path between given locations - Path *findHierarchicalPath( Bool isHuman, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to, Bool crusher); - Path *findClosestHierarchicalPath( Bool isHuman, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to, Bool crusher); - Path *internal_findHierarchicalPath( Bool isHuman, const LocomotorSurfaceTypeMask locomotorSurface, const Coord3D *from, const Coord3D *to, Bool crusher, Bool closestOK); - void processHierarchicalCell( const ICoord2D &scanCell, const ICoord2D &deltaPathfindCell, - PathfindCell *parentCell, - PathfindCell *goalCell, zoneStorageType parentZone, - zoneStorageType *examinedZones, Int &numExZones, - Bool crusher, Int &cellCount); - Bool checkForAdjust(Object *, const LocomotorSet& locomotorSet, Bool isHuman, Int cellX, Int cellY, - PathfindLayerEnum layer, Int iRadius, Bool center,Coord3D *dest, const Coord3D *groupDest) ; - Bool checkForLanding(Int cellX, Int cellY, - PathfindLayerEnum layer, Int iRadius, Bool center,Coord3D *dest) ; - Bool checkForTarget(const Object *obj, Int cellX, Int cellY, const Weapon *weapon, - const Object *victim, const Coord3D *victimPos, - Int iRadius, Bool center,Coord3D *dest) ; - Bool checkForPossible(Bool isCrusher, Int fromZone, Bool center, const LocomotorSet& locomotorSet, - Int cellX, Int cellY, PathfindLayerEnum layer, Coord3D *dest, Bool startingInObstacle) ; - void getRadiusAndCenter(const Object *obj, Int &iRadius, Bool ¢er); - void adjustCoordToCell(Int cellX, Int cellY, Bool centerInCell, Coord3D &pos, PathfindLayerEnum layer); - Bool checkDestination(const Object *obj, Int cellX, Int cellY, PathfindLayerEnum layer, Int iRadius, Bool centerInCell); - Bool checkForMovement(const Object *obj, TCheckMovementInfo &info); - Bool segmentIntersectsTallBuilding(const PathNode *curNode, PathNode *nextNode, - ObjectID ignoreBuilding, Coord3D *insertPos1, Coord3D *insertPos2, Coord3D *insertPos3); ///< Return true if the straight line between the given points intersects a tall building. - Bool circleClipsTallBuilding(const Coord3D *from, const Coord3D *to, Real radius, ObjectID ignoreBuilding, Coord3D *adjustTo); ///< Return true if the circle at the end of the line between the given points intersects a tall building. - - enum {NO_ATTACK=0}; - Int examineNeighboringCells(PathfindCell *parentCell, PathfindCell *goalCell, - const LocomotorSet& locomotorSet, Bool isHumanPlayer, - Bool centerInCell, Int radius, const ICoord2D &startCellNdx, - const Object *obj, Int attackDistance); - - Bool pathDestination( Object *obj, const LocomotorSet& locomotorSet, Coord3D *dest, - PathfindLayerEnum layer, const Coord3D *groupDest); ///< Checks cost between given locations - - Int checkPathCost(Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, - const Coord3D *to); - - void tightenPath(Object *obj, const LocomotorSet& locomotorSet, Coord3D *from, - const Coord3D *to); - - /** - return 0 to continue iterating the line, nonzero to terminate the iteration. - the nonzero result will be returned as the result of iterateCellsAlongLine(). - iterateCellsAlongLine will return zero if it completes. - */ - typedef Int (*CellAlongLineProc)(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - Int iterateCellsAlongLine(const Coord3D& startWorld, const Coord3D& endWorld, - PathfindLayerEnum layer, CellAlongLineProc proc, void* userData); - - Int iterateCellsAlongLine(const ICoord2D &start, const ICoord2D &end, - PathfindLayerEnum layer, CellAlongLineProc proc, void* userData); - - static Int linePassableCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int groundPathPassableCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int lineBlockedByObstacleCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int tightenPathCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int attackBlockedByObstacleCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int examineCellsCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int groundCellsCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - static Int moveAlliesDestinationCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - - static Int segmentIntersectsBuildingCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); - - void classifyMap( void ); ///< Classify all cells in grid as obstacles, etc - void classifyObjectFootprint( Object *obj, Bool insert ); /** Classify the cells under the given object - If 'insert' is true, object is being added - If 'insert' is false, object is being removed */ - void internal_classifyObjectFootprint( Object *obj, Bool insert ); /** Classify the cells under the given object - If 'insert' is true, object is being added - If 'insert' is false, object is being removed */ - void classifyFence( Object *obj, Bool insert ); /** Classify the cells under the given fence object. */ - void classifyUnitFootprint( Object *obj, Bool insert, Bool remove, Bool update ); /** Classify the cells under the given object If 'insert' is true, object is being added */ - /// Convert world coordinate to array index - void worldToGrid( const Coord3D *pos, ICoord2D *cellIndex ); - - Bool evaluateCell(PathfindCell* newCell, PathfindCell *parentCell, - const LocomotorSet& locomotorSet, - Bool centerInCell, Int radius, - const Object *obj, Int attackDistance); - - Path *buildActualPath( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, - const Coord3D *fromPos, PathfindCell *goalCell, Bool center, Bool blocked ); ///< Work backwards from goal cell to construct final path - Path *buildGroundPath( Bool isCrusher,const Coord3D *fromPos, PathfindCell *goalCell, - Bool center, Int pathDiameter ); ///< Work backwards from goal cell to construct final path - Path *buildHierachicalPath( const Coord3D *fromPos, PathfindCell *goalCell); ///< Work backwards from goal cell to construct final path - - void prependCells( Path *path, const Coord3D *fromPos, - PathfindCell *goalCell, Bool center ); ///< Add pathfind cells to a path. - - void debugShowSearch( Bool pathFound ); ///< Show all cells touched in the last search - static LocomotorSurfaceTypeMask validLocomotorSurfacesForCellType(PathfindCell::CellType t); - - void checkChangeLayers(PathfindCell *parentCell); - -#if defined(RTS_DEBUG) - void doDebugIcons(void) ; -#endif - -private: - /// This uses WAY too much memory. Should at least be array of pointers to cells w/ many fewer cells - PathfindCell *m_blockOfMapCells; ///< Pathfinding map - contains iconic representation of the map - PathfindCell **m_map; ///< Pathfinding map indexes - contains matrix indexing into the map. - IRegion2D m_extent; ///< Grid extent limits - IRegion2D m_logicalExtent; ///< Logical grid extent limits - - PathfindCell *m_openList; ///< Cells ready to be explored - PathfindCell *m_closedList; ///< Cells already explored - - Bool m_isMapReady; ///< True if all cells of map have been classified - Bool m_isTunneling; ///< True if path started in an obstacle - - Int m_frameToShowObstacles; ///< Time to redraw obstacles. For debug output. - - Coord3D debugPathPos; ///< Used for visual debugging - Path *debugPath; ///< Used for visual debugging - - ObjectID m_ignoreObstacleID; ///< Ignore the given obstacle - - PathfindZoneManager m_zoneManager; ///< Handles the pathfind zones. - - PathfindLayer m_layers[LAYER_LAST+1]; - - ObjectID m_wallPieces[MAX_WALL_PIECES]; - Int m_numWallPieces; - Real m_wallHeight; - - Int m_moveAlliesDepth; - - - // Pathfind queue - ObjectID m_queuedPathfindRequests[PATHFIND_QUEUE_LEN]; - Int m_queuePRHead; - Int m_queuePRTail; - Int m_cumulativeCellsAllocated; -}; - - -inline void Pathfinder::setIgnoreObstacleID( ObjectID objID ) -{ - m_ignoreObstacleID = objID; -} - -inline void Pathfinder::worldToGrid( const Coord3D *pos, ICoord2D *cellIndex ) -{ - cellIndex->x = REAL_TO_INT(pos->x/PATHFIND_CELL_SIZE); - cellIndex->y = REAL_TO_INT(pos->y/PATHFIND_CELL_SIZE); -} - -inline Bool Pathfinder::validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, Int x, Int y ) -{ - return validMovementPosition( isCrusher, locomotorSet.getValidSurfaces(), getCell( layer, x, y ) ); -} - -inline Bool Pathfinder::validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, const Coord3D *pos ) -{ - - Int x = REAL_TO_INT(pos->x/PATHFIND_CELL_SIZE); - Int y = REAL_TO_INT(pos->y/PATHFIND_CELL_SIZE); - - return validMovementPosition( isCrusher, layer, locomotorSet, x, y ); -} - -inline const Coord3D *Pathfinder::getDebugPathPosition( void ) -{ - return &debugPathPos; -} - -inline void Pathfinder::setDebugPathPosition( const Coord3D *pos ) -{ - debugPathPos = *pos; -} - -inline Path *Pathfinder::getDebugPath( void ) -{ - return debugPath; -} - -inline void Pathfinder::addObjectToPathfindMap( class Object *obj ) -{ - classifyObjectFootprint( obj, true ); -} - -inline void Pathfinder::removeObjectFromPathfindMap( class Object *obj ) -{ - classifyObjectFootprint( obj, false ); -} - -inline PathfindCell *Pathfinder::getCell( PathfindLayerEnum layer, Int x, Int y ) -{ - if (x >= m_extent.lo.x && x <= m_extent.hi.x && - y >= m_extent.lo.y && y <= m_extent.hi.y) - { - PathfindCell *cell = NULL; - if (layer > LAYER_GROUND && layer <= LAYER_LAST) - { - cell = m_layers[layer].getCell(x, y); - if (cell) - return cell; - } - return &m_map[x][y]; - } - else - { - return NULL; - } -} - -inline PathfindCell *Pathfinder::getCell( PathfindLayerEnum layer, const Coord3D *pos ) -{ - ICoord2D cell; - Bool overflow = worldToCell( pos, &cell ); - if (overflow) return NULL; - return getCell( layer, cell.x, cell.y ); -} - -inline PathfindCell *Pathfinder::getClippedCell( PathfindLayerEnum layer, const Coord3D *pos) -{ - ICoord2D cell; - worldToCell( pos, &cell ); - return getCell( layer, cell.x, cell.y ); -} - -inline Bool Pathfinder::worldToCell( const Coord3D *pos, ICoord2D *cell ) -{ - cell->x = REAL_TO_INT_FLOOR(pos->x/PATHFIND_CELL_SIZE); - cell->y = REAL_TO_INT_FLOOR(pos->y/PATHFIND_CELL_SIZE); - Bool overflow = false; - if (cell->x < m_extent.lo.x) {overflow = true; cell->x = m_extent.lo.x;} - if (cell->y < m_extent.lo.y) {overflow = true; cell->y = m_extent.lo.y;} - if (cell->x > m_extent.hi.x) {overflow = true; cell->x = m_extent.hi.x;} - if (cell->y > m_extent.hi.y) {overflow = true; cell->y = m_extent.hi.y;} - return overflow; -} - -/** - * Return true if the given object ID is registered as an obstacle in this cell - */ -inline Bool PathfindCell::isObstaclePresent( ObjectID objID ) const -{ - if (objID != INVALID_ID && (getType() == PathfindCell::CELL_OBSTACLE)) - { - DEBUG_ASSERTCRASH(m_info, ("Should have info to be obstacle.")); - return (m_info && m_info->m_obstacleID == objID); - } - - return false; -} - - -#endif // _PATHFIND_H_ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +//////////////////////////////////////////////////////////////////////////////// +// // +// (c) 2001-2003 Electronic Arts Inc. // +// // +//////////////////////////////////////////////////////////////////////////////// + +// AIPathfind.h +// AI pathfinding system +// Author: Michael S. Booth, October 2001 + +#pragma once + +#ifndef _PATHFIND_H_ +#define _PATHFIND_H_ + +#include "Common/GameType.h" +#include "Common/GameMemory.h" +#include "Common/Snapshot.h" +//#include "GameLogic/Locomotor.h" // no, do not include this, unless you like long recompiles +#include "GameLogic/LocomotorSet.h" +#include "GameLogic/GameLogic.h" +#include "Common/GameDefines.h" + +class Bridge; +class Object; +class Weapon; +class PathfindZoneManager; + +// How close is close enough when moving. + +#define PATHFIND_CLOSE_ENOUGH 1.0f +#define PATH_MAX_PRIORITY 0x7FFFFFFF + +#define INFANTRY_MOVES_THROUGH_INFANTRY + +// TheSuperHackers @info This is here to easily toggle the retail compatible hacks for the pathfinding +#if RETAIL_COMPATIBLE_CRC +#define RETAIL_COMPATIBLE_PATHFINDING (1) +#else +#define RETAIL_COMPATIBLE_PATHFINDING (0) +#endif + + typedef UnsignedShort zoneStorageType; + + +//---------------------------------------------------------------------------------------------------------- + +/** + * PathNodes are used to create a final Path to return from the + * pathfinder. Note that these are not used during the A* search. + */ +class PathNode : public MemoryPoolObject +{ +public: + PathNode(); + + Coord3D *getPosition( void ) { return &m_pos; } ///< return position of this node + const Coord3D *getPosition( void ) const { return &m_pos; } ///< return position of this node + void setPosition( const Coord3D *pos ) { m_pos = *pos; } ///< set the position of this path node + + const Coord3D *computeDirectionVector( void ); ///< compute direction to next node + + PathNode *getNext( void ) { return m_next; } ///< return next node in the path + PathNode *getPrevious( void ) { return m_prev; } ///< return previous node in the path + const PathNode *getNext( void ) const { return m_next; } ///< return next node in the path + const PathNode *getPrevious( void ) const { return m_prev; } ///< return previous node in the path + + PathfindLayerEnum getLayer( void ) const { return m_layer; } ///< return layer of this node. + void setLayer( PathfindLayerEnum layer ) { m_layer = layer; } ///< set the layer of this path node + + void setNextOptimized( PathNode *node ); + + PathNode *getNextOptimized(Coord2D* dir = NULL, Real* dist = NULL) ///< return next node in optimized path + { + if (dir) + *dir = m_nextOptiDirNorm2D; + if (dist) + *dist = m_nextOptiDist2D; + return m_nextOpti; + } + + const PathNode *getNextOptimized(Coord2D* dir = NULL, Real* dist = NULL) const ///< return next node in optimized path + { + if (dir) + *dir = m_nextOptiDirNorm2D; + if (dist) + *dist = m_nextOptiDist2D; + return m_nextOpti; + } + + void setCanOptimize(Bool canOpt) { m_canOptimize = canOpt;} + Bool getCanOptimize( void ) const { return m_canOptimize;} + + /// given a list, prepend this node, return new list + PathNode *prependToList( PathNode *list ); + + /// given a list, append this node, return new list. slow implementation. + /// @todo optimize this + PathNode *appendToList( PathNode *list ); + + /// given a node, append to this node + void append( PathNode *list ); + +public: + mutable Int m_id; // Used in Path::xfer() to save & recreate the path list. + +private: + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( PathNode, "PathNodePool" ); ///< @todo Set real numbers for mem alloc + + PathNode* m_nextOpti; ///< next node in the optimized path + PathNode* m_next; ///< next node in the path + PathNode* m_prev; ///< previous node in the path + Coord3D m_pos; ///< position of node in space + PathfindLayerEnum m_layer; ///< Layer for this section. + Bool m_canOptimize; ///< True if this cell can be optimized out. + + Real m_nextOptiDist2D; ///< if nextOpti is nonnull, the dist to it. + Coord2D m_nextOptiDirNorm2D; ///< if nextOpti is nonnull, normalized dir vec towards it. + +}; + +// this doesn't actually seem to be a particularly useful win, +// performance-wise, so I didn't enable it. (srj) +#define NO_CPOP_STARTS_FROM_PREV_SEG + +struct ClosestPointOnPathInfo +{ + Real distAlongPath; + Coord3D posOnPath; + PathfindLayerEnum layer; +}; + +/** + * This class encapsulates a "path" returned by the Pathfinder. + */ +class Path : public MemoryPoolObject, public Snapshot +{ +public: + Path(); + + PathNode *getFirstNode( void ) { return m_path; } + PathNode *getLastNode( void ) { return m_pathTail; } + + void updateLastNode( const Coord3D *pos ); + + void prependNode( const Coord3D *pos, PathfindLayerEnum layer ); ///< Create a new node at the head of the path + void appendNode( const Coord3D *pos, PathfindLayerEnum layer ); ///< Create a new node at the end of the path + void setBlockedByAlly(Bool blocked) {m_blockedByAlly = blocked;} + Bool getBlockedByAlly(void) {return m_blockedByAlly;} + void optimize( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, Bool blocked ); ///< Optimize the path to discard redundant nodes + + void optimizeGroundPath( Bool crusher, Int diameter ); ///< Optimize the ground path to discard redundant nodes + + /// Given a location, return nearest location on path, and along-path dist to end as function result + void computePointOnPath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D& pos, ClosestPointOnPathInfo& out); + + /// Given a location, return nearest location on path, and along-path dist to end as function result + void peekCachedPointOnPath( Coord3D& pos ) const {pos = m_cpopOut.posOnPath;} + + /// Given a flight path, compute the distance to goal (0 if we are past it) & return the goal pos. + Real computeFlightDistToGoal( const Coord3D *pos, Coord3D& goalPos ); + + /// Given a location, return closest location on path, and along-path dist to end as function result + void markOptimized(void) {m_isOptimized = true;} + +protected: + // snapshot interface + virtual void crc( Xfer *xfer ); + virtual void xfer( Xfer *xfer ); + virtual void loadPostProcess(); + +protected: + enum {MAX_CPOP=20}; ///< Max times we will return the cached cpop. + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( Path, "PathPool" ); ///< @todo Set real numbers for mem alloc + + PathNode* m_path; ///< The list of PathNode objects that define the path + PathNode* m_pathTail; + Bool m_isOptimized; ///< True if the path has been optimized + Bool m_blockedByAlly; ///< An ally needs to move off of this path. + // caching info for computePointOnPath. + Bool m_cpopValid; + Int m_cpopCountdown; ///< We only return the same cpop MAX_CPOP times. It is occasionally possible to get stuck. + Coord3D m_cpopIn; + ClosestPointOnPathInfo m_cpopOut; + const PathNode* m_cpopRecentStart; +}; + +//---------------------------------------------------------------------------------------------------------- + +// See GameType.h for +// enum {LAYER_INVALID = 0, LAYER_GROUND = 1, LAYER_TOP=2 }; + +// Fits in 4 bits for now +enum {MAX_WALL_PIECES = 128}; + +class PathfindCellInfo +{ + friend class PathfindCell; +public: +#if RETAIL_COMPATIBLE_PATHFINDING + static void forceCleanPathFindInfoCells(void); +#endif + static void allocateCellInfos(void); + static void releaseCellInfos(void); + + static PathfindCellInfo * getACellInfo(PathfindCell *cell, const ICoord2D &pos); + static void releaseACellInfo(PathfindCellInfo *theInfo); + +protected: + static PathfindCellInfo *s_infoArray; + static PathfindCellInfo *s_firstFree; ///< + + + PathfindCellInfo *m_nextOpen, *m_prevOpen; ///< for A* "open" list, shared by closed list + + PathfindCellInfo *m_pathParent; ///< "parent" cell from pathfinder + PathfindCell *m_cell; ///< Cell this info belongs to currently. + + UnsignedShort m_totalCost, m_costSoFar; ///< cost estimates for A* search + + /// have to include cell's coordinates, since cells are often accessed via pointer only + ICoord2D m_pos; + + ObjectID m_goalUnitID; ///< The objectID of the ground unit whose goal this is. + ObjectID m_posUnitID; ///< The objectID of the ground unit that is occupying this cell. + ObjectID m_goalAircraftID; ///< The objectID of the aircraft whose goal this is. + + ObjectID m_obstacleID; ///< the object ID who overlaps this cell + + UnsignedInt m_isFree:1; + UnsignedInt m_blockedByAlly:1;///< True if this cell is blocked by an allied unit. + UnsignedInt m_obstacleIsFence:1;///< True if occupied by a fence. + UnsignedInt m_obstacleIsTransparent:1;///< True if obstacle is transparent (undefined if obstacleid is invalid) + /// @todo Do we need both mark values in this cell? Can't store a single value and compare it? + UnsignedInt m_open:1; ///< place for marking this cell as on the open list + UnsignedInt m_closed:1; ///< place for marking this cell as on the closed list +}; + +/** + * This represents one cell in the pathfinding grid. + * These cells categorize the world into idealized cellular states, + * and are also used for efficient A* pathfinding. + * @todo Optimize memory usage of pathfind grid. + */ +class PathfindCell +{ +public: + + enum CellType + { + CELL_CLEAR = 0x00, ///< clear, unobstructed ground + CELL_WATER = 0x01, ///< water area + CELL_CLIFF = 0x02, ///< steep altitude change + CELL_RUBBLE = 0x03, ///< Cell is occupied by rubble. + CELL_OBSTACLE = 0x04, ///< Occupied by a structure + CELL_BRIDGE_IMPASSABLE = 0x05, ///< Piece of a bridge that is impassable. + CELL_IMPASSABLE = 0x06 ///< Just plain impassable except for aircraft. + }; + + enum CellFlags + { + NO_UNITS = 0x00, ///< No units in this cell. + UNIT_GOAL = 0x01, ///< A unit is heading to this cell. + UNIT_PRESENT_MOVING = 0x02, ///< A unit is moving through this cell. + UNIT_PRESENT_FIXED = 0x03, ///< A unit is stationary in this cell. + UNIT_GOAL_OTHER_MOVING = 0x05 ///< A unit is moving through this cell, and another unit has this as it's goal. + }; + + /// reset the cell + void reset( ); + + PathfindCell(void); + ~PathfindCell(void); + + Bool setTypeAsObstacle( Object *obstacle, Bool isFence, const ICoord2D &pos ); ///< flag this cell as an obstacle, from the given one + Bool removeObstacle( Object *obstacle ); ///< unflag this cell as an obstacle, from the given one + void setType( CellType type ); ///< set the cell type + CellType getType( void ) const { return (CellType)m_type; } ///< get the cell type + CellFlags getFlags( void ) const { return (CellFlags)m_flags; } ///< get the cell type + Bool isAircraftGoal( void) const {return m_aircraftGoal != 0;} + + Bool isObstaclePresent( ObjectID objID ) const; ///< return true if the given object ID is registered as an obstacle in this cell + + Bool isObstacleTransparent( ) const{return m_info?m_info->m_obstacleIsTransparent:false; } ///< return true if the obstacle in the cell is KINDOF_CAN_SEE_THROUGHT_STRUCTURE + + Bool isObstacleFence( void ) const {return m_info?m_info->m_obstacleIsFence:false; }///< return true if the given obstacle in the cell is a fence. + + /// Return estimated cost from given cell to reach goal cell + UnsignedInt costToGoal( PathfindCell *goal ); + + UnsignedInt costToHierGoal( PathfindCell *goal ); + + UnsignedInt costSoFar( PathfindCell *parent ); + + /// put self on "open" list in ascending cost order, return new list + PathfindCell *putOnSortedOpenList( PathfindCell *list ); + + /// remove self from "open" list + PathfindCell *removeFromOpenList( PathfindCell *list ); + + /// put self on "closed" list, return new list + PathfindCell *putOnClosedList( PathfindCell *list ); + + /// remove self from "closed" list + PathfindCell *removeFromClosedList( PathfindCell *list ); + + /// remove all cells from closed list. + static Int releaseClosedList( PathfindCell *list ); + + /// remove all cells from closed list. + static Int releaseOpenList( PathfindCell *list ); + + inline PathfindCell *getNextOpen(void) {return m_info->m_nextOpen?m_info->m_nextOpen->m_cell:NULL;} + + inline UnsignedShort getXIndex(void) const {return m_info->m_pos.x;} + inline UnsignedShort getYIndex(void) const {return m_info->m_pos.y;} + + inline Bool isBlockedByAlly(void) const {return m_info->m_blockedByAlly;} + inline void setBlockedByAlly(Bool blocked) {m_info->m_blockedByAlly = (blocked!=0);} + + inline Bool getOpen(void) const {return m_info->m_open;} + inline Bool getClosed(void) const {return m_info->m_closed;} + inline UnsignedInt getCostSoFar(void) const {return m_info->m_costSoFar;} + inline UnsignedInt getTotalCost(void) const {return m_info->m_totalCost;} + + inline void setCostSoFar(UnsignedInt cost) { if( m_info ) m_info->m_costSoFar = cost;} + inline void setTotalCost(UnsignedInt cost) { if( m_info ) m_info->m_totalCost = cost;} + + void setParentCell(PathfindCell* parent); + void clearParentCell(void); + void setParentCellHierarchical(PathfindCell* parent); + inline PathfindCell* getParentCell(void) const {return m_info ? m_info->m_pathParent ? m_info->m_pathParent->m_cell : NULL : NULL;} + + Bool startPathfind( PathfindCell *goalCell ); + Bool getPinched(void) const {return m_pinched;} + void setPinched(Bool pinch) {m_pinched = pinch; } + + Bool allocateInfo(const ICoord2D &pos); + void releaseInfo(void); + Bool hasInfo(void) const {return m_info!=NULL;} + zoneStorageType getZone(void) const {return m_zone;} + void setZone(zoneStorageType zone) {m_zone = zone;} + void setGoalUnit(ObjectID unit, const ICoord2D &pos ); + void setGoalAircraft(ObjectID unit, const ICoord2D &pos ); + void setPosUnit(ObjectID unit, const ICoord2D &pos ); + inline ObjectID getGoalUnit(void) const {ObjectID id = m_info?m_info->m_goalUnitID:INVALID_ID; return id;} + inline ObjectID getGoalAircraft(void) const {ObjectID id = m_info?m_info->m_goalAircraftID:INVALID_ID; return id;} + inline ObjectID getPosUnit(void) const {ObjectID id = m_info?m_info->m_posUnitID:INVALID_ID; return id;} + + inline ObjectID getObstacleID(void) const {ObjectID id = m_info?m_info->m_obstacleID:INVALID_ID; return id;} + + void setLayer( PathfindLayerEnum layer ) { m_layer = layer; } ///< set the cell layer + PathfindLayerEnum getLayer( void ) const { return (PathfindLayerEnum)m_layer; } ///< get the cell layer + + void setConnectLayer( PathfindLayerEnum layer ) { m_connectsToLayer = layer; } ///< set the cell layer connect id + PathfindLayerEnum getConnectLayer( void ) const { return (PathfindLayerEnum)m_connectsToLayer; } ///< get the cell layer connect id + +private: + PathfindCellInfo *m_info; + zoneStorageType m_zone:14; ///< Zone. Each zone is a set of adjacent terrain type. If from & to in the same zone, you can successfully pathfind. If not, + // you still may be able to if you can cross multiple terrain types. + UnsignedShort m_aircraftGoal:1; //< This is an aircraft goal cell. + UnsignedShort m_pinched:1; //< This cell is surrounded by obstacle cells. + UnsignedByte m_type:4; ///< what type of cell terrain this is. + UnsignedByte m_flags:4; ///< what type of units are in or moving through this cell. + UnsignedByte m_connectsToLayer:4; ///< This cell can pathfind onto this layer, if > LAYER_TOP. + UnsignedByte m_layer:4; ///< Layer of this cell. +}; + +typedef PathfindCell *PathfindCellP; + + +// how close a unit has to be in z to interact with the layer. +#define LAYER_Z_CLOSE_ENOUGH_F 10.0f +/** + * This class represents a bridge in the map. This is effectively + * a sub-rectangle of the big pathfind map. + */ +class PathfindLayer +{ +public: + PathfindLayer(); + ~PathfindLayer(); +public: + void reset(void); + Bool init(Bridge *theBridge, PathfindLayerEnum layer); + void allocateCells(const IRegion2D *extent); + void allocateCellsForWallLayer(const IRegion2D *extent, ObjectID *wallPieces, Int numPieces); + void classifyCells(); + void classifyWallCells(ObjectID *wallPieces, Int numPieces); + Bool setDestroyed(Bool destroyed); + Bool isUnused(void); // True if it doesn't contain a bridge. + Bool isDestroyed(void) {return m_destroyed;} // True if it has been destroyed. + PathfindCell *getCell(Int x, Int y); + Int getZone(void) {return m_zone;} + void setZone(Int zone) {m_zone = zone;} + void applyZone(void); // Propagates m_zone to all cells. + void getStartCellIndex(ICoord2D *start) {*start = m_startCell;} + void getEndCellIndex(ICoord2D *end) {*end = m_endCell;} + + ObjectID getBridgeID(void); + Bool connectsZones(PathfindZoneManager *zm, const LocomotorSet& locomotorSet,Int zone1, Int zone2); + Bool isPointOnWall(ObjectID *wallPieces, Int numPieces, const Coord3D *pt); + +#if defined(RTS_DEBUG) + void doDebugIcons(void) ; +#endif +protected: + void classifyLayerMapCell( Int i, Int j , PathfindCell *cell, Bridge *theBridge); + void classifyWallMapCell( Int i, Int j, PathfindCell *cell , ObjectID *wallPieces, Int numPieces); + +private: + PathfindCell *m_blockOfMapCells; ///< Pathfinding map - contains iconic representation of the map + PathfindCell **m_layerCells; ///< Pathfinding map indexes - contains matrix indexing into the map. + Int m_width; // Number of cells in x + Int m_height; // Number of cells in y + Int m_xOrigin; // Index of first cell in x + Int m_yOrigin; // Index of first cell in y + ICoord2D m_startCell; // pathfind cell indexes for center cell on the from side. + ICoord2D m_endCell; // pathfind cell indexes for center cell on the to side. + + PathfindLayerEnum m_layer; + Int m_zone; // Whole bridge is in same zone. + Bridge *m_bridge; // Corresponding bridge in TerrainLogic. + Bool m_destroyed; + + +}; + + +#define PATHFIND_CELL_SIZE 10 +#define PATHFIND_CELL_SIZE_F 10.0f + +enum { PATHFIND_QUEUE_LEN=512}; + +struct TCheckMovementInfo; + +/** + * This class is a helper class for zone manager. It maintains information regarding the + * LocomotorSurfaceTypeMask equivalencies within a ZONE_BLOCK_SIZE x ZONE_BLOCK_SIZE area of + * cells. This is used in hierarchical pathfinding to find the best coarse path at the + * block level. + */ +class ZoneBlock +{ +public: + + ZoneBlock(); + ~ZoneBlock(); // not virtual, please don't override without making virtual. jba. + + void blockCalculateZones( PathfindCell **map, PathfindLayer layers[], const IRegion2D &bounds); ///< Does zone calculations. + zoneStorageType getEffectiveZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher, zoneStorageType zone) const; + + void clearMarkedPassable(void) {m_markedPassable = false;} + Bool isPassable(void) {return m_markedPassable;} + void setPassable(Bool pass) {m_markedPassable = pass;} + + Bool getInteractsWithBridge(void) const {return m_interactsWithBridge;} + void setInteractsWithBridge(Bool interacts) {m_interactsWithBridge = interacts;} + +protected: + void allocateZones(void); + void freeZones(void); + +protected: + ICoord2D m_cellOrigin; + + zoneStorageType m_firstZone; // First zone in this block. + UnsignedShort m_numZones; // Number of zones in this block. If == 1, there is only one zone, and + // no zone equivalency arrays will be allocated. + + + UnsignedShort m_zonesAllocated; + zoneStorageType *m_groundCliffZones; + zoneStorageType *m_groundWaterZones; + zoneStorageType *m_groundRubbleZones; + zoneStorageType *m_crusherZones; + Bool m_interactsWithBridge; + Bool m_markedPassable; +}; +typedef ZoneBlock *ZoneBlockP; + +/** + * This class manages the zones in the map. A zone is an area in the map that + * is one contiguous type of terrain (clear, cliff, water, building). If + * a unit is in a zone, and wants to move to another location, the destination + * zone has to be the same, or it can't get there. + * There are equivalency tables for meta-zones. For example, an amphibious craft can + * travel through water and clear cells. + */ +class PathfindZoneManager +{ +public: + enum {INITIAL_ZONES = 256}; + enum {ZONE_BLOCK_SIZE = 10}; // Zones are calculated in blocks of 20x20. This way, the raw zone numbers can be used to + enum {UNINITIALIZED_ZONE = 0}; + // compute hierarchically between the 20x20 blocks of cells. jba. + PathfindZoneManager(); + ~PathfindZoneManager(); + + void reset(void); + + Bool needToCalculateZones(void) const {return m_nextFrameToCalculateZones <= TheGameLogic->getFrame() ;} ///< Returns true if the zones need to be recalculated. + void markZonesDirty( Bool insert ) ; ///< Called when the zones need to be recalculated. + void updateZonesForModify( PathfindCell **map, PathfindLayer layers[], const IRegion2D &structureBounds, const IRegion2D &globalBounds ) ; ///< Called to recalculate an area when a structure has been removed. + void calculateZones( PathfindCell **map, PathfindLayer layers[], const IRegion2D &bounds); ///< Does zone calculations. + zoneStorageType getEffectiveZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher, zoneStorageType zone) const; + zoneStorageType getEffectiveTerrainZone(zoneStorageType zone) const; + + zoneStorageType getNextZone(void); + + void getExtent(ICoord2D &extent) const {extent = m_zoneBlockExtent;} + + /// return zone relative the the block zone that this cell resides in. + zoneStorageType getBlockZone(LocomotorSurfaceTypeMask acceptableSurfaces, Bool crusher, Int cellX, Int cellY, PathfindCell **map) const; + void allocateBlocks(const IRegion2D &globalBounds); + + void clearPassableFlags(void); + Bool isPassable(Int cellX, Int cellY) const; + Bool clipIsPassable(Int cellX, Int cellY) const; + void setPassable(Int cellX, Int cellY, Bool passable); + + void setAllPassable(void); + + void setBridge(Int cellX, Int cellY, Bool bridge); + Bool interactsWithBridge(Int cellX, Int cellY) const; + +private: + void allocateZones(void); + void freeZones(void); + void freeBlocks(void); + +private: + ZoneBlock *m_blockOfZoneBlocks; ///< Zone blocks - Info for hierarchical pathfinding at a "blocky" level. + ZoneBlock **m_zoneBlocks; ///< Zone blocks as a matrix - contains matrix indexing into the map. + ICoord2D m_zoneBlockExtent; ///< Zone block extents. Not the same scale as the pathfind extents. + + UnsignedShort m_maxZone; ///< Max zone used. + UnsignedInt m_nextFrameToCalculateZones; ///< WHen should I recalculate, next?. + UnsignedShort m_zonesAllocated; + zoneStorageType *m_groundCliffZones; + zoneStorageType *m_groundWaterZones; + zoneStorageType *m_groundRubbleZones; + zoneStorageType *m_terrainZones; + zoneStorageType *m_crusherZones; + zoneStorageType *m_hierarchicalZones; +}; + +/** + * The pathfinding services interface provides access to the 3 expensive path find calls: + * findPath, findClosestPath, and findAttackPath. + * It is only available to units when their ai interface doPathfind method is called. + * This allows the pathfinder to spread out the pathfinding over a number of frames + * when a lot of units are trying to pathfind all at the same time. + */ +class PathfindServicesInterface { +public: + virtual Path *findPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, + const Coord3D *to )=0; ///< Find a short, valid path between given locations + /** Find a short, valid path to a location NEAR the to location. + This succeds when the destination is unreachable (like inside a building). + If the destination is unreachable, it will adjust the to point. */ + virtual Path *findClosestPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, + Coord3D *to, Bool blocked, Real pathCostMultiplier, Bool moveAllies )=0; + + /** Find a short, valid path to a location that obj can attack victim from. */ + virtual Path *findAttackPath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, + const Object *victim, const Coord3D* victimPos, const Weapon *weapon )=0; + + /** Patch to the exiting path from the current position, either because we became blocked, + or because we had to move off the path to avoid other units. */ + virtual Path *patchPath( const Object *obj, const LocomotorSet& locomotorSet, + Path *originalPath, Bool blocked ) = 0; + + /** Find a short, valid path to a location that is away from the repulsors. */ + virtual Path *findSafePath( const Object *obj, const LocomotorSet& locomotorSet, + const Coord3D *from, const Coord3D* repulsorPos1, const Coord3D* repulsorPos2, Real repulsorRadius ) = 0; + +}; + +/** + * The Pathfinding engine itself. + */ +class Pathfinder : PathfindServicesInterface, public Snapshot +{ +// The following routines are private, but available through the doPathfind callback to aiInterface. jba. +private: + virtual Path *findPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to); ///< Find a short, valid path between given locations + /** Find a short, valid path to a location NEAR the to location. + This succeds when the destination is unreachable (like inside a building). + If the destination is unreachable, it will adjust the to point. */ + virtual Path *findClosestPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, + Coord3D *to, Bool blocked, Real pathCostMultiplier, Bool moveAllies ); + + /** Find a short, valid path to a location that obj can attack victim from. */ + virtual Path *findAttackPath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, + const Object *victim, const Coord3D* victimPos, const Weapon *weapon ); + + /** Find a short, valid path to a location that is away from the repulsors. */ + virtual Path *findSafePath( const Object *obj, const LocomotorSet& locomotorSet, + const Coord3D *from, const Coord3D* repulsorPos1, const Coord3D* repulsorPos2, Real repulsorRadius ); + + /** Patch to the exiting path from the current position, either because we became blocked, + or because we had to move off the path to avoid other units. */ + virtual Path *patchPath( const Object *obj, const LocomotorSet& locomotorSet, + Path *originalPath, Bool blocked ); + +public: + Pathfinder( void ); + ~Pathfinder() ; + + void reset( void ); ///< Reset system in preparation for new map + + // --------------- inherited from Snapshot interface -------------- + void crc( Xfer *xfer ); + void xfer( Xfer *xfer ); + void loadPostProcess( void ); + + Bool clientSafeQuickDoesPathExist( const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to ); ///< Can we build any path at all between the locations (terrain & buildings check - fast) + Bool clientSafeQuickDoesPathExistForUI( const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to ); ///< Can we build any path at all between the locations (terrain onlyk - fast) + Bool slowDoesPathExist( Object *obj, const Coord3D *from, + const Coord3D *to, ObjectID ignoreObject=INVALID_ID ); ///< Can we build any path at all between the locations (terrain, buildings & units check - slower) + + Bool queueForPath(ObjectID id); ///< The object wants to request a pathfind, so put it on the list to process. + void processPathfindQueue(void); ///< Process some or all of the queued pathfinds. + void forceMapRecalculation( ); ///< Force pathfind map recomputation. If region is given, only that area is recomputed + + /** Returns an aircraft path to the goal. */ + Path *getAircraftPath( const Object *obj, const Coord3D *to); + Path *findGroundPath( const Coord3D *from, const Coord3D *to, Int pathRadius, + Bool crusher); ///< Find a short, valid path of the desired width on the ground. + + void addObjectToPathfindMap( class Object *obj ); ///< Classify the given object's cells in the map + void removeObjectFromPathfindMap( class Object *obj ); ///< De-classify the given object's cells in the map + + void removeUnitFromPathfindMap( Object *obj ); ///< De-classify the given mobile unit's cells in the map + void updateGoal( Object *obj, const Coord3D *newGoalPos, PathfindLayerEnum layer); ///< Update the given mobile unit's cells in the map + void updateAircraftGoal( Object *obj, const Coord3D *newGoalPos); ///< Update the given aircraft unit's cells in the map + void removeGoal( Object *obj); ///< Removes the given mobile unit's goal cells in the map + void updatePos( Object *obj, const Coord3D *newPos); ///< Update the given mobile unit's cells in the map + void removePos( Object *obj); ///< Removes the unit's position cells from the map + + Bool moveAllies(Object *obj, Path *path); + + // NOTE - The object MUST NOT MOVE between the call to createAWall... and removeWall... + // or BAD THINGS will happen. jba. + void createAWallFromMyFootprint( Object *obj ) {internal_classifyObjectFootprint(obj, true);} // Temporarily treat this object as an obstacle. + void removeWallFromMyFootprint( Object *obj ){internal_classifyObjectFootprint(obj, false);} // Undo createAWallFromMyFootprint. + + Path *getMoveAwayFromPath(Object *obj, Object *otherObj, Path *pathToAvoid, Object *otherObj2, Path *pathToAvoid2); + + void changeBridgeState( PathfindLayerEnum layer, Bool repaired ); + + Bool findBrokenBridge(const LocomotorSet &locomotorSet, const Coord3D *from, const Coord3D *to, ObjectID *bridgeID); + + void newMap(void); + + PathfindCell *getCell( PathfindLayerEnum layer, Int x, Int y ); ///< Return the cell at grid coords (x,y) + PathfindCell *getCell( PathfindLayerEnum layer, const Coord3D *pos ); ///< Given a position, return associated grid cell + PathfindCell *getClippedCell( PathfindLayerEnum layer, const Coord3D *pos ); ///< Given a position, return associated grid cell + void clip(Coord3D *from, Coord3D *to); + Bool worldToCell( const Coord3D *pos, ICoord2D *cell ); ///< Given a world position, return grid cell coordinate + + const ICoord2D *getExtent(void) const {return &m_extent.hi;} + + void setIgnoreObstacleID( ObjectID objID ); ///< if non-zero, the pathfinder will ignore the given obstacle + + Bool validMovementPosition( Bool isCrusher, LocomotorSurfaceTypeMask acceptableSurfaces, PathfindCell *toCell, PathfindCell *fromCell = NULL ); ///< Return true if given position is a valid movement location + Bool validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, Int x, Int y ); ///< Return true if given position is a valid movement location + Bool validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, const Coord3D *pos ); ///< Return true if given position is a valid movement location + Bool validMovementTerrain( PathfindLayerEnum layer, const Locomotor* locomotor, const Coord3D *pos ); ///< Return true if given position is a valid movement location + + Locomotor* chooseBestLocomotorForPosition(PathfindLayerEnum layer, LocomotorSet* locomotorSet, const Coord3D* pos ); + + Bool isViewBlockedByObstacle(const Object* obj, const Object* objOther); ///< Return true if the straight line between the given points contains any obstacle, and thus blocks vision + + Bool isAttackViewBlockedByObstacle(const Object* obj, const Coord3D& attackerPos, const Object* victim, const Coord3D& victimPos); ///< Return true if the straight line between the given points contains any obstacle, and thus blocks vision + + Bool isLinePassable( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, + PathfindLayerEnum layer, const Coord3D& startWorld, const Coord3D& endWorld, + Bool blocked, Bool allowPinched ); ///< Return true if the straight line between the given points is passable + + void moveAlliesAwayFromDestination( Object *obj,const Coord3D& destination); + + Bool isGroundPathPassable( Bool isCrusher, const Coord3D& startWorld, PathfindLayerEnum startLayer, + const Coord3D& endWorld, Int pathDiameter); ///< Return true if the straight line between the given points is passable + + // for debugging + const Coord3D *getDebugPathPosition( void ); + void setDebugPathPosition( const Coord3D *pos ); + Path *getDebugPath( void ); + void setDebugPath( Path *debugpath ); + +#if RETAIL_COMPATIBLE_PATHFINDING + void forceCleanCells(void); +#endif + void cleanOpenAndClosedLists(void); + + // Adjusts the destination to a spot near dest that is not occupied by other units. + Bool adjustDestination(Object *obj, const LocomotorSet& locomotorSet, + Coord3D *dest, const Coord3D *groupDest=NULL); + + // Adjusts the destination to a spot near dest for landing that is not occupied by other units. + Bool adjustToLandingDestination(Object *obj, Coord3D *dest); + + // Adjusts the destination to a spot that can attack target that is not occupied by other units. + Bool adjustTargetDestination(const Object *obj, const Object *target, const Coord3D *targetPos, + const Weapon *weapon, Coord3D *dest); + + // Adjusts destination to a spot near dest that is possible to path to. + Bool adjustToPossibleDestination(Object *obj, const LocomotorSet& locomotorSet, Coord3D *dest); + + void snapPosition(Object *obj, Coord3D *pos); // Snaps the current position to it's grid location. + void snapClosestGoalPosition(Object *obj, Coord3D *pos); // Snaps the current position to a good goal position. + Bool goalPosition(Object *obj, Coord3D *pos); // Returns the goal position on the grid. + + PathfindLayerEnum addBridge(Bridge *theBridge); // Adds a bridge layer, and returns the layer id. + + void addWallPiece(Object *wallPiece); // Adds a wall piece. + void removeWallPiece(Object *wallPiece); // Removes a wall piece. + Real getWallHeight(void) {return m_wallHeight;} + Bool isPointOnWall(const Coord3D *pos); + + void updateLayer(Object *obj, PathfindLayerEnum layer); ///< Updates object's layer. + + static void classifyMapCell( Int x, Int y, PathfindCell *cell); ///< Classify the given map cell + Int clearCellForDiameter( Bool crusher, Int cellX, Int cellY, PathfindLayerEnum layer, Int pathDiameter ); ///< Return true if given position is a valid movement location + +protected: + virtual Path *internalFindPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to); ///< Find a short, valid path between given locations + Path *findHierarchicalPath( Bool isHuman, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to, Bool crusher); + Path *findClosestHierarchicalPath( Bool isHuman, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *to, Bool crusher); + Path *internal_findHierarchicalPath( Bool isHuman, const LocomotorSurfaceTypeMask locomotorSurface, const Coord3D *from, const Coord3D *to, Bool crusher, Bool closestOK); + void processHierarchicalCell( const ICoord2D &scanCell, const ICoord2D &deltaPathfindCell, + PathfindCell *parentCell, + PathfindCell *goalCell, zoneStorageType parentZone, + zoneStorageType *examinedZones, Int &numExZones, + Bool crusher, Int &cellCount); + Bool checkForAdjust(Object *, const LocomotorSet& locomotorSet, Bool isHuman, Int cellX, Int cellY, + PathfindLayerEnum layer, Int iRadius, Bool center,Coord3D *dest, const Coord3D *groupDest) ; + Bool checkForLanding(Int cellX, Int cellY, + PathfindLayerEnum layer, Int iRadius, Bool center,Coord3D *dest) ; + Bool checkForTarget(const Object *obj, Int cellX, Int cellY, const Weapon *weapon, + const Object *victim, const Coord3D *victimPos, + Int iRadius, Bool center,Coord3D *dest) ; + Bool checkForPossible(Bool isCrusher, Int fromZone, Bool center, const LocomotorSet& locomotorSet, + Int cellX, Int cellY, PathfindLayerEnum layer, Coord3D *dest, Bool startingInObstacle) ; + void getRadiusAndCenter(const Object *obj, Int &iRadius, Bool ¢er); + void adjustCoordToCell(Int cellX, Int cellY, Bool centerInCell, Coord3D &pos, PathfindLayerEnum layer); + Bool checkDestination(const Object *obj, Int cellX, Int cellY, PathfindLayerEnum layer, Int iRadius, Bool centerInCell); + Bool checkForMovement(const Object *obj, TCheckMovementInfo &info); + Bool segmentIntersectsTallBuilding(const PathNode *curNode, PathNode *nextNode, + ObjectID ignoreBuilding, Coord3D *insertPos1, Coord3D *insertPos2, Coord3D *insertPos3); ///< Return true if the straight line between the given points intersects a tall building. + Bool circleClipsTallBuilding(const Coord3D *from, const Coord3D *to, Real radius, ObjectID ignoreBuilding, Coord3D *adjustTo); ///< Return true if the circle at the end of the line between the given points intersects a tall building. + + enum {NO_ATTACK=0}; + Int examineNeighboringCells(PathfindCell *parentCell, PathfindCell *goalCell, + const LocomotorSet& locomotorSet, Bool isHumanPlayer, + Bool centerInCell, Int radius, const ICoord2D &startCellNdx, + const Object *obj, Int attackDistance); + + Bool pathDestination( Object *obj, const LocomotorSet& locomotorSet, Coord3D *dest, + PathfindLayerEnum layer, const Coord3D *groupDest); ///< Checks cost between given locations + + Int checkPathCost(Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, + const Coord3D *to); + + void tightenPath(Object *obj, const LocomotorSet& locomotorSet, Coord3D *from, + const Coord3D *to); + + /** + return 0 to continue iterating the line, nonzero to terminate the iteration. + the nonzero result will be returned as the result of iterateCellsAlongLine(). + iterateCellsAlongLine will return zero if it completes. + */ + typedef Int (*CellAlongLineProc)(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + Int iterateCellsAlongLine(const Coord3D& startWorld, const Coord3D& endWorld, + PathfindLayerEnum layer, CellAlongLineProc proc, void* userData); + + Int iterateCellsAlongLine(const ICoord2D &start, const ICoord2D &end, + PathfindLayerEnum layer, CellAlongLineProc proc, void* userData); + + static Int linePassableCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int groundPathPassableCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int lineBlockedByObstacleCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int tightenPathCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int attackBlockedByObstacleCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int examineCellsCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int groundCellsCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + static Int moveAlliesDestinationCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + + static Int segmentIntersectsBuildingCallback(Pathfinder* pathfinder, PathfindCell* from, PathfindCell* to, Int to_x, Int to_y, void* userData); + + void classifyMap( void ); ///< Classify all cells in grid as obstacles, etc + void classifyObjectFootprint( Object *obj, Bool insert ); /** Classify the cells under the given object + If 'insert' is true, object is being added + If 'insert' is false, object is being removed */ + void internal_classifyObjectFootprint( Object *obj, Bool insert ); /** Classify the cells under the given object + If 'insert' is true, object is being added + If 'insert' is false, object is being removed */ + void classifyFence( Object *obj, Bool insert ); /** Classify the cells under the given fence object. */ + void classifyUnitFootprint( Object *obj, Bool insert, Bool remove, Bool update ); /** Classify the cells under the given object If 'insert' is true, object is being added */ + /// Convert world coordinate to array index + void worldToGrid( const Coord3D *pos, ICoord2D *cellIndex ); + + Bool evaluateCell(PathfindCell* newCell, PathfindCell *parentCell, + const LocomotorSet& locomotorSet, + Bool centerInCell, Int radius, + const Object *obj, Int attackDistance); + + Path *buildActualPath( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfaces, + const Coord3D *fromPos, PathfindCell *goalCell, Bool center, Bool blocked ); ///< Work backwards from goal cell to construct final path + Path *buildGroundPath( Bool isCrusher,const Coord3D *fromPos, PathfindCell *goalCell, + Bool center, Int pathDiameter ); ///< Work backwards from goal cell to construct final path + Path *buildHierachicalPath( const Coord3D *fromPos, PathfindCell *goalCell); ///< Work backwards from goal cell to construct final path + + void prependCells( Path *path, const Coord3D *fromPos, + PathfindCell *goalCell, Bool center ); ///< Add pathfind cells to a path. + + void debugShowSearch( Bool pathFound ); ///< Show all cells touched in the last search + static LocomotorSurfaceTypeMask validLocomotorSurfacesForCellType(PathfindCell::CellType t); + + void checkChangeLayers(PathfindCell *parentCell); + +#if defined(RTS_DEBUG) + void doDebugIcons(void) ; +#endif + +private: + /// This uses WAY too much memory. Should at least be array of pointers to cells w/ many fewer cells + PathfindCell *m_blockOfMapCells; ///< Pathfinding map - contains iconic representation of the map + PathfindCell **m_map; ///< Pathfinding map indexes - contains matrix indexing into the map. + IRegion2D m_extent; ///< Grid extent limits + IRegion2D m_logicalExtent; ///< Logical grid extent limits + + PathfindCell *m_openList; ///< Cells ready to be explored + PathfindCell *m_closedList; ///< Cells already explored + + Bool m_isMapReady; ///< True if all cells of map have been classified + Bool m_isTunneling; ///< True if path started in an obstacle + + Int m_frameToShowObstacles; ///< Time to redraw obstacles. For debug output. + + Coord3D debugPathPos; ///< Used for visual debugging + Path *debugPath; ///< Used for visual debugging + + ObjectID m_ignoreObstacleID; ///< Ignore the given obstacle + + PathfindZoneManager m_zoneManager; ///< Handles the pathfind zones. + + PathfindLayer m_layers[LAYER_LAST+1]; + + ObjectID m_wallPieces[MAX_WALL_PIECES]; + Int m_numWallPieces; + Real m_wallHeight; + + Int m_moveAlliesDepth; + + + // Pathfind queue + ObjectID m_queuedPathfindRequests[PATHFIND_QUEUE_LEN]; + Int m_queuePRHead; + Int m_queuePRTail; + Int m_cumulativeCellsAllocated; +}; + + +inline void Pathfinder::setIgnoreObstacleID( ObjectID objID ) +{ + m_ignoreObstacleID = objID; +} + +inline void Pathfinder::worldToGrid( const Coord3D *pos, ICoord2D *cellIndex ) +{ + cellIndex->x = REAL_TO_INT(pos->x/PATHFIND_CELL_SIZE); + cellIndex->y = REAL_TO_INT(pos->y/PATHFIND_CELL_SIZE); +} + +inline Bool Pathfinder::validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, Int x, Int y ) +{ + return validMovementPosition( isCrusher, locomotorSet.getValidSurfaces(), getCell( layer, x, y ) ); +} + +inline Bool Pathfinder::validMovementPosition( Bool isCrusher, PathfindLayerEnum layer, const LocomotorSet& locomotorSet, const Coord3D *pos ) +{ + + Int x = REAL_TO_INT(pos->x/PATHFIND_CELL_SIZE); + Int y = REAL_TO_INT(pos->y/PATHFIND_CELL_SIZE); + + return validMovementPosition( isCrusher, layer, locomotorSet, x, y ); +} + +inline const Coord3D *Pathfinder::getDebugPathPosition( void ) +{ + return &debugPathPos; +} + +inline void Pathfinder::setDebugPathPosition( const Coord3D *pos ) +{ + debugPathPos = *pos; +} + +inline Path *Pathfinder::getDebugPath( void ) +{ + return debugPath; +} + +inline void Pathfinder::addObjectToPathfindMap( class Object *obj ) +{ + classifyObjectFootprint( obj, true ); +} + +inline void Pathfinder::removeObjectFromPathfindMap( class Object *obj ) +{ + classifyObjectFootprint( obj, false ); +} + +inline PathfindCell *Pathfinder::getCell( PathfindLayerEnum layer, Int x, Int y ) +{ + if (x >= m_extent.lo.x && x <= m_extent.hi.x && + y >= m_extent.lo.y && y <= m_extent.hi.y) + { + PathfindCell *cell = NULL; + if (layer > LAYER_GROUND && layer <= LAYER_LAST) + { + cell = m_layers[layer].getCell(x, y); + if (cell) + return cell; + } + return &m_map[x][y]; + } + else + { + return NULL; + } +} + +inline PathfindCell *Pathfinder::getCell( PathfindLayerEnum layer, const Coord3D *pos ) +{ + ICoord2D cell; + Bool overflow = worldToCell( pos, &cell ); + if (overflow) return NULL; + return getCell( layer, cell.x, cell.y ); +} + +inline PathfindCell *Pathfinder::getClippedCell( PathfindLayerEnum layer, const Coord3D *pos) +{ + ICoord2D cell; + worldToCell( pos, &cell ); + return getCell( layer, cell.x, cell.y ); +} + +inline Bool Pathfinder::worldToCell( const Coord3D *pos, ICoord2D *cell ) +{ + cell->x = REAL_TO_INT_FLOOR(pos->x/PATHFIND_CELL_SIZE); + cell->y = REAL_TO_INT_FLOOR(pos->y/PATHFIND_CELL_SIZE); + Bool overflow = false; + if (cell->x < m_extent.lo.x) {overflow = true; cell->x = m_extent.lo.x;} + if (cell->y < m_extent.lo.y) {overflow = true; cell->y = m_extent.lo.y;} + if (cell->x > m_extent.hi.x) {overflow = true; cell->x = m_extent.hi.x;} + if (cell->y > m_extent.hi.y) {overflow = true; cell->y = m_extent.hi.y;} + return overflow; +} + +/** + * Return true if the given object ID is registered as an obstacle in this cell + */ +inline Bool PathfindCell::isObstaclePresent( ObjectID objID ) const +{ + if (objID != INVALID_ID && (getType() == PathfindCell::CELL_OBSTACLE)) + { + DEBUG_ASSERTCRASH(m_info, ("Should have info to be obstacle.")); + return (m_info && m_info->m_obstacleID == objID); + } + + return false; +} + + +#endif // _PATHFIND_H_ diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeployStyleAIUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeployStyleAIUpdate.h index 55bbff1b571..1c2a848980f 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeployStyleAIUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DeployStyleAIUpdate.h @@ -109,8 +109,8 @@ class DeployStyleAIUpdate : public AIUpdateInterface UnsignedInt getUnpackTime() const { return getDeployStyleAIUpdateModuleData()->m_unpackTime/ GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER; } UnsignedInt getPackTime() const { return getDeployStyleAIUpdateModuleData()->m_packTime/ GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER; } #else - UnsignedInt getUnpackTime() const { return getDeployStyleAIUpdateModuleData()->m_unpackTime / GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER; } - UnsignedInt getPackTime() const { return getDeployStyleAIUpdateModuleData()->m_packTime / GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER; } + UnsignedInt getUnpackTime() const { return getDeployStyleAIUpdateModuleData()->m_unpackTime; } + UnsignedInt getPackTime() const { return getDeployStyleAIUpdateModuleData()->m_packTime; } #endif Bool doTurretsFunctionOnlyWhenDeployed() const { return getDeployStyleAIUpdateModuleData()->m_turretsFunctionOnlyWhenDeployed; } Bool doTurretsHaveToCenterBeforePacking() const { return getDeployStyleAIUpdateModuleData()->m_turretsMustCenterBeforePacking; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h index d6a31229d77..b4774fe3fdb 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h @@ -109,6 +109,8 @@ class NetworkMesh } } + void Flush(); + void UpdateConnectivity(PlayerConnection* connection); std::function m_cbOnConnected = nullptr; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/NetworkDefs.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/NetworkDefs.h index d31cd83d169..c5a5775ad18 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/NetworkDefs.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/NetworkDefs.h @@ -66,7 +66,7 @@ static const Int MAX_PACKET_SIZE = 476; * to each player every frame */ #define MAX_MESSAGE_LEN 1024 -#define MAX_MESSAGES 128 +#define MAX_MESSAGES 1024 static const Int numCommandsPerCommandPacket = (MAX_MESSAGE_LEN - sizeof(UnsignedInt) - sizeof(UnsignedShort))/sizeof(GameMessage); #pragma pack(push, 1) struct CommandPacket diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index e16465aa3bc..4b36fe004c7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -1529,13 +1529,21 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) if (connState == EConnectionState::CONNECTED_DIRECT || connState == EConnectionState::CONNECTION_DISCONNECTED) { strConnectionMessage.format(L"Connection state to %hs changed to: %hs (mechanism: %hs | protocol: %hs)", strDisplayName.c_str(), strState.c_str(), strConnectionType.c_str(), connection->IsIPV4() ? "IPv4" : "IPv6"); + GadgetListBoxAddEntryText(listboxGameSetupChat, strConnectionMessage, GameMakeColor(255, 194, 15, 255), -1, -1); } else { - strConnectionMessage.format(L"Connection state to %hs changed to: %hs", strDisplayName.c_str(), strState.c_str()); +#if !defined(_DEBUG) + if (connState == EConnectionState::CONNECTION_FAILED) + { +#endif + strConnectionMessage.format(L"Connection state to %hs changed to: %hs", strDisplayName.c_str(), strState.c_str()); + GadgetListBoxAddEntryText(listboxGameSetupChat, strConnectionMessage, GameMakeColor(255, 194, 15, 255), -1, -1); + +#if !defined(_DEBUG) + } +#endif } - - GadgetListBoxAddEntryText(listboxGameSetupChat, strConnectionMessage, GameMakeColor(255, 194, 15, 255), -1, -1); }); // player doesnt have map events diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPathfind.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPathfind.cpp index 3cbd9a7eca4..233142db2a7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPathfind.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIPathfind.cpp @@ -142,7 +142,7 @@ void PathNode::setNextOptimized(PathNode *node) m_nextOptiDist2D = m_nextOptiDirNorm2D.length(); if (m_nextOptiDist2D == 0.0f) { - //DEBUG_LOG(("Warning - Path Seg length == 0, adjusting. john a.")); + //DEBUG_LOG(("Warning - Path Seg length == 0, adjusting. john a.")); m_nextOptiDist2D = 0.01f; } m_nextOptiDirNorm2D.x /= m_nextOptiDist2D; @@ -353,7 +353,7 @@ void Path::xfer( Xfer *xfer ) xfer->xferBool(&m_blockedByAlly); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI == AI_DEBUG_PATHS) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -420,7 +420,7 @@ void Path::appendNode( const Coord3D *pos, PathfindLayerEnum layer ) { /* Check for duplicates. */ if (pos->x == m_pathTail->getPosition()->x && pos->y == m_pathTail->getPosition()->y) { - DEBUG_LOG(("Warning - Path Seg length == 0, ignoring. john a.")); + DEBUG_LOG(("Warning - Path Seg length == 0, ignoring. john a.")); return; } } @@ -517,7 +517,7 @@ void Path::optimize( const Object *obj, LocomotorSurfaceTypeMask acceptableSurfa for( ; node != anchor; node = node->getPrevious() ) { Bool isPassable = false; - //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); + //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); if (TheAI->pathfinder()->isLinePassable( obj, acceptableSurfaces, layer, *anchor->getPosition(), *node->getPosition(), blocked, false)) { @@ -634,7 +634,7 @@ void Path::optimizeGroundPath( Bool crusher, Int pathDiameter ) for( ; node != anchor; node = node->getPrevious() ) { Bool isPassable = false; - //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); + //CRCDEBUG_LOG(("Path::optimize() calling isLinePassable()")); if (TheAI->pathfinder()->isGroundPathPassable( crusher, *anchor->getPosition(), layer, *node->getPosition(), pathDiameter)) { @@ -756,7 +756,7 @@ void Path::computePointOnPath( ClosestPointOnPathInfo& out ) { - CRCDEBUG_LOG(("Path::computePointOnPath() for %s", DebugDescribeObject(obj).str())); + CRCDEBUG_LOG(("Path::computePointOnPath() for %s", DebugDescribeObject(obj).str())); out.layer = LAYER_GROUND; out.posOnPath.zero(); @@ -773,7 +773,7 @@ void Path::computePointOnPath( { out = m_cpopOut; m_cpopCountdown--; - CRCDEBUG_LOG(("Path::computePointOnPath() end because we're really close")); + CRCDEBUG_LOG(("Path::computePointOnPath() end because we're really close")); return; } m_cpopCountdown = MAX_CPOP; @@ -926,7 +926,7 @@ void Path::computePointOnPath( k = 1.0f; Bool gotPos = false; - CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 1")); + CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 1")); if (TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), out.layer, pos, *nextNodePos, false, true )) { @@ -960,7 +960,7 @@ void Path::computePointOnPath( tryPos.x = (nextNodePos->x + next->getPosition()->x) * 0.5; tryPos.y = (nextNodePos->y + next->getPosition()->y) * 0.5; tryPos.z = nextNodePos->z; - CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 2")); + CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 2")); if (veryClose || TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), closeNext->getLayer(), pos, tryPos, false, true )) { gotPos = true; @@ -978,7 +978,7 @@ void Path::computePointOnPath( out.posOnPath.y = closeNodePos->y + tryDist * segmentDirNorm.y; out.posOnPath.z = closeNodePos->z; - CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 3")); + CRCDEBUG_LOG(("Path::computePointOnPath() calling isLinePassable() 3")); if (TheAI->pathfinder()->isLinePassable( obj, locomotorSet.getValidSurfaces(), out.layer, pos, out.posOnPath, false, true )) { k = 0.5f; @@ -1029,7 +1029,7 @@ void Path::computePointOnPath( m_cpopIn = pos; m_cpopOut = out; m_cpopValid = true; - CRCDEBUG_LOG(("Path::computePointOnPath() end")); + CRCDEBUG_LOG(("Path::computePointOnPath() end")); } @@ -1099,7 +1099,39 @@ Real Path::computeFlightDistToGoal( const Coord3D *pos, Coord3D& goalPos ) enum { PATHFIND_CELLS_PER_FRAME=5000}; // Number of cells we will search pathfinding per frame. enum {CELL_INFOS_TO_ALLOCATE = 30000}; PathfindCellInfo *PathfindCellInfo::s_infoArray = NULL; -PathfindCellInfo *PathfindCellInfo::s_firstFree = NULL; +PathfindCellInfo *PathfindCellInfo::s_firstFree = NULL; + +#if RETAIL_COMPATIBLE_PATHFINDING +// TheSuperHackers @info This variable is here so the code will run down the retail compatible path till a failure mode is hit +// The pathfinding will then switch over to the corrected pathfinding code for SH clients +Bool s_useFixedPathfinding = false; +Bool s_forceCleanCells = false; + +void PathfindCellInfo::forceCleanPathFindInfoCells() { + + for (Int i = 0; i < CELL_INFOS_TO_ALLOCATE - 1; i++) { + s_infoArray[i].m_nextOpen = NULL; + s_infoArray[i].m_prevOpen = NULL; + s_infoArray[i].m_open = FALSE; + s_infoArray[i].m_closed = FALSE; + } +} + +void Pathfinder::forceCleanCells() { + + for (int j = 0; j <= m_extent.hi.y; ++j) { + for (int i = 0; i <= m_extent.hi.x; ++i) { + if (m_map[ i ][ j ].hasInfo()) { + m_map[i][j].releaseInfo(); + } + } + } + +} +#else +Bool s_useFixedPathfinding = true; +#endif + /** * Allocates a pool of pathfind cell infos. */ @@ -1238,7 +1270,11 @@ Bool PathfindCell::startPathfind( PathfindCell *goalCell ) if (goalCell) { m_info->m_totalCost = costToGoal( goalCell ); } - m_info->m_open = TRUE; + if (s_useFixedPathfinding) { + m_info->m_open = FALSE; + } else { + m_info->m_open = TRUE; + } m_info->m_closed = FALSE; return true; } @@ -1292,6 +1328,14 @@ Bool PathfindCell::allocateInfo( const ICoord2D &pos ) */ void PathfindCell::releaseInfo( void ) { + // TheSuperHackers @bugfix Mauller/SkyAero 05/06/2025 Parent cell links need clearing to prevent dangling pointers on starting points that can link them to an invalid parent cell. + // Parent cells are only cleared within Pathfinder::prependCells, so cells that do not make it onto the final path do not get their parent cell cleared. + // Cells with a special flags also do not get their pathfindinfo cleared and therefore can leave a parent cell set on a starting cell. + if (s_useFixedPathfinding) { + if (m_info) { + m_info->m_pathParent = NULL; + } + } if (m_type==PathfindCell::CELL_OBSTACLE) { return; } @@ -1343,7 +1387,7 @@ void PathfindCell::setGoalUnit(ObjectID unitID, const ICoord2D &pos ) allocateInfo(pos); } if (!m_info) { - DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); + DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); return; } m_info->m_goalUnitID = unitID; @@ -1379,7 +1423,7 @@ void PathfindCell::setGoalAircraft(ObjectID unitID, const ICoord2D &pos ) allocateInfo(pos); } if (!m_info) { - DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); + DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); return; } m_info->m_goalAircraftID = unitID; @@ -1415,7 +1459,7 @@ void PathfindCell::setPosUnit(ObjectID unitID, const ICoord2D &pos ) allocateInfo(pos); } if (!m_info) { - DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); + DEBUG_CRASH(("Ran out of pathfind cells - fatal error!!!!! jba.")); return; } if (m_info->m_goalUnitID!=INVALID_ID && (m_info->m_goalUnitID==m_info->m_posUnitID)) { @@ -1586,9 +1630,22 @@ Int PathfindCell::releaseOpenList( PathfindCell *list ) DEBUG_ASSERTCRASH(list->m_info->m_closed==FALSE && list->m_info->m_open==TRUE, ("Serious error - Invalid flags. jba")); PathfindCell *cur = list; PathfindCellInfo *curInfo = list->m_info; + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding + // One crash mode is where a cell has no pathfindinfo, resulting in a nullptr access and a crash. + // Therefore we signal that we need to clean the maps pathing cell and the pathfindinfo cells + if(!curInfo && !s_useFixedPathfinding) { + list = NULL; + s_useFixedPathfinding = true; + s_forceCleanCells = true; + return count; + } +#endif //RETAIL_COMPATIBLE_PATHFINDING + if (curInfo->m_nextOpen) { list = curInfo->m_nextOpen->m_cell; - } else { + } else { list = NULL; } DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); @@ -1610,22 +1667,28 @@ Int PathfindCell::releaseClosedList( PathfindCell *list ) DEBUG_ASSERTCRASH(list->m_info->m_closed==TRUE && list->m_info->m_open==FALSE, ("Serious error - Invalid flags. jba")); PathfindCell *cur = list; PathfindCellInfo *curInfo = list->m_info; + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This is only here to catch a crash point in the retail compatible pathfinding + // One crash mode is where a cell has no pathfindinfo, resulting in a nullptr access and a crash. + // Therefore we signal that we need to clean the maps pathing cell and the pathfindinfo cells + if(!curInfo && !s_useFixedPathfinding) { + list = NULL; + s_useFixedPathfinding = true; + s_forceCleanCells = true; + return count; + } +#endif //RETAIL_COMPATIBLE_PATHFINDING - // NGMP_CHANGE: Safety check - // NGMP_BACKPORT: Port this to SH once verified - if (curInfo != nullptr) - { - if (curInfo->m_nextOpen) { - list = curInfo->m_nextOpen->m_cell; - } - else { - list = NULL; - } - DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); - curInfo->m_nextOpen = NULL; - curInfo->m_prevOpen = NULL; - curInfo->m_closed = FALSE; + if (curInfo->m_nextOpen) { + list = curInfo->m_nextOpen->m_cell; + } else { + list = NULL; } + DEBUG_ASSERTCRASH(cur == curInfo->m_cell, ("Bad backpointer in PathfindCellInfo")); + curInfo->m_nextOpen = NULL; + curInfo->m_prevOpen = NULL; + curInfo->m_closed = FALSE; cur->releaseInfo(); } return count; @@ -1743,6 +1806,16 @@ UnsignedInt PathfindCell::costSoFar( PathfindCell *parent ) Int numTurns = 0; PathfindCell *prevCell = parent->getParentCell(); if (prevCell) { + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info this is a possible crash point in the retail pathfinding, we just prevent the crash at this point + // External code should catch the issue in another block and cleanup the pathfinding before switching to the fixed pathfinding. + if (!prevCell->hasInfo()) + { + return cost; + } +#endif + ICoord2D dir; dir.x = prevCell->getXIndex() - parent->getXIndex(); dir.y = prevCell->getYIndex() - parent->getYIndex(); @@ -2236,7 +2309,7 @@ void PathfindZoneManager::allocateZones(void) while (m_zonesAllocated <= m_maxZone) { m_zonesAllocated *= 2; } - DEBUG_LOG(("Allocating zone tables of size %d", m_zonesAllocated)); + DEBUG_LOG(("Allocating zone tables of size %d", m_zonesAllocated)); // pool[]ify m_groundCliffZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; m_groundWaterZones = MSGNEW("PathfindZoneInfo") zoneStorageType[m_zonesAllocated]; @@ -2376,13 +2449,13 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye } } - //DEBUG_LOG(("Collapsed zones %d", m_maxZone)); + //DEBUG_LOG(("Collapsed zones %d", m_maxZone)); } } Int totalZones = m_maxZone; // if (totalZones>maxZones/2) { -// DEBUG_LOG(("Max zones %d", m_maxZone)); +// DEBUG_LOG(("Max zones %d", m_maxZone)); // } // Collapse the zones into a 1,2,3... sequence, removing collapsed zones. @@ -2424,7 +2497,7 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye //#if defined(DEBUG_LOGGING) // QueryPerformanceCounter((LARGE_INTEGER *)&endTime64); // timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); -// DEBUG_LOG(("Time to calculate first %f", timeToUpdate)); +// DEBUG_LOG(("Time to calculate first %f", timeToUpdate)); //#endif //#endif @@ -2555,7 +2628,7 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye //#if defined(DEBUG_LOGGING) // QueryPerformanceCounter((LARGE_INTEGER *)&endTime64); // timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); -// DEBUG_LOG(("Time to calculate second %f", timeToUpdate)); +// DEBUG_LOG(("Time to calculate second %f", timeToUpdate)); //#endif //#endif @@ -2602,7 +2675,7 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye //// Int zone1 = map[i][j].getZone(); //// Int zone2 = map[i-1][j].getZone(); //// if (m_terrainZones[zone1] != m_terrainZones[zone2]) { -//// //DEBUG_LOG(("Matching terrain zone %d to %d.", zone1, zone2)); +//// //DEBUG_LOG(("Matching terrain zone %d to %d.", zone1, zone2)); //// } // applyZone(map[i][j], map[i-1][j], m_groundRubbleZones, m_maxZone); // } @@ -2627,7 +2700,7 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye //// Int zone1 = map[i][j].getZone(); //// Int zone2 = map[i][j-1].getZone(); //// if (m_terrainZones[zone1] != m_terrainZones[zone2]) { -//// //DEBUG_LOG(("Matching terrain zone %d to %d.", zone1, zone2)); +//// //DEBUG_LOG(("Matching terrain zone %d to %d.", zone1, zone2)); //// } // applyZone(map[i][j], map[i][j-1], m_groundRubbleZones, m_maxZone); // } @@ -2781,17 +2854,17 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye QueryPerformanceCounter((LARGE_INTEGER *)&endTime64); timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); -// DEBUG_LOG(("Time to calculate zones %f, cells %d", timeToUpdate, (globalBounds.hi.x-globalBounds.lo.x)*(globalBounds.hi.y-globalBounds.lo.y))); +// DEBUG_LOG(("Time to calculate zones %f, cells %d", timeToUpdate, (globalBounds.hi.x-globalBounds.lo.x)*(globalBounds.hi.y-globalBounds.lo.y))); if ( updateSamples < 400 ) { averageTimeToUpdate = ((averageTimeToUpdate * updateSamples) + timeToUpdate) / (updateSamples + 1.0f); updateSamples++; - DEBUG_LOG(("computing...: %f", averageTimeToUpdate)); + DEBUG_LOG(("computing...: %f", averageTimeToUpdate)); } else if ( updateSamples == 400 ) { - DEBUG_LOG((" =============DONE============= Average time to calculate zones: %f", averageTimeToUpdate)); - DEBUG_LOG((" Percent of baseline : %f", averageTimeToUpdate/0.003335f)); + DEBUG_LOG((" =============DONE============= Average time to calculate zones: %f", averageTimeToUpdate)); + DEBUG_LOG((" Percent of baseline : %f", averageTimeToUpdate/0.003335f)); updateSamples = 777; #ifdef forceRefreshCalling s_stopForceCalling = TRUE; @@ -2800,7 +2873,7 @@ void PathfindZoneManager::calculateZones( PathfindCell **map, PathfindLayer laye #endif #endif -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI == AI_DEBUG_ZONES) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -2935,17 +3008,17 @@ void PathfindZoneManager::updateZonesForModify(PathfindCell **map, PathfindLayer } } } - //DEBUG_LOG(("Collapsed zones %d", m_maxZone)); + //DEBUG_LOG(("Collapsed zones %d", m_maxZone)); } } #ifdef DEBUG_QPF #if defined(DEBUG_LOGGING) QueryPerformanceCounter((LARGE_INTEGER *)&endTime64); timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); - //DEBUG_LOG(("Time to update zones %f, cells %d", timeToUpdate, (globalBounds.hi.x-globalBounds.lo.x)*(globalBounds.hi.y-globalBounds.lo.y))); + //DEBUG_LOG(("Time to update zones %f, cells %d", timeToUpdate, (globalBounds.hi.x-globalBounds.lo.x)*(globalBounds.hi.y-globalBounds.lo.y))); #endif #endif -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI==AI_DEBUG_ZONES) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -3269,7 +3342,7 @@ Bool PathfindLayer::isUnused(void) /** * Draws debug cell info. */ -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) void PathfindLayer::doDebugIcons(void) { if (isUnused()) return; extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -3843,7 +3916,7 @@ Pathfinder::~Pathfinder( void ) void Pathfinder::reset( void ) { frameToShowObstacles = 0; - DEBUG_LOG(("Pathfind cell is %d bytes, PathfindCellInfo is %d bytes", sizeof(PathfindCell), sizeof(PathfindCellInfo))); + DEBUG_LOG(("Pathfind cell is %d bytes, PathfindCellInfo is %d bytes", sizeof(PathfindCell), sizeof(PathfindCellInfo))); if (m_blockOfMapCells) { delete []m_blockOfMapCells; @@ -3904,6 +3977,11 @@ void Pathfinder::reset( void ) m_wallHeight = 0.0f; } m_zoneManager.reset(); + +#if RETAIL_COMPATIBLE_PATHFINDING + s_useFixedPathfinding = false; + s_forceCleanCells = false; +#endif } /** @@ -3979,7 +4057,7 @@ PathfindLayerEnum Pathfinder::addBridge(Bridge *theBridge) if (m_layers[layer].init(theBridge, (PathfindLayerEnum)layer) ) { return (PathfindLayerEnum)layer; } - DEBUG_LOG(("WARNING: Bridge failed to init in pathfinder")); + DEBUG_LOG(("WARNING: Bridge failed to init in pathfinder")); return LAYER_GROUND; // failed to init, usually cause off of the map. jba. } layer++; @@ -3998,7 +4076,7 @@ void Pathfinder::updateLayer(Object *obj, PathfindLayerEnum layer) layer = LAYER_GROUND; } } - //DEBUG_LOG(("Object layer is %d", layer)); + //DEBUG_LOG(("Object layer is %d", layer)); obj->setLayer(layer); } @@ -4042,10 +4120,6 @@ void Pathfinder::classifyFence( Object *obj, Bool insert ) #endif Bool didAnything = false; - // NGMP_CHANGE: Safety - cellBounds.hi.x = 9999999; - cellBounds.hi.y = 9999999; - for (Int iy = 0; iy < numStepsY; ++iy, tl_x += ydx, tl_y += ydy) { Real x = tl_x; @@ -4707,7 +4781,7 @@ void Pathfinder::debugShowSearch( Bool pathFound ) if (!TheGlobalData->m_debugAI) { return; } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); // show all explored cells for debugging @@ -4827,11 +4901,35 @@ void Pathfinder::cleanOpenAndClosedLists(void) { if (m_openList) { count += PathfindCell::releaseOpenList(m_openList); m_openList = NULL; - } + } + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info this is here as the map cells are contained within the pathfinder and cannot be cleaned externally. + // If the crash mode within PathfinCell::releaseOpenList is hit, it will set s_forceCleanCells to allow the system to cleanly recover. + if (s_forceCleanCells) { + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + // TheSuperHackers @info cells on the closed list are forcefully cleaned up by this point + m_closedList = NULL; + s_forceCleanCells = false; + } +#endif + if (m_closedList) { count += PathfindCell::releaseClosedList(m_closedList); m_closedList = NULL; - } + } + +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info this is here and performs the same function as the above block, but for when the crash occurs within the closed list. + // If the crash mode within PathfinCell::releaseClosedList is hit, it will set s_forceCleanCells to allow the system to cleanly recover. + if (s_forceCleanCells) { + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + s_forceCleanCells = false; + } +#endif + m_cumulativeCellsAllocated += count; //#ifdef RTS_DEBUG #if 0 @@ -4848,12 +4946,12 @@ void Pathfinder::cleanOpenAndClosedLists(void) { needInfo = true; } if (!needInfo) { - DEBUG_LOG(("leaked cell %d, %d", m_map[i][j].getXIndex(), m_map[i][j].getYIndex())); + DEBUG_LOG(("leaked cell %d, %d", m_map[i][j].getXIndex(), m_map[i][j].getYIndex())); m_map[i][j].releaseInfo(); } DEBUG_ASSERTCRASH((needInfo), ("Minor temporary memory leak - Extra cell allocated. Tell JBA steps if repeatable.")); }; - //DEBUG_LOG(("Pathfind used %d cells.", count)); + //DEBUG_LOG(("Pathfind used %d cells.", count)); #endif //#endif @@ -5189,7 +5287,7 @@ void Pathfinder::snapClosestGoalPosition(Object *obj, Coord3D *pos) } } } - //DEBUG_LOG(("Couldn't find goal.")); + //DEBUG_LOG(("Couldn't find goal.")); } /** @@ -5438,7 +5536,7 @@ Bool Pathfinder::adjustDestination(Object *obj, const LocomotorSet& locomotorSet // Didn't work, so just do simple adjust. return(adjustDestination(obj, locomotorSet, dest, NULL)); } - //DEBUG_LOG(("adjustDestination failed, dest (%f, %f), adj dest (%f,%f), %x %s", dest->x, dest->y, adjustDest.x, adjustDest.y, + //DEBUG_LOG(("adjustDestination failed, dest (%f, %f), adj dest (%f,%f), %x %s", dest->x, dest->y, adjustDest.x, adjustDest.y, //obj, obj->getTemplate()->getName().str())); return false; } @@ -5667,7 +5765,7 @@ Bool Pathfinder::queueForPath(ObjectID id) AIUpdateInterface *tmpAI = tmpObj->getAIUpdateInterface(); if (tmpAI) { const Coord3D* pos = tmpAI->friend_getRequestedDestination(); - DEBUG_ASSERTLOG(pos->x != 0.0 && pos->y != 0.0, ("Queueing pathfind to (0, 0), usually a bug. (Unit Name: '%s', Type: '%s')", tmpObj->getName().str(), tmpObj->getTemplate()->getName().str())); + DEBUG_ASSERTLOG(pos->x != 0.0 && pos->y != 0.0, ("Queueing pathfind to (0, 0), usually a bug. (Unit Name: '%s', Type: '%s')", tmpObj->getName().str(), tmpObj->getTemplate()->getName().str())); } } } @@ -5699,7 +5797,7 @@ Bool Pathfinder::queueForPath(ObjectID id) return true; } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) void Pathfinder::doDebugIcons(void) { const Int FRAMES_TO_SHOW_OBSTACLES = 100; extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -5962,13 +6060,13 @@ void Pathfinder::processPathfindQueue(void) timeToUpdate = ((double)(endTime64-startTime64) / (double)(freq64)); if (timeToUpdate>0.01f) { - DEBUG_LOG(("%d Pathfind queue: %d paths, %d cells --", TheGameLogic->getFrame(), pathsFound, m_cumulativeCellsAllocated)); - DEBUG_LOG(("time %f (%f)", timeToUpdate, (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("%d Pathfind queue: %d paths, %d cells --", TheGameLogic->getFrame(), pathsFound, m_cumulativeCellsAllocated)); + DEBUG_LOG(("time %f (%f)", timeToUpdate, (::GetTickCount()-startTimeMS)/1000.0f)); } #endif #endif } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) doDebugIcons(); #endif @@ -6108,7 +6206,7 @@ struct ExamineCellsStruct to->setParentCell(from) ; to->setTotalCost(to->getCostSoFar() + costRemaining) ; - //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", + //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", // to->getXIndex(), to->getYIndex(), // to->costSoFar(from), newCostSoFar, costRemaining, to->getCostSoFar() + costRemaining)); @@ -6374,7 +6472,7 @@ Int Pathfinder::examineNeighboringCells(PathfindCell *parentCell, PathfindCell * } newCell->setTotalCost(newCell->getCostSoFar() + costRemaining) ; - //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", + //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", // newCell->getXIndex(), newCell->getYIndex(), // newCell->costSoFar(parentCell), newCostSoFar, costRemaining, newCell->getCostSoFar() + costRemaining)); @@ -6440,12 +6538,12 @@ Path *Pathfinder::findPath( Object *obj, const LocomotorSet& locomotorSet, const Path *linkPath = findClosestPath(obj, locomotorSet, node->getPosition(), prior->getPosition(), false, 0, true); if (linkPath==NULL) { - DEBUG_LOG(("Couldn't find path - unexpected. jba.")); + DEBUG_LOG(("Couldn't find path - unexpected. jba.")); continue; } PathNode *linkNode = linkPath->getLastNode(); if (linkNode==NULL) { - DEBUG_LOG(("Empty path - unexpected. jba.")); + DEBUG_LOG(("Empty path - unexpected. jba.")); continue; } for (linkNode=linkNode->getPrevious(); linkNode; linkNode=linkNode->getPrevious()) { @@ -6474,9 +6572,9 @@ Path *Pathfinder::findPath( Object *obj, const LocomotorSet& locomotorSet, const Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *rawTo) { - //CRCDEBUG_LOG(("Pathfinder::findPath()")); + //CRCDEBUG_LOG(("Pathfinder::findPath()")); #ifdef INTENSE_DEBUG - DEBUG_LOG(("internal find path...")); + DEBUG_LOG(("internal find path...")); #endif #ifdef DEBUG_LOGGING @@ -6494,7 +6592,7 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe } if (rawTo->x == 0.0f && rawTo->y == 0.0f) { - DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug.")); + DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug.")); return NULL; } DEBUG_ASSERTCRASH(m_openList==NULL && m_closedList == NULL, ("Dangling lists.")); @@ -6570,10 +6668,18 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe zone2 = m_zoneManager.getEffectiveZone(locomotorSet.getValidSurfaces(), isCrusher, goalCell->getZone()); if (layer==LAYER_WALL && zone1 == 0) { + if (s_useFixedPathfinding) { + goalCell->releaseInfo(); + parentCell->releaseInfo(); + } return NULL; } if (destinationLayer==LAYER_WALL && zone2 == 0) { + if (s_useFixedPathfinding) { + goalCell->releaseInfo(); + parentCell->releaseInfo(); + } return NULL; } @@ -6585,10 +6691,10 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe zone1 = m_zoneManager.getEffectiveZone(locomotorSet.getValidSurfaces(), isCrusher, zone1); } - //DEBUG_LOG(("Zones %d to %d", zone1, zone2)); + //DEBUG_LOG(("Zones %d to %d", zone1, zone2)); if ( zone1 != zone2) { - //DEBUG_LOG(("Intense Debug Info - Pathfind Zone screen failed-cannot reach desired location.")); + //DEBUG_LOG(("Intense Debug Info - Pathfind Zone screen failed-cannot reach desired location.")); goalCell->releaseInfo(); parentCell->releaseInfo(); return NULL; @@ -6633,15 +6739,15 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe // success - found a path to the goal Bool show = TheGlobalData->m_debugAI==AI_DEBUG_PATHS; #ifdef INTENSE_DEBUG - DEBUG_LOG(("internal find path SUCCESS...")); + DEBUG_LOG(("internal find path SUCCESS...")); Int count = 0; if (cellCount>1000 && obj) { show = true; - DEBUG_LOG(("cells %d obj %s %x from (%f,%f) to(%f, %f)", count, obj->getTemplate()->getName().str(), obj, from->x, from->y, to->x, to->y)); + DEBUG_LOG(("cells %d obj %s %x from (%f,%f) to(%f, %f)", count, obj->getTemplate()->getName().str(), obj, from->x, from->y, to->x, to->y)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif TheScriptEngine->AppendDebugMessage("Big path", false); @@ -6653,8 +6759,14 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe m_isTunneling = false; // construct and return path Path *path = buildActualPath( obj, locomotorSet.getValidSurfaces(), from, goalCell, centerInCell, false ); - parentCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + } + else { + parentCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return path; } @@ -6669,9 +6781,9 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe } // failure - goal cannot be reached -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) #ifdef INTENSE_DEBUG - DEBUG_LOG(("internal find path FAILURE...")); + DEBUG_LOG(("internal find path FAILURE...")); #endif if (TheGlobalData->m_debugAI == AI_DEBUG_PATHS) { @@ -6712,7 +6824,7 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("state %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("state %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif } @@ -6724,14 +6836,21 @@ Path *Pathfinder::internalFindPath( Object *obj, const LocomotorSet& locomotorSe Bool valid; valid = validMovementPosition( isCrusher, obj->getLayer(), locomotorSet, to ) ; - DEBUG_LOG(("%d Pathfind failed from (%f,%f) to (%f,%f), OV %d --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y, valid)); - DEBUG_LOG(("Unit '%s', time %f, cells %d", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f,cellCount)); + DEBUG_LOG(("%d Pathfind failed from (%f,%f) to (%f,%f), OV %d --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y, valid)); + DEBUG_LOG(("Unit '%s', time %f, cells %d", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f,cellCount)); } #endif m_isTunneling = false; - goalCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } + else { + goalCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return NULL; } @@ -6816,7 +6935,7 @@ Path *Pathfinder::buildGroundPath(Bool isCrusher, const Coord3D *fromPos, Pathfi path->optimizeGroundPath( isCrusher, pathDiameter ); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI==AI_DEBUG_GROUND_PATHS) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -6880,7 +6999,7 @@ Path *Pathfinder::buildHierachicalPath( const Coord3D *fromPos, PathfindCell *go } } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI==AI_DEBUG_PATHS) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -6943,7 +7062,7 @@ struct MADStruct { return 0; // Packing or unpacking objects for example } - //DEBUG_LOG(("Moving ally")); + //DEBUG_LOG(("Moving ally")); otherObj->getAI()->aiMoveAwayFromUnit(d->obj, CMD_FROM_AI); } } @@ -7023,7 +7142,7 @@ struct GroundCellsStruct Path *Pathfinder::findGroundPath( const Coord3D *from, const Coord3D *rawTo, Int pathDiameter, Bool crusher) { - //CRCDEBUG_LOG(("Pathfinder::findGroundPath()")); + //CRCDEBUG_LOG(("Pathfinder::findGroundPath()")); #ifdef DEBUG_LOGGING Int startTimeMS = ::GetTickCount(); #endif @@ -7043,7 +7162,7 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, } if (rawTo->x == 0.0f && rawTo->y == 0.0f) { - DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug.")); + DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug.")); return NULL; } DEBUG_ASSERTCRASH(m_openList==NULL && m_closedList == NULL, ("Dangling lists.")); @@ -7107,6 +7226,9 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, PathfindLayerEnum layer = TheTerrainLogic->getLayerForDestination(from); PathfindCell *parentCell = getClippedCell( layer,&clipFrom ); if (parentCell == NULL) { + if (s_useFixedPathfinding) { + goalCell->releaseInfo(); + } return NULL; } if (parentCell!=goalCell) { @@ -7123,7 +7245,7 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, zone1 = m_zoneManager.getEffectiveZone(LOCOMOTORSURFACE_GROUND, false, parentCell->getZone()); zone2 = m_zoneManager.getEffectiveZone(LOCOMOTORSURFACE_GROUND, false, goalCell->getZone()); - //DEBUG_LOG(("Zones %d to %d", zone1, zone2)); + //DEBUG_LOG(("Zones %d to %d", zone1, zone2)); if ( zone1 != zone2) { goalCell->releaseInfo(); @@ -7159,9 +7281,9 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, // success - found a path to the goal #ifdef INTENSE_DEBUG DEBUG_LOG((" time %d msec %d cells", (::GetTickCount()-startTimeMS), cellCount)); - DEBUG_LOG((" SUCCESS")); + DEBUG_LOG((" SUCCESS")); #endif -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) Bool show = TheGlobalData->m_debugAI==AI_DEBUG_GROUND_PATHS; if (show) debugShowSearch(true); @@ -7169,8 +7291,14 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, m_isTunneling = false; // construct and return path Path *path = buildGroundPath(crusher, from, goalCell, centerInCell, pathDiameter ); - parentCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + } + else { + parentCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return path; } @@ -7305,13 +7433,13 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, } //DEBUG_LOG(("CELL(%d,%d)L%d CD%d CSF %d, CR%d // ",newCell->getXIndex(), newCell->getYIndex(), // newCell->getLayer(), clearDiameter, newCostSoFar, costRemaining)); - //if ((cellCount&7)==0) DEBUG_LOG_RAW(("\n")); + //if ((cellCount&7)==0) DEBUG_LOG_RAW(("\n")); newCell->setCostSoFar(newCostSoFar); // keep track of path we're building - point back to cell we moved here from newCell->setParentCell(parentCell) ; newCell->setTotalCost(newCell->getCostSoFar() + costRemaining) ; - //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", + //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", // newCell->getXIndex(), newCell->getYIndex(), // newCell->costSoFar(parentCell), newCostSoFar, costRemaining, newCell->getCostSoFar() + costRemaining)); @@ -7331,9 +7459,9 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, } // failure - goal cannot be reached #ifdef INTENSE_DEBUG - DEBUG_LOG((" FAILURE")); + DEBUG_LOG((" FAILURE")); #endif -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -7367,15 +7495,22 @@ Path *Pathfinder::findGroundPath( const Coord3D *from, } #endif - DEBUG_LOG(("%d FindGroundPath failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y)); - DEBUG_LOG(("time %f", (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("%d FindGroundPath failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y)); + DEBUG_LOG(("time %f", (::GetTickCount()-startTimeMS)/1000.0f)); #ifdef DUMP_PERF_STATS TheGameLogic->incrementOverallFailedPathfinds(); #endif m_isTunneling = false; - goalCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } + else { + goalCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return NULL; } @@ -7439,8 +7574,14 @@ void Pathfinder::processHierarchicalCell( const ICoord2D &scanCell, const ICoord } newCell->allocateInfo(scanCell); - if (!newCell->getClosed() && !newCell->getOpen()) { - m_closedList = newCell->putOnClosedList(m_closedList); + if (s_useFixedPathfinding) { + if (newCell->hasInfo() && !newCell->getClosed() && !newCell->getOpen()) { + m_closedList = newCell->putOnClosedList(m_closedList); + } + } else { + if (!newCell->getClosed() && !newCell->getOpen()) { + m_closedList = newCell->putOnClosedList(m_closedList); + } } adjNewCell->allocateInfo(adjacentCell); @@ -7498,13 +7639,13 @@ Path *Pathfinder::findClosestHierarchicalPath( Bool isHuman, const LocomotorSet& Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSurfaceTypeMask locomotorSurface, const Coord3D *from, const Coord3D *rawTo, Bool crusher, Bool closestOK) { - //CRCDEBUG_LOG(("Pathfinder::findGroundPath()")); + //CRCDEBUG_LOG(("Pathfinder::findGroundPath()")); #ifdef DEBUG_LOGGING Int startTimeMS = ::GetTickCount(); #endif if (rawTo->x == 0.0f && rawTo->y == 0.0f) { - DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug.")); + DEBUG_LOG(("Attempting pathfind to 0,0, generally a bug.")); return NULL; } DEBUG_ASSERTCRASH(m_openList==NULL && m_closedList == NULL, ("Dangling lists.")); @@ -7597,7 +7738,24 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu // Close parent cell; m_openList = parentCell->removeFromOpenList(m_openList); m_closedList = parentCell->putOnClosedList(m_closedList); - startCell->allocateInfo(ndx); + if (!startCell->allocateInfo(ndx)) { + //TheSuperHackers @info We need to forcefully cleanup dangling pathfinding cells if this failure condition is hit in retail + //Retail clients will crash beyond this point, but we attempt to recover by performing a full cleanup then enabling the fixed pathfinding codepath + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + goalCell->releaseInfo(); + } +#if RETAIL_COMPATIBLE_PATHFINDING + else { + s_useFixedPathfinding = true; + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + m_openList = NULL; + m_closedList = NULL; + } +#endif + return NULL; + } startCell->setParentCellHierarchical(parentCell); cellCount++; Int curCost = startCell->costToHierGoal(parentCell); @@ -7609,7 +7767,24 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu m_openList = startCell->putOnSortedOpenList( m_openList ); cellCount++; - cell->allocateInfo(toNdx); + if(!cell->allocateInfo(toNdx)) { + //TheSuperHackers @info We need to forcefully cleanup dangling pathfinding cells if this failure condition is hit in retail + //Retail clients will crash beyond this point, but we attempt to recover by performing a full cleanup then enabling the fixed pathfinding codepath + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + goalCell->releaseInfo(); + } +#if RETAIL_COMPATIBLE_PATHFINDING + else { + s_useFixedPathfinding = true; + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + m_openList = NULL; + m_closedList = NULL; + } +#endif + return NULL; + } curCost = cell->costToHierGoal(parentCell); remCost = cell->costToHierGoal(goalCell); cell->setCostSoFar(curCost); @@ -7693,13 +7868,47 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu PathfindCell *startCell = getCell(LAYER_GROUND, ndx.x, ndx.y); if (startCell==NULL) continue; if (startCell != parentCell) { - startCell->allocateInfo(ndx); + if(!startCell->allocateInfo(ndx)) { + //TheSuperHackers @info We need to forcefully cleanup dangling pathfinding cells if this failure condition is hit in retail + //Retail clients will crash beyond this point, but we attempt to recover by performing a full cleanup then enabling the fixed pathfinding codepath + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + goalCell->releaseInfo(); + } +#if RETAIL_COMPATIBLE_PATHFINDING + else { + s_useFixedPathfinding = true; + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + m_openList = NULL; + m_closedList = NULL; + } +#endif + return NULL; + } startCell->setParentCellHierarchical(parentCell); if (!startCell->getClosed() && !startCell->getOpen()) { m_closedList = startCell->putOnClosedList(m_closedList); } } - cell->allocateInfo(toNdx); + if(!cell->allocateInfo(toNdx)) { + //TheSuperHackers @info We need to forcefully cleanup dangling pathfinding cells if this failure condition is hit in retail + //Retail clients will crash beyond this point, but we attempt to recover by performing a full cleanup then enabling the fixed pathfinding codepath + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + goalCell->releaseInfo(); + } +#if RETAIL_COMPATIBLE_PATHFINDING + else { + s_useFixedPathfinding = true; + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + m_openList = NULL; + m_closedList = NULL; + } +#endif + return NULL; + } cell->setParentCellHierarchical(startCell); cellCount++; @@ -7726,7 +7935,7 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu m_isTunneling = false; // construct and return path Path *path = buildHierachicalPath( from, goalCell ); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) Bool show = TheGlobalData->m_debugAI==AI_DEBUG_PATHS; show |= (TheGlobalData->m_debugAI==AI_DEBUG_GROUND_PATHS); if (show) { @@ -7753,15 +7962,21 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu #endif } #endif - if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); goalCell->releaseInfo(); + } else { + if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + goalCell->releaseInfo(); + } + parentCell->releaseInfo(); + cleanOpenAndClosedLists(); } - parentCell->releaseInfo(); - cleanOpenAndClosedLists(); return path; } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) #if 0 Bool show = TheGlobalData->m_debugAI==AI_DEBUG_PATHS; show |= (TheGlobalData->m_debugAI==AI_DEBUG_GROUND_PATHS); @@ -7928,7 +8143,7 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu m_isTunneling = false; // construct and return path Path *path = buildHierachicalPath( from, closestCell ); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) #if 0 if (TheGlobalData->m_debugAI) { @@ -7954,15 +8169,21 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu } #endif #endif - if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); goalCell->releaseInfo(); + } else { + if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + goalCell->releaseInfo(); + } + cleanOpenAndClosedLists(); } - cleanOpenAndClosedLists(); return path; } // failure - goal cannot be reached -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -7996,15 +8217,22 @@ Path *Pathfinder::internal_findHierarchicalPath( Bool isHuman, const LocomotorSu } #endif - DEBUG_LOG(("%d FindHierarchicalPath failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y)); - DEBUG_LOG(("time %f", (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("%d FindHierarchicalPath failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y)); + DEBUG_LOG(("time %f", (::GetTickCount()-startTimeMS)/1000.0f)); #ifdef DUMP_PERF_STATS TheGameLogic->incrementOverallFailedPathfinds(); #endif m_isTunneling = false; - goalCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } else { + goalCell->releaseInfo(); + cleanOpenAndClosedLists(); + } + return NULL; } @@ -8218,7 +8446,7 @@ void Pathfinder::clip( Coord3D *from, Coord3D *to ) Bool Pathfinder::pathDestination( Object *obj, const LocomotorSet& locomotorSet, Coord3D *dest, PathfindLayerEnum layer, const Coord3D *groupDest) { - //CRCDEBUG_LOG(("Pathfinder::pathDestination()")); + //CRCDEBUG_LOG(("Pathfinder::pathDestination()")); if (m_isMapReady == false) return NULL; if (!obj) return false; @@ -8268,7 +8496,9 @@ Bool Pathfinder::pathDestination( Object *obj, const LocomotorSet& locomotorSet if (parentCell!=goalCell) { if (!parentCell->allocateInfo(startCellNdx)) { - desiredCell->releaseInfo(); + if (!s_useFixedPathfinding) { + desiredCell->releaseInfo(); + } goalCell->releaseInfo(); return FALSE; } @@ -8383,6 +8613,10 @@ Bool Pathfinder::pathDestination( Object *obj, const LocomotorSet& locomotorSet if (!newCell->allocateInfo(newCellCoord)) { // Out of cells for pathing... + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + goalCell->releaseInfo(); + } return cellCount; } cellCount++; @@ -8413,7 +8647,7 @@ Bool Pathfinder::pathDestination( Object *obj, const LocomotorSet& locomotorSet newCell->setTotalCost(newCell->getCostSoFar() + costRemaining) ; - //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", + //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", // newCell->getXIndex(), newCell->getYIndex(), // newCell->costSoFar(parentCell), newCostSoFar, costRemaining, newCell->getCostSoFar() + costRemaining)); @@ -8430,7 +8664,7 @@ Bool Pathfinder::pathDestination( Object *obj, const LocomotorSet& locomotorSet } } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (closestCell) { debugShowSearch(true); *dest = closestPos; @@ -8439,8 +8673,15 @@ Bool Pathfinder::pathDestination( Object *obj, const LocomotorSet& locomotorSet } #endif m_isTunneling = false; - cleanOpenAndClosedLists(); - goalCell->releaseInfo(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } else { + cleanOpenAndClosedLists(); + goalCell->releaseInfo(); + } + return false; } @@ -8496,7 +8737,7 @@ void Pathfinder::tightenPath(Object *obj, const LocomotorSet& locomotorSet, Coor Int Pathfinder::checkPathCost(Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D *rawTo) { - //CRCDEBUG_LOG(("Pathfinder::checkPathCost()")); + //CRCDEBUG_LOG(("Pathfinder::checkPathCost()")); if (m_isMapReady == false) return NULL; enum {MAX_COST = 0x7fff0000}; if (!obj) return MAX_COST; @@ -8566,16 +8807,26 @@ Int Pathfinder::checkPathCost(Object *obj, const LocomotorSet& locomotorSet, con parentCell = m_openList; m_openList = parentCell->removeFromOpenList(m_openList); - // put parent cell onto closed list - its evaluation is finished - m_closedList = parentCell->putOnClosedList( m_closedList ); + // put parent cell onto closed list - its evaluation is finished - Retail compatible behaviour + if (!s_useFixedPathfinding) { + m_closedList = parentCell->putOnClosedList( m_closedList ); + } if (parentCell==goalCell) { Int cost = parentCell->getTotalCost(); m_isTunneling = false; cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + parentCell->releaseInfo(); + } return cost; } + // put parent cell onto closed list - its evaluation is finished - Fixed behaviour + if (s_useFixedPathfinding) { + m_closedList = parentCell->putOnClosedList( m_closedList ); + } + if (cellCount > MAX_CELL_COUNT) { continue; } @@ -8636,6 +8887,11 @@ Int Pathfinder::checkPathCost(Object *obj, const LocomotorSet& locomotorSet, con if (!newCell->allocateInfo(newCellCoord)) { // Out of cells for pathing... + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } return cellCount; } cellCount++; @@ -8666,7 +8922,7 @@ Int Pathfinder::checkPathCost(Object *obj, const LocomotorSet& locomotorSet, con newCell->setTotalCost(newCell->getCostSoFar() + costRemaining) ; - //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", + //DEBUG_LOG(("Cell (%d,%d), Parent cost %d, newCostSoFar %d, cost rem %d, tot %d", // newCell->getXIndex(), newCell->getYIndex(), // newCell->costSoFar(parentCell), newCostSoFar, costRemaining, newCell->getCostSoFar() + costRemaining)); @@ -8684,10 +8940,16 @@ Int Pathfinder::checkPathCost(Object *obj, const LocomotorSet& locomotorSet, con } m_isTunneling = false; - if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); goalCell->releaseInfo(); + } else { + if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + goalCell->releaseInfo(); + } + cleanOpenAndClosedLists(); } - cleanOpenAndClosedLists(); return MAX_COST; } @@ -8701,7 +8963,7 @@ Int Pathfinder::checkPathCost(Object *obj, const LocomotorSet& locomotorSet, con Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, Coord3D *rawTo, Bool blocked, Real pathCostMultiplier, Bool moveAllies) { - //CRCDEBUG_LOG(("Pathfinder::findClosestPath()")); + //CRCDEBUG_LOG(("Pathfinder::findClosestPath()")); #ifdef DEBUG_LOGGING Int startTimeMS = ::GetTickCount(); #endif @@ -8824,6 +9086,9 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet if (parentCell!=goalCell) { worldToCell(&clipFrom, &pos2d); if (!parentCell->allocateInfo(pos2d)) { + if (s_useFixedPathfinding) { + goalCell->releaseInfo(); + } return NULL; } } @@ -8874,11 +9139,11 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet } if (count>1000) { show = true; - DEBUG_LOG(("FCP - cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); + DEBUG_LOG(("FCP - cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif TheScriptEngine->AppendDebugMessage("Big path FCP", false); @@ -8889,9 +9154,16 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet m_isTunneling = false; // construct and return path Path *path = buildActualPath( obj, locomotorSet.getValidSurfaces(), from, goalCell, centerInCell, blocked); - parentCell->releaseInfo(); - goalCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } else { + parentCell->releaseInfo(); + goalCell->releaseInfo(); + cleanOpenAndClosedLists(); + } + return path; } // put parent cell onto closed list - its evaluation is finished @@ -8947,16 +9219,16 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet #ifdef INTENSE_DEBUG if (count>5000) { show = true; - DEBUG_LOG(("FCP CC cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); + DEBUG_LOG(("FCP CC cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif - DEBUG_LOG(("%d Pathfind(findClosestPath) chugged from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y)); - DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("%d Pathfind(findClosestPath) chugged from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y)); + DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); #ifdef INTENSE_DEBUG TheScriptEngine->AppendDebugMessage("Big path FCP CC", false); #endif @@ -8970,8 +9242,14 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet rawTo->y = closesetCell->getYIndex()*PATHFIND_CELL_SIZE_F + PATHFIND_CELL_SIZE_F/2.0f; // construct and return path Path *path = buildActualPath( obj, locomotorSet.getValidSurfaces(), from, closesetCell, centerInCell, blocked ); - goalCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } else { + goalCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return path; } @@ -8980,10 +9258,10 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet Bool valid; valid = validMovementPosition( isCrusher, obj->getLayer(), locomotorSet, to ) ; - DEBUG_LOG(("Pathfind(findClosestPath) failed from (%f,%f) to (%f,%f), original valid %d --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y, valid)); - DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("Pathfind(findClosestPath) failed from (%f,%f) to (%f,%f), original valid %d --", TheGameLogic->getFrame(), from->x, from->y, to->x, to->y, valid)); + DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); #endif -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI) debugShowSearch(false); #endif @@ -8991,8 +9269,14 @@ Path *Pathfinder::findClosestPath( Object *obj, const LocomotorSet& locomotorSet TheGameLogic->incrementOverallFailedPathfinds(); #endif m_isTunneling = false; - goalCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + goalCell->releaseInfo(); + } else { + goalCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return NULL; } @@ -9031,7 +9315,7 @@ Path *Pathfinder::buildActualPath( const Object *obj, LocomotorSurfaceTypeMask a // cleanup the path by checking line of sight path->optimize(obj, acceptableSurfaces, blocked); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI==AI_DEBUG_PATHS) { extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); @@ -9115,22 +9399,45 @@ void Pathfinder::prependCells( Path *path, const Coord3D *fromPos, prevCell = cell; } - // TODO_NGMP: Investigate this further, is it truly valid for cell to be null? For now, we ignore if nullptr, the worst implication of this is they need to re-send the unit. - // TODO_NGMP: Should also investigate this again once Maullers pathfind fixes are in, it's probably unnecessary then - if (cell == nullptr) cell = prevCell; - if (cell != nullptr) - { - m_zoneManager.setPassable(cell->getXIndex(), cell->getYIndex(), true); - if (goalCellNull) { - // Very short path. - adjustCoordToCell(cell->getXIndex(), cell->getYIndex(), center, pos, cell->getLayer()); - path->prependNode(&pos, cell->getLayer()); - } - // put actual start position as first node on the path, so it begins right at the unit's feet - if (fromPos->x != path->getFirstNode()->getPosition()->x || fromPos->y != path->getFirstNode()->getPosition()->y) { - path->prependNode(fromPos, cell->getLayer()); +#if RETAIL_COMPATIBLE_PATHFINDING + // TheSuperHackers @info This pathway is here for retail compatibility, it is to catch when a starting cell has a dangling parent that contains no pathing information + // Beyond this point a retail client will crash due to a null pointer access within cell->getXIndex() + // To recover from this we set the cell to the previous cell, which should be the actual starting cell then set to use the fixed pathing and perform a forced cleanup + if (!s_useFixedPathfinding) { + if (!cell->hasInfo()) { + cell = prevCell; + + m_zoneManager.setPassable(cell->getXIndex(), cell->getYIndex(), true); + if (goalCellNull) { + // Very short path. + adjustCoordToCell(cell->getXIndex(), cell->getYIndex(), center, pos, cell->getLayer()); + path->prependNode( &pos, cell->getLayer() ); + } + // put actual start position as first node on the path, so it begins right at the unit's feet + if (fromPos->x != path->getFirstNode()->getPosition()->x || fromPos->y != path->getFirstNode()->getPosition()->y) { + path->prependNode( fromPos, cell->getLayer() ); + } + + s_useFixedPathfinding = true; + PathfindCellInfo::forceCleanPathFindInfoCells(); + forceCleanCells(); + m_openList = NULL; + m_closedList = NULL; + return; } } +#endif + + m_zoneManager.setPassable(cell->getXIndex(), cell->getYIndex(), true); + if (goalCellNull) { + // Very short path. + adjustCoordToCell(cell->getXIndex(), cell->getYIndex(), center, pos, cell->getLayer()); + path->prependNode( &pos, cell->getLayer() ); + } + // put actual start position as first node on the path, so it begins right at the unit's feet + if (fromPos->x != path->getFirstNode()->getPosition()->x || fromPos->y != path->getFirstNode()->getPosition()->y) { + path->prependNode( fromPos, cell->getLayer() ); + } } @@ -9431,23 +9738,23 @@ Bool Pathfinder::isViewBlockedByObstacle(const Object* obj, const Object* objOth //----------------------------------------------------------------------------- Bool Pathfinder::isAttackViewBlockedByObstacle(const Object* attacker, const Coord3D& attackerPos, const Object* victim, const Coord3D& victimPos) { - //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() - attackerPos is (%g,%g,%g) (%X,%X,%X)", + //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() - attackerPos is (%g,%g,%g) (%X,%X,%X)", // attackerPos.x, attackerPos.y, attackerPos.z, // AS_INT(attackerPos.x),AS_INT(attackerPos.y),AS_INT(attackerPos.z))); - //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() - victimPos is (%g,%g,%g) (%X,%X,%X)", + //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() - victimPos is (%g,%g,%g) (%X,%X,%X)", // victimPos.x, victimPos.y, victimPos.z, // AS_INT(victimPos.x),AS_INT(victimPos.y),AS_INT(victimPos.z))); // Global switch to turn this off in case it doesn't work. if (!TheAI->getAiData()->m_attackUsesLineOfSight) { - //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 1")); + //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 1")); return false; } // If the attacker doesn't need line of sight, isn't blocked. if (!attacker->isKindOf(KINDOF_ATTACK_NEEDS_LINE_OF_SIGHT)) { - //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 2")); + //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 2")); return false; } @@ -9469,7 +9776,7 @@ Bool Pathfinder::isAttackViewBlockedByObstacle(const Object* attacker, const Coo if (viewBlocked) { - //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 3")); + //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 3")); return true; } } @@ -9497,7 +9804,7 @@ Bool Pathfinder::isAttackViewBlockedByObstacle(const Object* attacker, const Coo } Int ret = iterateCellsAlongLine(attackerPos, victimPos, layer, attackBlockedByObstacleCallback, &info); - //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 4")); + //CRCDEBUG_LOG(("Pathfinder::isAttackViewBlockedByObstacle() 4")); return ret != 0; } @@ -9696,7 +10003,7 @@ Bool Pathfinder::isLinePassable( const Object *obj, LocomotorSurfaceTypeMask acc Bool allowPinched) { LinePassableStruct info; - //CRCDEBUG_LOG(("Pathfinder::isLinePassable(): %d %d %d ", m_ignoreObstacleID, m_isMapReady, m_isTunneling)); + //CRCDEBUG_LOG(("Pathfinder::isLinePassable(): %d %d %d ", m_ignoreObstacleID, m_isMapReady, m_isTunneling)); info.obj = obj; info.acceptableSurfaces = acceptableSurfaces; @@ -9795,7 +10102,7 @@ void Pathfinder::updateGoal( Object *obj, const Coord3D *newGoalPos, PathfindLay PathfindLayerEnum originalLayer = obj->getDestinationLayer(); - //DEBUG_LOG(("Object Goal layer is %d", layer)); + //DEBUG_LOG(("Object Goal layer is %d", layer)); Bool layerChanged = originalLayer != layer; Bool doGround=false; @@ -9844,7 +10151,7 @@ void Pathfinder::updateGoal( Object *obj, const Coord3D *newGoalPos, PathfindLay if (warn && cell->getGoalUnit()!=INVALID_ID && cell->getGoalUnit() != obj->getID()) { warn = false; // jba intense debug - //DEBUG_LOG(, ("Units got stuck close to each other. jba")); + //DEBUG_LOG(, ("Units got stuck close to each other. jba")); } cellNdx.x = i; cellNdx.y = j; @@ -9857,7 +10164,7 @@ void Pathfinder::updateGoal( Object *obj, const Coord3D *newGoalPos, PathfindLay if (warn && cell->getGoalUnit()!=INVALID_ID && cell->getGoalUnit() != obj->getID()) { warn = false; // jba intense debug - //DEBUG_LOG(, ("Units got stuck close to each other. jba")); + //DEBUG_LOG(, ("Units got stuck close to each other. jba")); } cellNdx.x = i; cellNdx.y = j; @@ -10051,7 +10358,7 @@ void Pathfinder::updatePos( Object *obj, const Coord3D *newPos) ai->setCurPathfindCell(newCell); Int i,j; ICoord2D cellNdx; - //DEBUG_LOG(("Updating unit pos at cell %d, %d", newCell.x, newCell.y)); + //DEBUG_LOG(("Updating unit pos at cell %d, %d", newCell.x, newCell.y)); if (curCell.x>=0 && curCell.y>=0) { for (i=curCell.x-radius; i=0 && curCell.y>=0) { for (i=curCell.x-radius; igetAI()->aiMoveAwayFromUnit(obj, CMD_FROM_AI); } } @@ -10384,8 +10691,13 @@ Path *Pathfinder::getMoveAwayFromPath(Object* obj, Object *otherObj, m_isTunneling = false; // construct and return path Path *newPath = buildActualPath( obj, locomotorSet.getValidSurfaces(), obj->getPosition(), parentCell, centerInCell, false); - parentCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + } else { + parentCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return newPath; } // put parent cell onto closed list - its evaluation is finished @@ -10398,15 +10710,20 @@ Path *Pathfinder::getMoveAwayFromPath(Object* obj, Object *otherObj, } -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) debugShowSearch(true); #endif - DEBUG_LOG(("%d getMoveAwayFromPath pathfind failed --", TheGameLogic->getFrame())); - DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("%d getMoveAwayFromPath pathfind failed --", TheGameLogic->getFrame())); + DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); m_isTunneling = false; - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + } else { + cleanOpenAndClosedLists(); + } return NULL; } @@ -10417,7 +10734,7 @@ Path *Pathfinder::getMoveAwayFromPath(Object* obj, Object *otherObj, Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet, Path *originalPath, Bool blocked ) { - //CRCDEBUG_LOG(("Pathfinder::patchPath()")); + //CRCDEBUG_LOG(("Pathfinder::patchPath()")); #ifdef DEBUG_LOGGING Int startTimeMS = ::GetTickCount(); #endif @@ -10473,7 +10790,7 @@ Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet // until goal is found. // -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) extern void addIcon(const Coord3D *pos, Real width, Int numFramesDuration, RGBColor color); if (TheGlobalData->m_debugAI) { @@ -10496,7 +10813,7 @@ Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet info.radius = radius; info.considerTransient = blocked; info.acceptableSurfaces = locomotorSet.getValidSurfaces(); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI) { RGBColor color; color.setFromInt(0); @@ -10522,7 +10839,11 @@ Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet } if (startNode == originalPath->getLastNode()) { - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + parentCell->releaseInfo(); + } else { + cleanOpenAndClosedLists(); + } return NULL; // no open nodes. } PathfindCell *candidateGoal; @@ -10530,6 +10851,9 @@ Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet ICoord2D goalCellNdx; worldToCell(&goalPos, &goalCellNdx); if (!candidateGoal->allocateInfo(goalCellNdx)) { + if (s_useFixedPathfinding) { + parentCell->releaseInfo(); + } return NULL; } @@ -10564,9 +10888,15 @@ Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet // cleanup the path by checking line of sight path->optimize(obj, locomotorSet.getValidSurfaces(), blocked); - parentCell->releaseInfo(); - cleanOpenAndClosedLists(); - candidateGoal->releaseInfo(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + candidateGoal->releaseInfo(); + } else { + parentCell->releaseInfo(); + cleanOpenAndClosedLists(); + candidateGoal->releaseInfo(); + } return path; } @@ -10580,21 +10910,27 @@ Path *Pathfinder::patchPath( const Object *obj, const LocomotorSet& locomotorSet } } - DEBUG_LOG(("%d patchPath Pathfind failed --", TheGameLogic->getFrame())); - DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); + DEBUG_LOG(("%d patchPath Pathfind failed --", TheGameLogic->getFrame())); + DEBUG_LOG(("Unit '%s', time %f", obj->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (TheGlobalData->m_debugAI) { debugShowSearch(true); } #endif m_isTunneling = false; - if (!candidateGoal->getOpen() && !candidateGoal->getClosed()) - { - // Not on one of the lists + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); candidateGoal->releaseInfo(); + } else { + if (!candidateGoal->getOpen() && !candidateGoal->getClosed()) + { + // Not on one of the lists + candidateGoal->releaseInfo(); + } + cleanOpenAndClosedLists(); } - cleanOpenAndClosedLists(); return NULL; } @@ -10604,36 +10940,36 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot const Object *victim, const Coord3D* victimPos, const Weapon *weapon ) { /* - CRCDEBUG_LOG(("Pathfinder::findAttackPath() for object %d (%s)", obj->getID(), obj->getTemplate()->getName().str())); + CRCDEBUG_LOG(("Pathfinder::findAttackPath() for object %d (%s)", obj->getID(), obj->getTemplate()->getName().str())); XferCRC xferCRC; xferCRC.open("lightCRC"); xferCRC.xferSnapshot((Object *)obj); xferCRC.close(); - CRCDEBUG_LOG(("obj CRC is %X", xferCRC.getCRC())); + CRCDEBUG_LOG(("obj CRC is %X", xferCRC.getCRC())); if (from) { - CRCDEBUG_LOG(("from: (%g,%g,%g) (%X,%X,%X)", + CRCDEBUG_LOG(("from: (%g,%g,%g) (%X,%X,%X)", from->x, from->y, from->z, AS_INT(from->x), AS_INT(from->y), AS_INT(from->z))); } if (victim) { - CRCDEBUG_LOG(("victim is %d (%s)", victim->getID(), victim->getTemplate()->getName().str())); + CRCDEBUG_LOG(("victim is %d (%s)", victim->getID(), victim->getTemplate()->getName().str())); XferCRC xferCRC; xferCRC.open("lightCRC"); xferCRC.xferSnapshot((Object *)victim); xferCRC.close(); - CRCDEBUG_LOG(("victim CRC is %X", xferCRC.getCRC())); + CRCDEBUG_LOG(("victim CRC is %X", xferCRC.getCRC())); } if (victimPos) { - CRCDEBUG_LOG(("from: (%g,%g,%g) (%X,%X,%X)", + CRCDEBUG_LOG(("from: (%g,%g,%g) (%X,%X,%X)", victimPos->x, victimPos->y, victimPos->z, AS_INT(victimPos->x), AS_INT(victimPos->y), AS_INT(victimPos->z))); } */ if (m_isMapReady == false) return NULL; // Should always be ok. -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) // Int startTimeMS = ::GetTickCount(); #endif @@ -10811,11 +11147,11 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot } if (count>1000) { show = true; - DEBUG_LOG(("FAP cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); + DEBUG_LOG(("FAP cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif TheScriptEngine->AppendDebugMessage("Big Attack path", false); @@ -10823,8 +11159,8 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot #endif if (show) debugShowSearch(true); - #if defined(RTS_DEBUG) - //DEBUG_LOG(("Attack path took %d cells, %f sec", cellCount, (::GetTickCount()-startTimeMS)/1000.0f)); + #if defined(RTS_DEBUG) + //DEBUG_LOG(("Attack path took %d cells, %f sec", cellCount, (::GetTickCount()-startTimeMS)/1000.0f)); #endif // put parent cell onto closed list - its evaluation is finished m_closedList = parentCell->putOnClosedList( m_closedList ); @@ -10887,10 +11223,16 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot } } Path *path = buildActualPath( obj, locomotorSet.getValidSurfaces(), obj->getPosition(), parentCell, centerInCell, false); - if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); goalCell->releaseInfo(); + } else { + if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + goalCell->releaseInfo(); + } + cleanOpenAndClosedLists(); } - cleanOpenAndClosedLists(); return path; } } @@ -10920,11 +11262,11 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot } #ifdef INTENSE_DEBUG - DEBUG_LOG(("obj %s %x", obj->getTemplate()->getName().str(), obj)); + DEBUG_LOG(("obj %s %x", obj->getTemplate()->getName().str(), obj)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif debugShowSearch(true); @@ -10937,19 +11279,25 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot cleanOpenAndClosedLists(); return path; } -#if defined(RTS_DEBUG) - DEBUG_LOG(("%d (%d cells) Attack Pathfind failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), cellCount, from->x, from->y, victim->getPosition()->x, victim->getPosition()->y)); - DEBUG_LOG(("Unit '%s', attacking '%s' time %f", obj->getTemplate()->getName().str(), victim->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); +#if defined(RTS_DEBUG) + DEBUG_LOG(("%d (%d cells) Attack Pathfind failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), cellCount, from->x, from->y, victim->getPosition()->x, victim->getPosition()->y)); + DEBUG_LOG(("Unit '%s', attacking '%s' time %f", obj->getTemplate()->getName().str(), victim->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); #endif #endif #ifdef DUMP_PERF_STATS TheGameLogic->incrementOverallFailedPathfinds(); #endif m_isTunneling = false; - if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); goalCell->releaseInfo(); + } else { + if (goalCell->hasInfo() && !goalCell->getClosed() && !goalCell->getOpen()) { + goalCell->releaseInfo(); + } + cleanOpenAndClosedLists(); } - cleanOpenAndClosedLists(); return NULL; } @@ -10957,9 +11305,9 @@ Path *Pathfinder::findAttackPath( const Object *obj, const LocomotorSet& locomot Path *Pathfinder::findSafePath( const Object *obj, const LocomotorSet& locomotorSet, const Coord3D *from, const Coord3D* repulsorPos1, const Coord3D* repulsorPos2, Real repulsorRadius) { - //CRCDEBUG_LOG(("Pathfinder::findSafePath()")); + //CRCDEBUG_LOG(("Pathfinder::findSafePath()")); if (m_isMapReady == false) return NULL; // Should always be ok. -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) // Int startTimeMS = ::GetTickCount(); #endif @@ -11036,7 +11384,7 @@ Path *Pathfinder::findSafePath( const Object *obj, const LocomotorSet& locomotor farthestDistanceSqr = distSqr; if (cellCount > MAX_CELLS) { #ifdef INTENSE_DEBUG - DEBUG_LOG(("Took intermediate path, dist %f, goal dist %f", sqrt(farthestDistanceSqr), repulsorRadius)); + DEBUG_LOG(("Took intermediate path, dist %f, goal dist %f", sqrt(farthestDistanceSqr), repulsorRadius)); #endif ok = true; // Already a big search, just take this one. } @@ -11054,11 +11402,11 @@ Path *Pathfinder::findSafePath( const Object *obj, const LocomotorSet& locomotor } if (count>2000) { show = true; - DEBUG_LOG(("cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); + DEBUG_LOG(("cells %d obj %s %x", count, obj->getTemplate()->getName().str(), obj)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif TheScriptEngine->AppendDebugMessage("Big Safe path", false); @@ -11066,13 +11414,18 @@ Path *Pathfinder::findSafePath( const Object *obj, const LocomotorSet& locomotor #endif if (show) debugShowSearch(true); -#if defined(RTS_DEBUG) - //DEBUG_LOG(("Attack path took %d cells, %f sec", cellCount, (::GetTickCount()-startTimeMS)/1000.0f)); +#if defined(RTS_DEBUG) + //DEBUG_LOG(("Attack path took %d cells, %f sec", cellCount, (::GetTickCount()-startTimeMS)/1000.0f)); #endif // construct and return path Path *path = buildActualPath( obj, locomotorSet.getValidSurfaces(), obj->getPosition(), parentCell, centerInCell, false); - parentCell->releaseInfo(); - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + } else { + parentCell->releaseInfo(); + cleanOpenAndClosedLists(); + } return path; } @@ -11087,68 +11440,73 @@ Path *Pathfinder::findSafePath( const Object *obj, const LocomotorSet& locomotor } #ifdef INTENSE_DEBUG - DEBUG_LOG(("obj %s %x count %d", obj->getTemplate()->getName().str(), obj, cellCount)); + DEBUG_LOG(("obj %s %x count %d", obj->getTemplate()->getName().str(), obj, cellCount)); #ifdef STATE_MACHINE_DEBUG if( obj->getAIUpdateInterface() ) { - DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); + DEBUG_LOG(("State %s", obj->getAIUpdateInterface()->getCurrentStateName().str())); } #endif TheScriptEngine->AppendDebugMessage("Overflowed Safe path", false); #endif #if 0 -#if defined(RTS_DEBUG) - DEBUG_LOG(("%d (%d cells) Attack Pathfind failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), cellCount, from->x, from->y, victim->getPosition()->x, victim->getPosition()->y)); - DEBUG_LOG(("Unit '%s', attacking '%s' time %f", obj->getTemplate()->getName().str(), victim->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); +#if defined(RTS_DEBUG) + DEBUG_LOG(("%d (%d cells) Attack Pathfind failed from (%f,%f) to (%f,%f) --", TheGameLogic->getFrame(), cellCount, from->x, from->y, victim->getPosition()->x, victim->getPosition()->y)); + DEBUG_LOG(("Unit '%s', attacking '%s' time %f", obj->getTemplate()->getName().str(), victim->getTemplate()->getName().str(), (::GetTickCount()-startTimeMS)/1000.0f)); #endif #endif #ifdef DUMP_PERF_STATS TheGameLogic->incrementOverallFailedPathfinds(); #endif m_isTunneling = false; - cleanOpenAndClosedLists(); + if (s_useFixedPathfinding) { + cleanOpenAndClosedLists(); + parentCell->releaseInfo(); + } else { + cleanOpenAndClosedLists(); + } return NULL; } //----------------------------------------------------------------------------- void Pathfinder::crc( Xfer *xfer ) { - CRCDEBUG_LOG(("Pathfinder::crc() on frame %d", TheGameLogic->getFrame())); - CRCDEBUG_LOG(("beginning CRC: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("Pathfinder::crc() on frame %d", TheGameLogic->getFrame())); + CRCDEBUG_LOG(("beginning CRC: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferUser( &m_extent, sizeof(IRegion2D) ); - CRCDEBUG_LOG(("m_extent: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_extent: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferBool( &m_isMapReady ); - CRCDEBUG_LOG(("m_isMapReady: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_isMapReady: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferBool( &m_isTunneling ); - CRCDEBUG_LOG(("m_isTunneling: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_isTunneling: %8.8X", ((XferCRC *)xfer)->getCRC())); Int obsolete1 = 0; xfer->xferInt( &obsolete1 ); xfer->xferUser(&m_ignoreObstacleID, sizeof(ObjectID)); - CRCDEBUG_LOG(("m_ignoreObstacleID: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_ignoreObstacleID: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferUser(m_queuedPathfindRequests, sizeof(ObjectID)*PATHFIND_QUEUE_LEN); - CRCDEBUG_LOG(("m_queuedPathfindRequests: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_queuedPathfindRequests: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferInt(&m_queuePRHead); - CRCDEBUG_LOG(("m_queuePRHead: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_queuePRHead: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferInt(&m_queuePRTail); - CRCDEBUG_LOG(("m_queuePRTail: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_queuePRTail: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferInt(&m_numWallPieces); - CRCDEBUG_LOG(("m_numWallPieces: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_numWallPieces: %8.8X", ((XferCRC *)xfer)->getCRC())); for (Int i=0; ixferObjectID(&m_wallPieces[MAX_WALL_PIECES]); } - CRCDEBUG_LOG(("m_wallPieces: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_wallPieces: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferReal(&m_wallHeight); - CRCDEBUG_LOG(("m_wallHeight: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_wallHeight: %8.8X", ((XferCRC *)xfer)->getCRC())); xfer->xferInt(&m_cumulativeCellsAllocated); - CRCDEBUG_LOG(("m_cumulativeCellsAllocated: %8.8X", ((XferCRC *)xfer)->getCRC())); + CRCDEBUG_LOG(("m_cumulativeCellsAllocated: %8.8X", ((XferCRC *)xfer)->getCRC())); } // end crc diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeployStyleAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeployStyleAIUpdate.cpp index 1de718fba11..472ccaf86b5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeployStyleAIUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/DeployStyleAIUpdate.cpp @@ -98,7 +98,11 @@ UpdateSleepTime DeployStyleAIUpdate::update( void ) // when we have a pending command... Object *self = getObject(); Weapon *weapon = self->getCurrentWeapon(); +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + UnsignedInt now = TheGameLogic->getFrameLegacy(); +#else UnsignedInt now = TheGameLogic->getFrame(); +#endif const DeployStyleAIUpdateModuleData *data = getDeployStyleAIUpdateModuleData(); //Are we attempting to move? If so we can't do it unless we are undeployed. @@ -233,6 +237,13 @@ UpdateSleepTime DeployStyleAIUpdate::update( void ) case DEPLOY: if( data->m_manualDeployAnimations ) { +#if defined(GENERALS_ONLINE) && defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + if (!TheGameLogic->HasLegacyFrameAdvanced()) + { + break; + } +#endif + UnsignedInt totalFrames = getPackTime(); UnsignedInt framesLeft = m_frameToWaitForDeploy - now; Drawable *draw = self->getDrawable(); @@ -247,6 +258,12 @@ UpdateSleepTime DeployStyleAIUpdate::update( void ) case UNDEPLOY: if( data->m_manualDeployAnimations ) { +#if defined(GENERALS_ONLINE) && defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + if (!TheGameLogic->HasLegacyFrameAdvanced()) + { + break; + } +#endif UnsignedInt framesLeft = m_frameToWaitForDeploy - now; Drawable *draw = self->getDrawable(); if( draw ) @@ -276,7 +293,11 @@ void DeployStyleAIUpdate::setMyState( DeployStateTypes stateID, Bool reverseDepl { m_state = stateID; Object *self = getObject(); +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + UnsignedInt now = TheGameLogic->getFrameLegacy(); +#else UnsignedInt now = TheGameLogic->getFrame(); +#endif const DeployStyleAIUpdateModuleData *data = getDeployStyleAIUpdateModuleData(); switch( stateID ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp index 4dbfa7e239f..4e9eadf1c11 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp @@ -115,7 +115,11 @@ NeutronMissileUpdate::NeutronMissileUpdate( Thing *thing, const ModuleData* modu m_vel.zero(); m_state = PRELAUNCH; +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + m_stateTimestamp = TheGameLogic->getFrameLegacy(); +#else m_stateTimestamp = TheGameLogic->getFrame(); +#endif m_isArmed = false; m_isLaunched = false; m_launcherID = INVALID_ID; @@ -169,7 +173,11 @@ void NeutronMissileUpdate::projectileFireAtObjectOrPosition( const Object *victi m_exhaustSysTmpl = exhaustSysOverride; m_state = LAUNCH; +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + m_stateTimestamp = TheGameLogic->getFrameLegacy(); +#else m_stateTimestamp = TheGameLogic->getFrame(); +#endif if( victim ) { @@ -249,7 +257,12 @@ void NeutronMissileUpdate::doLaunch( void ) FXList::doFXObj(getNeutronMissileUpdateModuleData()->m_launchFX, getObject()); m_heightAtLaunch = getObject()->getPosition()->z; + +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + m_frameAtLaunch = TheGameLogic->getFrameLegacy(); +#else m_frameAtLaunch = TheGameLogic->getFrame(); +#endif } @@ -268,7 +281,11 @@ void NeutronMissileUpdate::doLaunch( void ) TheParticleSystemManager->createAttachedParticleSystemID(m_exhaustSysTmpl, getObject()); m_state = ATTACK; +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + m_stateTimestamp = TheGameLogic->getFrameLegacy(); +#else m_stateTimestamp = TheGameLogic->getFrame(); +#endif // arm the missile's "warhead" m_isArmed = true; @@ -380,7 +397,12 @@ void NeutronMissileUpdate::doAttack( void ) Coord3D pos = *getObject()->getPosition(); const NeutronMissileUpdateModuleData* d = getNeutronMissileUpdateModuleData(); + +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + UnsignedInt now = TheGameLogic->getFrameLegacy(); +#else UnsignedInt now = TheGameLogic->getFrame(); +#endif if (d->m_specialSpeedTime > 0 && now <= m_frameAtLaunch + d->m_specialSpeedTime) { getObject()->getDrawable()->setInstanceMatrix(NULL); @@ -471,6 +493,13 @@ void NeutronMissileUpdate::detonate() */ UpdateSleepTime NeutronMissileUpdate::update( void ) { +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + if (!TheGameLogic->HasLegacyFrameAdvanced()) + { + return UPDATE_SLEEP_NONE; + } +#endif + m_deliveryDecal.update(); if (!m_reachedIntermediatePos) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp index a8430df4f2e..d93a2fd76de 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -1163,9 +1163,17 @@ void ConnectionManager::sendRemoteCommand(NetCommandRef *msg) { if (allCommandsReady(msg->getCommand()->getExecutionFrame(), TRUE)) { UnsignedInt cushion = msg->getCommand()->getExecutionFrame() - TheGameLogic->getFrame(); if ((cushion < m_smallestPacketArrivalCushion) || (m_smallestPacketArrivalCushion == -1)) { +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + m_smallestPacketArrivalCushion = (float)cushion * 1.5f; +#else m_smallestPacketArrivalCushion = cushion; +#endif } +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + m_frameMetrics.addCushion((float)cushion * 1.5f); +#else m_frameMetrics.addCushion(cushion); +#endif // DEBUG_LOG(("Adding %d to cushion for frame %d", cushion, msg->getCommand()->getExecutionFrame())); } } @@ -1286,7 +1294,7 @@ void ConnectionManager::updateRunAhead(Int oldRunAhead, Int frameRate, Bool didS #if !defined(GENERALS_ONLINE) newRunAhead += (newRunAhead * TheGlobalData->m_networkRunAheadSlack) / 100; // Add in 10% of slack to the run ahead in case of network hiccups. #else - newRunAhead += ceil(((float)TheGlobalData->m_networkRunAheadSlack / 100.f) * newRunAhead);// Add in 10% of slack to the run ahead in case of network hiccups. + newRunAhead += (Int)ceilf(newRunAhead * (TheGlobalData->m_networkRunAheadSlack / 100.0f)); #endif @@ -1321,12 +1329,19 @@ void ConnectionManager::updateRunAhead(Int oldRunAhead, Int frameRate, Bool didS } #if defined(GENERALS_ONLINE) // provide instant responsiveness if there are no remote human players - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetInstance()->GetLobbyInterface()->GetNetworkMesh(); - if (pMesh != nullptr) + if (TheNGMPGame != nullptr) { - if (pMesh->GetAllConnections().size() == 0) + auto lobbyInterface = NGMP_OnlineServicesManager::GetInstance()->GetLobbyInterface(); + if (lobbyInterface != nullptr) { - newRunAhead = 0; + NetworkMesh* pMesh = lobbyInterface->GetNetworkMesh(); + if (pMesh != nullptr) + { + if (pMesh->GetAllConnections().size() == 0) + { + newRunAhead = 0; + } + } } } #endif @@ -1402,16 +1417,20 @@ void ConnectionManager::updateRunAhead(Int oldRunAhead, Int frameRate, Bool didS #if defined(GENERALS_ONLINE) if (TheNGMPGame != nullptr) { - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetInstance()->GetLobbyInterface()->GetNetworkMesh(); - - if (pMesh != nullptr) - { - float maxLatency = (float)pMesh->getMaximumHistoricalLatency(); - msg->setAverageLatency(maxLatency / 1000.f); - } - else + auto lobbyInterface = NGMP_OnlineServicesManager::GetInstance()->GetLobbyInterface(); + if (lobbyInterface != nullptr) { - msg->setAverageLatency(m_frameMetrics.getAverageLatency()); + NetworkMesh* pMesh = lobbyInterface->GetNetworkMesh(); + + if (pMesh != nullptr) + { + float maxLatency = (float)pMesh->getMaximumHistoricalLatency(); + msg->setAverageLatency(maxLatency / 1000.f); + } + else + { + msg->setAverageLatency(m_frameMetrics.getAverageLatency()); + } } } else diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp index d75bc9652fc..aa76670f978 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp @@ -356,8 +356,13 @@ void NGMPGame::launchGame(void) TheWritableGlobalData->m_showMetrics = true; #endif - TheWritableGlobalData->m_networkRunAheadMetricsTime = 16; +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + TheWritableGlobalData->m_networkRunAheadMetricsTime = 16 * 10; + TheWritableGlobalData->m_networkRunAheadSlack = 20; +#else + TheWritableGlobalData->m_networkRunAheadMetricsTime = 33 * 10; TheWritableGlobalData->m_networkRunAheadSlack = 10; +#endif #if defined(GENERALS_ONLINE_HIGH_FPS_RENDER) TheWritableGlobalData->m_horizontalScrollSpeedFactor = NGMP_OnlineServicesManager::Settings.Camera_MoveSpeedRatio(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index c5e94393f55..826526f0cb0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -542,6 +542,15 @@ NetworkMesh::NetworkMesh() } } + +void NetworkMesh::Flush() +{ + for (auto& connectionData : m_mapConnections) + { + SteamNetworkingSockets()->FlushMessagesOnConnection(connectionData.second.m_hSteamConnection); + } +} + void NetworkMesh::UpdateConnectivity(PlayerConnection* connection) { nlohmann::json j; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp index edb612ac283..1a2fb68cda3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp @@ -51,6 +51,9 @@ Bool NextGenTransport::update(void) retval = FALSE; } + // flush + NGMP_OnlineServicesManager::GetInstance()->GetLobbyInterface()->GetNetworkMesh()->Flush(); + return retval; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/GameNetworkingSockets.dll b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/GameNetworkingSockets.dll index 10f6611f024a3a0458d67ba42d37c6efd9d26989..854f8adb7255dd21c0e3d6e198ab250950c5ac1e 100644 GIT binary patch delta 312256 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 z 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
7)fGV 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 delta 312849 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. -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - - -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - -#include "GameNetwork/NetCommandMsg.h" -#include "Common/GameState.h" -#include "Common/PlayerList.h" -#include "Common/Player.h" - -/** - * Base constructor - */ -NetCommandMsg::NetCommandMsg() -{ - //Added By Sadullah Nader - //Initializations inserted - m_executionFrame = 0; - m_id = 0; - m_playerID = 0; - - // - m_timestamp = 0; - m_referenceCount = 1; // start this off as 1. This means that an "attach" is implied by creating a NetCommandMsg object. - m_commandType = NETCOMMANDTYPE_UNKNOWN; -} - -/** - * Destructor - */ -NetCommandMsg::~NetCommandMsg() { -} - -/** - * Adds one to the reference count. - */ -void NetCommandMsg::attach() { - ++m_referenceCount; -} - -/** - * Subtracts one from the reference count. If the reference count is 0, the this object is destroyed. - */ -void NetCommandMsg::detach() { - --m_referenceCount; - if (m_referenceCount == 0) { - deleteInstance(this); - return; - } - DEBUG_ASSERTCRASH(m_referenceCount > 0, ("Invalid reference count for NetCommandMsg")); // Just to make sure... - if (m_referenceCount < 0) { - deleteInstance(this); - } -} - -/** - * Returns the value by which this type of message should be sorted. - */ -Int NetCommandMsg::getSortNumber() { - return m_id; -} - -//------------------------------- -// NetGameCommandMsg -//------------------------------- - -/** - * Constructor with no argument, sets everything to default values. - */ -NetGameCommandMsg::NetGameCommandMsg() : NetCommandMsg() { - //Added By Sadullah Nader - //Initializations inserted - m_argSize = 0; - m_numArgs = 0; - // - - m_type = (GameMessage::Type)0; - m_commandType = NETCOMMANDTYPE_GAMECOMMAND; - m_argList = NULL; - m_argTail = NULL; -} - -/** - * Constructor with a GameMessage argument. Sets member variables appropriately for this GameMessage. - * Also copies all the arguments. - */ -NetGameCommandMsg::NetGameCommandMsg(GameMessage *msg) : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_GAMECOMMAND; - - m_type = msg->getType(); - Int count = msg->getArgumentCount(); - for (Int i = 0; i < count; ++i) { - addArgument(msg->getArgumentDataType(i), *(msg->getArgument(i))); - } -} - -/** - * Destructor - */ -NetGameCommandMsg::~NetGameCommandMsg() { - GameMessageArgument *arg = m_argList; - while (arg != NULL) { - m_argList = m_argList->m_next; - deleteInstance(arg); - arg = m_argList; - } -} - -/** - * Add an argument to this command. - */ -void NetGameCommandMsg::addArgument(const GameMessageArgumentDataType type, GameMessageArgumentType arg) -{ - if (m_argTail == NULL) { - m_argList = newInstance(GameMessageArgument); - m_argTail = m_argList; - m_argList->m_data = arg; - m_argList->m_type = type; - m_argList->m_next = NULL; - return; - } - - GameMessageArgument *newArg = newInstance(GameMessageArgument); - newArg->m_data = arg; - newArg->m_type = type; - newArg->m_next = NULL; - m_argTail->m_next = newArg; - m_argTail = newArg; -} - -// here's where we figure out which slot corresponds to which player -static Int indexFromMask(UnsignedInt mask) -{ - Player *player = NULL; - Int i; - - for( i = 0; i < MAX_PLAYER_COUNT; i++ ) - { - player = ThePlayerList->getNthPlayer( i ); - if( player && player->getPlayerMask() == mask ) - return i; - } // end for i - - return -1; -} - -/** - * Construct a new GameMessage object from the data in this object. - */ -GameMessage *NetGameCommandMsg::constructGameMessage() -{ - GameMessage *retval = newInstance(GameMessage)(m_type); - - AsciiString name; - name.format("player%d", getPlayerID()); - retval->friend_setPlayerIndex( ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(name))->getPlayerIndex()); -// retval->friend_setPlayerIndex(indexFromMask(ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(name))->getPlayerMask())); - - GameMessageArgument *arg = m_argList; - while (arg != NULL) { -// retval->appendGenericArgument(arg->m_data); - if (arg->m_type == ARGUMENTDATATYPE_INTEGER) { - retval->appendIntegerArgument(arg->m_data.integer); - } else if (arg->m_type == ARGUMENTDATATYPE_REAL) { - retval->appendRealArgument(arg->m_data.real); - } else if (arg->m_type == ARGUMENTDATATYPE_BOOLEAN) { - retval->appendBooleanArgument(arg->m_data.boolean); - } else if (arg->m_type == ARGUMENTDATATYPE_OBJECTID) { - retval->appendObjectIDArgument(arg->m_data.objectID); - } else if (arg->m_type == ARGUMENTDATATYPE_DRAWABLEID) { - retval->appendDrawableIDArgument(arg->m_data.drawableID); - } else if (arg->m_type == ARGUMENTDATATYPE_TEAMID) { - retval->appendTeamIDArgument(arg->m_data.teamID); - } else if (arg->m_type == ARGUMENTDATATYPE_LOCATION) { - retval->appendLocationArgument(arg->m_data.location); - } else if (arg->m_type == ARGUMENTDATATYPE_PIXEL) { - retval->appendPixelArgument(arg->m_data.pixel); - } else if (arg->m_type == ARGUMENTDATATYPE_PIXELREGION) { - retval->appendPixelRegionArgument(arg->m_data.pixelRegion); - } else if (arg->m_type == ARGUMENTDATATYPE_TIMESTAMP) { - retval->appendTimestampArgument(arg->m_data.timestamp); - } else if (arg->m_type == ARGUMENTDATATYPE_WIDECHAR) { - retval->appendWideCharArgument(arg->m_data.wChar); - } - arg = arg->m_next; - } - return retval; -} - -/** - * Sets the type of game message - */ -void NetGameCommandMsg::setGameMessageType(GameMessage::Type type) { - m_type = type; -} - -//------------------------- -// NetAckBothCommandMsg -//------------------------- -/** - * Constructor. Sets the member variables according to the given message. - */ -NetAckBothCommandMsg::NetAckBothCommandMsg(NetCommandMsg *msg) : NetCommandMsg() { - m_commandID = msg->getID(); - m_commandType = NETCOMMANDTYPE_ACKBOTH; - m_originalPlayerID = msg->getPlayerID(); -} - -/** - * Constructor. Sets the member variables to default values. - */ -NetAckBothCommandMsg::NetAckBothCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_ACKBOTH; -} - -/** - * Destructor. - */ -NetAckBothCommandMsg::~NetAckBothCommandMsg() { -} - -/** - * Returns the command ID of the command being ack'd. - */ -UnsignedShort NetAckBothCommandMsg::getCommandID() { - return m_commandID; -} - -/** - * Set the command ID of the command being ack'd. - */ -void NetAckBothCommandMsg::setCommandID(UnsignedShort commandID) { - m_commandID = commandID; -} - -/** - * Get the player id of the player who originally sent the command. - */ -UnsignedByte NetAckBothCommandMsg::getOriginalPlayerID() { - return m_originalPlayerID; -} - -/** - * Set the player id of the player who originally sent the command. - */ -void NetAckBothCommandMsg::setOriginalPlayerID(UnsignedByte originalPlayerID) { - m_originalPlayerID = originalPlayerID; -} - -Int NetAckBothCommandMsg::getSortNumber() { - return m_commandID; -} - -//------------------------- -// NetAckStage1CommandMsg -//------------------------- -/** - * Constructor. Sets the member variables according to the given message. - */ -NetAckStage1CommandMsg::NetAckStage1CommandMsg(NetCommandMsg *msg) : NetCommandMsg() { - m_commandID = msg->getID(); - m_commandType = NETCOMMANDTYPE_ACKSTAGE1; - m_originalPlayerID = msg->getPlayerID(); -} - -/** - * Constructor. Sets the member variables to default values. - */ -NetAckStage1CommandMsg::NetAckStage1CommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_ACKSTAGE1; -} - -/** - * Destructor. - */ -NetAckStage1CommandMsg::~NetAckStage1CommandMsg() { -} - -/** - * Returns the command ID of the command being ack'd. - */ -UnsignedShort NetAckStage1CommandMsg::getCommandID() { - return m_commandID; -} - -/** - * Set the command ID of the command being ack'd. - */ -void NetAckStage1CommandMsg::setCommandID(UnsignedShort commandID) { - m_commandID = commandID; -} - -/** - * Get the player id of the player who originally sent the command. - */ -UnsignedByte NetAckStage1CommandMsg::getOriginalPlayerID() { - return m_originalPlayerID; -} - -/** - * Set the player id of the player who originally sent the command. - */ -void NetAckStage1CommandMsg::setOriginalPlayerID(UnsignedByte originalPlayerID) { - m_originalPlayerID = originalPlayerID; -} - -Int NetAckStage1CommandMsg::getSortNumber() { - return m_commandID; -} - -//------------------------- -// NetAckStage2CommandMsg -//------------------------- -/** - * Constructor. Sets the member variables according to the given message. - */ -NetAckStage2CommandMsg::NetAckStage2CommandMsg(NetCommandMsg *msg) : NetCommandMsg() { - m_commandID = msg->getID(); - m_commandType = NETCOMMANDTYPE_ACKSTAGE2; - m_originalPlayerID = msg->getPlayerID(); -} - -/** - * Constructor. Sets the member variables to default values. - */ -NetAckStage2CommandMsg::NetAckStage2CommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_ACKSTAGE2; -} - -/** - * Destructor. - */ -NetAckStage2CommandMsg::~NetAckStage2CommandMsg() { -} - -/** - * Returns the command ID of the command being ack'd. - */ -UnsignedShort NetAckStage2CommandMsg::getCommandID() { - return m_commandID; -} - -/** - * Set the command ID of the command being ack'd. - */ -void NetAckStage2CommandMsg::setCommandID(UnsignedShort commandID) { - m_commandID = commandID; -} - -/** - * Get the player id of the player who originally sent the command. - */ -UnsignedByte NetAckStage2CommandMsg::getOriginalPlayerID() { - return m_originalPlayerID; -} - -/** - * Set the player id of the player who originally sent the command. - */ -void NetAckStage2CommandMsg::setOriginalPlayerID(UnsignedByte originalPlayerID) { - m_originalPlayerID = originalPlayerID; -} - -Int NetAckStage2CommandMsg::getSortNumber() { - return m_commandID; -} - -//------------------------- -// NetFrameCommandMsg -//------------------------- -/** - * Constructor. - */ -NetFrameCommandMsg::NetFrameCommandMsg() : NetCommandMsg() { - m_commandCount = 0; - m_commandType = NETCOMMANDTYPE_FRAMEINFO; -} - -/** - * Destructor - */ -NetFrameCommandMsg::~NetFrameCommandMsg() { -} - -/** - * Set the command count of this frame. - */ -void NetFrameCommandMsg::setCommandCount(UnsignedShort commandCount) { - m_commandCount = commandCount; -} - -/** - * Return the command count of this frame. - */ -UnsignedShort NetFrameCommandMsg::getCommandCount() { - return m_commandCount; -} - -//------------------------- -// NetPlayerLeaveCommandMsg -//------------------------- -/** - * Constructor - */ -NetPlayerLeaveCommandMsg::NetPlayerLeaveCommandMsg() : NetCommandMsg() { - m_leavingPlayerID = 0; - m_commandType = NETCOMMANDTYPE_PLAYERLEAVE; -} - -/** - * Destructor - */ -NetPlayerLeaveCommandMsg::~NetPlayerLeaveCommandMsg() { -} - -/** - * Get the id of the player leaving the game. - */ -UnsignedByte NetPlayerLeaveCommandMsg::getLeavingPlayerID() { - return m_leavingPlayerID; -} - -/** - * Set the id of the player leaving the game. - */ -void NetPlayerLeaveCommandMsg::setLeavingPlayerID(UnsignedByte id) { - m_leavingPlayerID = id; -} - -//------------------------- -// NetRunAheadMetricsCommandMsg -//------------------------- -/** - * Constructor - */ -NetRunAheadMetricsCommandMsg::NetRunAheadMetricsCommandMsg() : NetCommandMsg() { - m_averageLatency = 0.0; - m_averageFps = 0; - m_commandType = NETCOMMANDTYPE_RUNAHEADMETRICS; -} - -/** - * Destructor - */ -NetRunAheadMetricsCommandMsg::~NetRunAheadMetricsCommandMsg() { -} - -/** - * set the average latency - */ -void NetRunAheadMetricsCommandMsg::setAverageLatency(Real avgLat) { - m_averageLatency = avgLat; -} - -/** - * get the average latency - */ -Real NetRunAheadMetricsCommandMsg::getAverageLatency() { - return m_averageLatency; -} - -/** - * set the average fps - */ -void NetRunAheadMetricsCommandMsg::setAverageFps(Int fps) { - m_averageFps = fps; -} - -/** - * get the average fps - */ -Int NetRunAheadMetricsCommandMsg::getAverageFps() { - return m_averageFps; -} - -//------------------------- -// NetRunAheadCommandMsg -//------------------------- -NetRunAheadCommandMsg::NetRunAheadCommandMsg() : NetCommandMsg() { - m_runAhead = min(max(20, MIN_RUNAHEAD), MAX_FRAMES_AHEAD/2); - m_frameRate = 30; - m_commandType = NETCOMMANDTYPE_RUNAHEAD; -} - -NetRunAheadCommandMsg::~NetRunAheadCommandMsg() { -} - -UnsignedShort NetRunAheadCommandMsg::getRunAhead() { - return m_runAhead; -} - -void NetRunAheadCommandMsg::setRunAhead(UnsignedShort runAhead) { - m_runAhead = runAhead; -} - -UnsignedByte NetRunAheadCommandMsg::getFrameRate() { - return m_frameRate; -} - -void NetRunAheadCommandMsg::setFrameRate(UnsignedByte frameRate) { - m_frameRate = frameRate; -} - -//------------------------- -// NetDestroyPlayerCommandMsg -//------------------------- -/** - * Constructor - */ -NetDestroyPlayerCommandMsg::NetDestroyPlayerCommandMsg() : NetCommandMsg() -{ - m_playerIndex = 0; - m_commandType = NETCOMMANDTYPE_DESTROYPLAYER; -} - -/** - * Destructor - */ -NetDestroyPlayerCommandMsg::~NetDestroyPlayerCommandMsg() -{ -} - -/** - * set the CRC - */ -void NetDestroyPlayerCommandMsg::setPlayerIndex( UnsignedInt playerIndex ) -{ - m_playerIndex = playerIndex; -} - -/** - * get the average CRC - */ -UnsignedInt NetDestroyPlayerCommandMsg::getPlayerIndex( void ) -{ - return m_playerIndex; -} - -//------------------------- -// NetKeepAliveCommandMsg -//------------------------- -/** - * Constructor - */ -NetKeepAliveCommandMsg::NetKeepAliveCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_KEEPALIVE; -} - -NetKeepAliveCommandMsg::~NetKeepAliveCommandMsg() { -} - -//------------------------- -// NetDisconnectKeepAliveCommandMsg -//------------------------- -/** - * Constructor - */ -NetDisconnectKeepAliveCommandMsg::NetDisconnectKeepAliveCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_DISCONNECTKEEPALIVE; -} - -NetDisconnectKeepAliveCommandMsg::~NetDisconnectKeepAliveCommandMsg() { -} - -//------------------------- -// NetDisconnectPlayerCommandMsg -//------------------------- -/** - * Constructor - */ -NetDisconnectPlayerCommandMsg::NetDisconnectPlayerCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_DISCONNECTPLAYER; - m_disconnectSlot = 0; -} - -/** - * Destructor - */ -NetDisconnectPlayerCommandMsg::~NetDisconnectPlayerCommandMsg() { -} - -/** - * Returns the disconnecting slot number - */ -UnsignedByte NetDisconnectPlayerCommandMsg::getDisconnectSlot() { - return m_disconnectSlot; -} - -/** - * Sets the disconnecting slot number - */ -void NetDisconnectPlayerCommandMsg::setDisconnectSlot(UnsignedByte slot) { - m_disconnectSlot = slot; -} - -/** - * Sets the disconnect frame - */ -void NetDisconnectPlayerCommandMsg::setDisconnectFrame(UnsignedInt frame) { - m_disconnectFrame = frame; -} - -/** - * returns the disconnect frame - */ -UnsignedInt NetDisconnectPlayerCommandMsg::getDisconnectFrame() { - return m_disconnectFrame; -} - -//------------------------- -// NetPacketRouterQueryCommandMsg -//------------------------- -/** - * Constructor - */ -NetPacketRouterQueryCommandMsg::NetPacketRouterQueryCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_PACKETROUTERQUERY; -} - -/** - * Destructor - */ -NetPacketRouterQueryCommandMsg::~NetPacketRouterQueryCommandMsg() { -} - -//------------------------- -// NetPacketRouterAckCommandMsg -//------------------------- -/** - * Constructor - */ -NetPacketRouterAckCommandMsg::NetPacketRouterAckCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_PACKETROUTERACK; -} - -/** - * Destructor - */ -NetPacketRouterAckCommandMsg::~NetPacketRouterAckCommandMsg() { -} - -//------------------------- -// NetDisconnectChatCommandMsg -//------------------------- -/** - * Constructor - */ -NetDisconnectChatCommandMsg::NetDisconnectChatCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_DISCONNECTCHAT; -} - -/** - * Destructor - */ -NetDisconnectChatCommandMsg::~NetDisconnectChatCommandMsg() { -} - -/** - * Set the chat text for this message. - */ -void NetDisconnectChatCommandMsg::setText(UnicodeString text) { - m_text = text; -} - -/** - * Get the chat text for this message. - */ -UnicodeString NetDisconnectChatCommandMsg::getText() { - return m_text; -} - -//------------------------- -// NetChatCommandMsg -//------------------------- -/** - * Constructor - */ -NetChatCommandMsg::NetChatCommandMsg() : NetCommandMsg() -{ - m_commandType = NETCOMMANDTYPE_CHAT; - //added by Sadullah Nader - //Initializations inserted - m_playerMask = 0; - // -} - -/** - * Destructor - */ -NetChatCommandMsg::~NetChatCommandMsg() -{ -} - -/** - * Set the chat text for this message. - */ -void NetChatCommandMsg::setText(UnicodeString text) -{ - m_text = text; -} - -/** - * Get the chat text for this message. - */ -UnicodeString NetChatCommandMsg::getText() -{ - return m_text; -} - -/** - * Get the bitmask of chat recipients from this message. - */ -Int NetChatCommandMsg::getPlayerMask() -{ - return m_playerMask; -} - -/** - * Set a bitmask of chat recipients in this message. - */ -void NetChatCommandMsg::setPlayerMask( Int playerMask ) -{ - m_playerMask = playerMask; -} - -//------------------------- -// NetDisconnectVoteCommandMsg -//------------------------- -/** - * Constructor - */ -NetDisconnectVoteCommandMsg::NetDisconnectVoteCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_DISCONNECTVOTE; - m_slot = 0; -} - -/** - * Destructor - */ -NetDisconnectVoteCommandMsg::~NetDisconnectVoteCommandMsg() { -} - -/** - * Set the slot that is being voted for. - */ -void NetDisconnectVoteCommandMsg::setSlot(UnsignedByte slot) { - m_slot = slot; -} - -/** - * Get the slot that is being voted for. - */ -UnsignedByte NetDisconnectVoteCommandMsg::getSlot() { - return m_slot; -} - -/** - * Get the vote frame. - */ -UnsignedInt NetDisconnectVoteCommandMsg::getVoteFrame() { - return m_voteFrame; -} - -/** - * Set the vote frame. - */ -void NetDisconnectVoteCommandMsg::setVoteFrame(UnsignedInt voteFrame) { - m_voteFrame = voteFrame; -} - -//------------------------- -// NetProgressCommandMsg -//------------------------- -NetProgressCommandMsg::NetProgressCommandMsg( void ) : NetCommandMsg() -{ - m_commandType = NETCOMMANDTYPE_PROGRESS; - m_percent = 0; -} - -NetProgressCommandMsg::~NetProgressCommandMsg( void ) {} - -UnsignedByte NetProgressCommandMsg::getPercentage() -{ - return m_percent; -} - -void NetProgressCommandMsg::setPercentage( UnsignedByte percent ) -{ - m_percent = percent; -} - -//------------------------- -// NetWrapperCommandMsg -//------------------------- -NetWrapperCommandMsg::NetWrapperCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_WRAPPER; - m_numChunks = 0; - m_data = NULL; - m_totalDataLength = 0; - m_chunkNumber = 0; - m_dataLength = 0; - m_dataOffset = 0; - m_wrappedCommandID = 0; -} - -NetWrapperCommandMsg::~NetWrapperCommandMsg() { - if (m_data != NULL) { - delete m_data; - m_data = NULL; - } -} - -UnsignedByte * NetWrapperCommandMsg::getData() { - return m_data; -} - -void NetWrapperCommandMsg::setData(UnsignedByte *data, UnsignedInt dataLength) -{ - if (m_data != NULL) { - delete m_data; - m_data = NULL; - } - - m_data = NEW UnsignedByte[dataLength]; // pool[]ify - memcpy(m_data, data, dataLength); - m_dataLength = dataLength; -} - -UnsignedInt NetWrapperCommandMsg::getDataLength() { - return m_dataLength; -} - -UnsignedInt NetWrapperCommandMsg::getDataOffset() { - return m_dataOffset; -} - -void NetWrapperCommandMsg::setDataOffset(UnsignedInt offset) { - m_dataOffset = offset; -} - -UnsignedInt NetWrapperCommandMsg::getChunkNumber() { - return m_chunkNumber; -} - -void NetWrapperCommandMsg::setChunkNumber(UnsignedInt chunkNumber) { - m_chunkNumber = chunkNumber; -} - -UnsignedInt NetWrapperCommandMsg::getNumChunks() { - return m_numChunks; -} - -void NetWrapperCommandMsg::setNumChunks(UnsignedInt numChunks) { - m_numChunks = numChunks; -} - -UnsignedInt NetWrapperCommandMsg::getTotalDataLength() { - return m_totalDataLength; -} - -void NetWrapperCommandMsg::setTotalDataLength(UnsignedInt totalDataLength) { - m_totalDataLength = totalDataLength; -} - -UnsignedShort NetWrapperCommandMsg::getWrappedCommandID() { - return m_wrappedCommandID; -} - -void NetWrapperCommandMsg::setWrappedCommandID(UnsignedShort wrappedCommandID) { - m_wrappedCommandID = wrappedCommandID; -} - -//------------------------- -// NetFileCommandMsg -//------------------------- -NetFileCommandMsg::NetFileCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_FILE; - m_data = NULL; - m_portableFilename.clear(); - m_dataLength = 0; -} - -NetFileCommandMsg::~NetFileCommandMsg() { - if (m_data != NULL) { - delete[] m_data; - m_data = NULL; - } -} - -AsciiString NetFileCommandMsg::getRealFilename() -{ - return TheGameState->portableMapPathToRealMapPath(m_portableFilename); -} - -void NetFileCommandMsg::setRealFilename(AsciiString filename) -{ - m_portableFilename = TheGameState->realMapPathToPortableMapPath(filename); -} - -UnsignedInt NetFileCommandMsg::getFileLength() { - return m_dataLength; -} - -UnsignedByte * NetFileCommandMsg::getFileData() { - return m_data; -} - -void NetFileCommandMsg::setFileData(UnsignedByte *data, UnsignedInt dataLength) -{ - m_dataLength = dataLength; - m_data = NEW UnsignedByte[dataLength]; // pool[]ify - memcpy(m_data, data, dataLength); -} - -//------------------------- -// NetFileAnnounceCommandMsg -//------------------------- -NetFileAnnounceCommandMsg::NetFileAnnounceCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_FILEANNOUNCE; - m_portableFilename.clear(); - m_fileID = 0; - m_playerMask = 0; -} - -NetFileAnnounceCommandMsg::~NetFileAnnounceCommandMsg() { -} - -AsciiString NetFileAnnounceCommandMsg::getRealFilename() -{ - return TheGameState->portableMapPathToRealMapPath(m_portableFilename); -} - -void NetFileAnnounceCommandMsg::setRealFilename(AsciiString filename) -{ - m_portableFilename = TheGameState->realMapPathToPortableMapPath(filename); -} - -UnsignedShort NetFileAnnounceCommandMsg::getFileID() { - return m_fileID; -} - -void NetFileAnnounceCommandMsg::setFileID(UnsignedShort fileID) { - m_fileID = fileID; -} - -UnsignedByte NetFileAnnounceCommandMsg::getPlayerMask(void) { - return m_playerMask; -} - -void NetFileAnnounceCommandMsg::setPlayerMask(UnsignedByte playerMask) { - m_playerMask = playerMask; -} - - -//------------------------- -// NetFileProgressCommandMsg -//------------------------- -NetFileProgressCommandMsg::NetFileProgressCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_FILEPROGRESS; - m_fileID = 0; - m_progress = 0; -} - -NetFileProgressCommandMsg::~NetFileProgressCommandMsg() { -} - -UnsignedShort NetFileProgressCommandMsg::getFileID() { - return m_fileID; -} - -void NetFileProgressCommandMsg::setFileID(UnsignedShort val) { - m_fileID = val; -} - -Int NetFileProgressCommandMsg::getProgress() { - return m_progress; -} - -void NetFileProgressCommandMsg::setProgress(Int val) { - m_progress = val; -} - -//------------------------- -// NetDisconnectFrameCommandMsg -//------------------------- -NetDisconnectFrameCommandMsg::NetDisconnectFrameCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_DISCONNECTFRAME; - m_disconnectFrame = 0; -} - -NetDisconnectFrameCommandMsg::~NetDisconnectFrameCommandMsg() { -} - -UnsignedInt NetDisconnectFrameCommandMsg::getDisconnectFrame() { - return m_disconnectFrame; -} - -void NetDisconnectFrameCommandMsg::setDisconnectFrame(UnsignedInt disconnectFrame) { - m_disconnectFrame = disconnectFrame; -} - -//------------------------- -// NetDisconnectScreenOffCommandMsg -//------------------------- -NetDisconnectScreenOffCommandMsg::NetDisconnectScreenOffCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_DISCONNECTSCREENOFF; - m_newFrame = 0; -} - -NetDisconnectScreenOffCommandMsg::~NetDisconnectScreenOffCommandMsg() { -} - -UnsignedInt NetDisconnectScreenOffCommandMsg::getNewFrame() { - return m_newFrame; -} - -void NetDisconnectScreenOffCommandMsg::setNewFrame(UnsignedInt newFrame) { - m_newFrame = newFrame; -} - -//------------------------- -// NetFrameResendRequestCommandMsg -//------------------------- -NetFrameResendRequestCommandMsg::NetFrameResendRequestCommandMsg() : NetCommandMsg() { - m_commandType = NETCOMMANDTYPE_FRAMERESENDREQUEST; - m_frameToResend = 0; -} - -NetFrameResendRequestCommandMsg::~NetFrameResendRequestCommandMsg() { -} - -UnsignedInt NetFrameResendRequestCommandMsg::getFrameToResend() { - return m_frameToResend; -} - -void NetFrameResendRequestCommandMsg::setFrameToResend(UnsignedInt frame) { - m_frameToResend = frame; -} +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +//////////////////////////////////////////////////////////////////////////////// +// // +// (c) 2001-2003 Electronic Arts Inc. // +// // +//////////////////////////////////////////////////////////////////////////////// + + +#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine + +#include "GameNetwork/NetCommandMsg.h" +#include "Common/GameState.h" +#include "Common/PlayerList.h" +#include "Common/Player.h" + +/** + * Base constructor + */ +NetCommandMsg::NetCommandMsg() +{ + //Added By Sadullah Nader + //Initializations inserted + m_executionFrame = 0; + m_id = 0; + m_playerID = 0; + + // + m_timestamp = 0; + m_referenceCount = 1; // start this off as 1. This means that an "attach" is implied by creating a NetCommandMsg object. + m_commandType = NETCOMMANDTYPE_UNKNOWN; +} + +/** + * Destructor + */ +NetCommandMsg::~NetCommandMsg() { +} + +/** + * Adds one to the reference count. + */ +void NetCommandMsg::attach() { + ++m_referenceCount; +} + +/** + * Subtracts one from the reference count. If the reference count is 0, the this object is destroyed. + */ +void NetCommandMsg::detach() { + --m_referenceCount; + if (m_referenceCount == 0) { + deleteInstance(this); + return; + } + DEBUG_ASSERTCRASH(m_referenceCount > 0, ("Invalid reference count for NetCommandMsg")); // Just to make sure... + if (m_referenceCount < 0) { + deleteInstance(this); + } +} + +/** + * Returns the value by which this type of message should be sorted. + */ +Int NetCommandMsg::getSortNumber() { + return m_id; +} + +//------------------------------- +// NetGameCommandMsg +//------------------------------- + +/** + * Constructor with no argument, sets everything to default values. + */ +NetGameCommandMsg::NetGameCommandMsg() : NetCommandMsg() { + //Added By Sadullah Nader + //Initializations inserted + m_argSize = 0; + m_numArgs = 0; + // + + m_type = (GameMessage::Type)0; + m_commandType = NETCOMMANDTYPE_GAMECOMMAND; + m_argList = NULL; + m_argTail = NULL; +} + +/** + * Constructor with a GameMessage argument. Sets member variables appropriately for this GameMessage. + * Also copies all the arguments. + */ +NetGameCommandMsg::NetGameCommandMsg(GameMessage *msg) : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_GAMECOMMAND; + + m_type = msg->getType(); + Int count = msg->getArgumentCount(); + for (Int i = 0; i < count; ++i) { + addArgument(msg->getArgumentDataType(i), *(msg->getArgument(i))); + } +} + +/** + * Destructor + */ +NetGameCommandMsg::~NetGameCommandMsg() { + GameMessageArgument *arg = m_argList; + while (arg != NULL) { + m_argList = m_argList->m_next; + deleteInstance(arg); + arg = m_argList; + } +} + +/** + * Add an argument to this command. + */ +void NetGameCommandMsg::addArgument(const GameMessageArgumentDataType type, GameMessageArgumentType arg) +{ + if (m_argTail == NULL) { + m_argList = newInstance(GameMessageArgument); + m_argTail = m_argList; + m_argList->m_data = arg; + m_argList->m_type = type; + m_argList->m_next = NULL; + return; + } + + GameMessageArgument *newArg = newInstance(GameMessageArgument); + newArg->m_data = arg; + newArg->m_type = type; + newArg->m_next = NULL; + m_argTail->m_next = newArg; + m_argTail = newArg; +} + +// here's where we figure out which slot corresponds to which player +static Int indexFromMask(UnsignedInt mask) +{ + Player *player = NULL; + Int i; + + for( i = 0; i < MAX_PLAYER_COUNT; i++ ) + { + player = ThePlayerList->getNthPlayer( i ); + if( player && player->getPlayerMask() == mask ) + return i; + } // end for i + + return -1; +} + +/** + * Construct a new GameMessage object from the data in this object. + */ +GameMessage *NetGameCommandMsg::constructGameMessage() +{ + GameMessage *retval = newInstance(GameMessage)(m_type); + + AsciiString name; + name.format("player%d", getPlayerID()); + retval->friend_setPlayerIndex( ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(name))->getPlayerIndex()); +// retval->friend_setPlayerIndex(indexFromMask(ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(name))->getPlayerMask())); + + GameMessageArgument *arg = m_argList; + while (arg != NULL) { +// retval->appendGenericArgument(arg->m_data); + switch (arg->m_type) { + + case ARGUMENTDATATYPE_INTEGER: + retval->appendIntegerArgument(arg->m_data.integer); + break; + case ARGUMENTDATATYPE_REAL: + retval->appendRealArgument(arg->m_data.real); + break; + case ARGUMENTDATATYPE_BOOLEAN: + retval->appendBooleanArgument(arg->m_data.boolean); + break; + case ARGUMENTDATATYPE_OBJECTID: + retval->appendObjectIDArgument(arg->m_data.objectID); + break; + case ARGUMENTDATATYPE_DRAWABLEID: + retval->appendDrawableIDArgument(arg->m_data.drawableID); + break; + case ARGUMENTDATATYPE_TEAMID: + retval->appendTeamIDArgument(arg->m_data.teamID); + break; + case ARGUMENTDATATYPE_LOCATION: + retval->appendLocationArgument(arg->m_data.location); + break; + case ARGUMENTDATATYPE_PIXEL: + retval->appendPixelArgument(arg->m_data.pixel); + break; + case ARGUMENTDATATYPE_PIXELREGION: + retval->appendPixelRegionArgument(arg->m_data.pixelRegion); + break; + case ARGUMENTDATATYPE_TIMESTAMP: + retval->appendTimestampArgument(arg->m_data.timestamp); + break; + case ARGUMENTDATATYPE_WIDECHAR: + retval->appendWideCharArgument(arg->m_data.wChar); + break; + + } // switch (arg->m_type) + + arg = arg->m_next; + } + return retval; +} + +/** + * Sets the type of game message + */ +void NetGameCommandMsg::setGameMessageType(GameMessage::Type type) { + m_type = type; +} + +//------------------------- +// NetAckBothCommandMsg +//------------------------- +/** + * Constructor. Sets the member variables according to the given message. + */ +NetAckBothCommandMsg::NetAckBothCommandMsg(NetCommandMsg *msg) : NetCommandMsg() { + m_commandID = msg->getID(); + m_commandType = NETCOMMANDTYPE_ACKBOTH; + m_originalPlayerID = msg->getPlayerID(); +} + +/** + * Constructor. Sets the member variables to default values. + */ +NetAckBothCommandMsg::NetAckBothCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_ACKBOTH; +} + +/** + * Destructor. + */ +NetAckBothCommandMsg::~NetAckBothCommandMsg() { +} + +/** + * Returns the command ID of the command being ack'd. + */ +UnsignedShort NetAckBothCommandMsg::getCommandID() { + return m_commandID; +} + +/** + * Set the command ID of the command being ack'd. + */ +void NetAckBothCommandMsg::setCommandID(UnsignedShort commandID) { + m_commandID = commandID; +} + +/** + * Get the player id of the player who originally sent the command. + */ +UnsignedByte NetAckBothCommandMsg::getOriginalPlayerID() { + return m_originalPlayerID; +} + +/** + * Set the player id of the player who originally sent the command. + */ +void NetAckBothCommandMsg::setOriginalPlayerID(UnsignedByte originalPlayerID) { + m_originalPlayerID = originalPlayerID; +} + +Int NetAckBothCommandMsg::getSortNumber() { + return m_commandID; +} + +//------------------------- +// NetAckStage1CommandMsg +//------------------------- +/** + * Constructor. Sets the member variables according to the given message. + */ +NetAckStage1CommandMsg::NetAckStage1CommandMsg(NetCommandMsg *msg) : NetCommandMsg() { + m_commandID = msg->getID(); + m_commandType = NETCOMMANDTYPE_ACKSTAGE1; + m_originalPlayerID = msg->getPlayerID(); +} + +/** + * Constructor. Sets the member variables to default values. + */ +NetAckStage1CommandMsg::NetAckStage1CommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_ACKSTAGE1; +} + +/** + * Destructor. + */ +NetAckStage1CommandMsg::~NetAckStage1CommandMsg() { +} + +/** + * Returns the command ID of the command being ack'd. + */ +UnsignedShort NetAckStage1CommandMsg::getCommandID() { + return m_commandID; +} + +/** + * Set the command ID of the command being ack'd. + */ +void NetAckStage1CommandMsg::setCommandID(UnsignedShort commandID) { + m_commandID = commandID; +} + +/** + * Get the player id of the player who originally sent the command. + */ +UnsignedByte NetAckStage1CommandMsg::getOriginalPlayerID() { + return m_originalPlayerID; +} + +/** + * Set the player id of the player who originally sent the command. + */ +void NetAckStage1CommandMsg::setOriginalPlayerID(UnsignedByte originalPlayerID) { + m_originalPlayerID = originalPlayerID; +} + +Int NetAckStage1CommandMsg::getSortNumber() { + return m_commandID; +} + +//------------------------- +// NetAckStage2CommandMsg +//------------------------- +/** + * Constructor. Sets the member variables according to the given message. + */ +NetAckStage2CommandMsg::NetAckStage2CommandMsg(NetCommandMsg *msg) : NetCommandMsg() { + m_commandID = msg->getID(); + m_commandType = NETCOMMANDTYPE_ACKSTAGE2; + m_originalPlayerID = msg->getPlayerID(); +} + +/** + * Constructor. Sets the member variables to default values. + */ +NetAckStage2CommandMsg::NetAckStage2CommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_ACKSTAGE2; +} + +/** + * Destructor. + */ +NetAckStage2CommandMsg::~NetAckStage2CommandMsg() { +} + +/** + * Returns the command ID of the command being ack'd. + */ +UnsignedShort NetAckStage2CommandMsg::getCommandID() { + return m_commandID; +} + +/** + * Set the command ID of the command being ack'd. + */ +void NetAckStage2CommandMsg::setCommandID(UnsignedShort commandID) { + m_commandID = commandID; +} + +/** + * Get the player id of the player who originally sent the command. + */ +UnsignedByte NetAckStage2CommandMsg::getOriginalPlayerID() { + return m_originalPlayerID; +} + +/** + * Set the player id of the player who originally sent the command. + */ +void NetAckStage2CommandMsg::setOriginalPlayerID(UnsignedByte originalPlayerID) { + m_originalPlayerID = originalPlayerID; +} + +Int NetAckStage2CommandMsg::getSortNumber() { + return m_commandID; +} + +//------------------------- +// NetFrameCommandMsg +//------------------------- +/** + * Constructor. + */ +NetFrameCommandMsg::NetFrameCommandMsg() : NetCommandMsg() { + m_commandCount = 0; + m_commandType = NETCOMMANDTYPE_FRAMEINFO; +} + +/** + * Destructor + */ +NetFrameCommandMsg::~NetFrameCommandMsg() { +} + +/** + * Set the command count of this frame. + */ +void NetFrameCommandMsg::setCommandCount(UnsignedShort commandCount) { + m_commandCount = commandCount; +} + +/** + * Return the command count of this frame. + */ +UnsignedShort NetFrameCommandMsg::getCommandCount() { + return m_commandCount; +} + +//------------------------- +// NetPlayerLeaveCommandMsg +//------------------------- +/** + * Constructor + */ +NetPlayerLeaveCommandMsg::NetPlayerLeaveCommandMsg() : NetCommandMsg() { + m_leavingPlayerID = 0; + m_commandType = NETCOMMANDTYPE_PLAYERLEAVE; +} + +/** + * Destructor + */ +NetPlayerLeaveCommandMsg::~NetPlayerLeaveCommandMsg() { +} + +/** + * Get the id of the player leaving the game. + */ +UnsignedByte NetPlayerLeaveCommandMsg::getLeavingPlayerID() { + return m_leavingPlayerID; +} + +/** + * Set the id of the player leaving the game. + */ +void NetPlayerLeaveCommandMsg::setLeavingPlayerID(UnsignedByte id) { + m_leavingPlayerID = id; +} + +//------------------------- +// NetRunAheadMetricsCommandMsg +//------------------------- +/** + * Constructor + */ +NetRunAheadMetricsCommandMsg::NetRunAheadMetricsCommandMsg() : NetCommandMsg() { + m_averageLatency = 0.0; + m_averageFps = 0; + m_commandType = NETCOMMANDTYPE_RUNAHEADMETRICS; +} + +/** + * Destructor + */ +NetRunAheadMetricsCommandMsg::~NetRunAheadMetricsCommandMsg() { +} + +/** + * set the average latency + */ +void NetRunAheadMetricsCommandMsg::setAverageLatency(Real avgLat) { + m_averageLatency = avgLat; +} + +/** + * get the average latency + */ +Real NetRunAheadMetricsCommandMsg::getAverageLatency() { + return m_averageLatency; +} + +/** + * set the average fps + */ +void NetRunAheadMetricsCommandMsg::setAverageFps(Int fps) { + m_averageFps = fps; +} + +/** + * get the average fps + */ +Int NetRunAheadMetricsCommandMsg::getAverageFps() { + return m_averageFps; +} + +//------------------------- +// NetRunAheadCommandMsg +//------------------------- +NetRunAheadCommandMsg::NetRunAheadCommandMsg() : NetCommandMsg() { + m_runAhead = min(max(20, MIN_RUNAHEAD), MAX_FRAMES_AHEAD/2); + m_frameRate = 30; + m_commandType = NETCOMMANDTYPE_RUNAHEAD; +} + +NetRunAheadCommandMsg::~NetRunAheadCommandMsg() { +} + +UnsignedShort NetRunAheadCommandMsg::getRunAhead() { + return m_runAhead; +} + +void NetRunAheadCommandMsg::setRunAhead(UnsignedShort runAhead) { + m_runAhead = runAhead; +} + +UnsignedByte NetRunAheadCommandMsg::getFrameRate() { + return m_frameRate; +} + +void NetRunAheadCommandMsg::setFrameRate(UnsignedByte frameRate) { + m_frameRate = frameRate; +} + +//------------------------- +// NetDestroyPlayerCommandMsg +//------------------------- +/** + * Constructor + */ +NetDestroyPlayerCommandMsg::NetDestroyPlayerCommandMsg() : NetCommandMsg() +{ + m_playerIndex = 0; + m_commandType = NETCOMMANDTYPE_DESTROYPLAYER; +} + +/** + * Destructor + */ +NetDestroyPlayerCommandMsg::~NetDestroyPlayerCommandMsg() +{ +} + +/** + * set the CRC + */ +void NetDestroyPlayerCommandMsg::setPlayerIndex( UnsignedInt playerIndex ) +{ + m_playerIndex = playerIndex; +} + +/** + * get the average CRC + */ +UnsignedInt NetDestroyPlayerCommandMsg::getPlayerIndex( void ) +{ + return m_playerIndex; +} + +//------------------------- +// NetKeepAliveCommandMsg +//------------------------- +/** + * Constructor + */ +NetKeepAliveCommandMsg::NetKeepAliveCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_KEEPALIVE; +} + +NetKeepAliveCommandMsg::~NetKeepAliveCommandMsg() { +} + +//------------------------- +// NetDisconnectKeepAliveCommandMsg +//------------------------- +/** + * Constructor + */ +NetDisconnectKeepAliveCommandMsg::NetDisconnectKeepAliveCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_DISCONNECTKEEPALIVE; +} + +NetDisconnectKeepAliveCommandMsg::~NetDisconnectKeepAliveCommandMsg() { +} + +//------------------------- +// NetDisconnectPlayerCommandMsg +//------------------------- +/** + * Constructor + */ +NetDisconnectPlayerCommandMsg::NetDisconnectPlayerCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_DISCONNECTPLAYER; + m_disconnectSlot = 0; +} + +/** + * Destructor + */ +NetDisconnectPlayerCommandMsg::~NetDisconnectPlayerCommandMsg() { +} + +/** + * Returns the disconnecting slot number + */ +UnsignedByte NetDisconnectPlayerCommandMsg::getDisconnectSlot() { + return m_disconnectSlot; +} + +/** + * Sets the disconnecting slot number + */ +void NetDisconnectPlayerCommandMsg::setDisconnectSlot(UnsignedByte slot) { + m_disconnectSlot = slot; +} + +/** + * Sets the disconnect frame + */ +void NetDisconnectPlayerCommandMsg::setDisconnectFrame(UnsignedInt frame) { + m_disconnectFrame = frame; +} + +/** + * returns the disconnect frame + */ +UnsignedInt NetDisconnectPlayerCommandMsg::getDisconnectFrame() { + return m_disconnectFrame; +} + +//------------------------- +// NetPacketRouterQueryCommandMsg +//------------------------- +/** + * Constructor + */ +NetPacketRouterQueryCommandMsg::NetPacketRouterQueryCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_PACKETROUTERQUERY; +} + +/** + * Destructor + */ +NetPacketRouterQueryCommandMsg::~NetPacketRouterQueryCommandMsg() { +} + +//------------------------- +// NetPacketRouterAckCommandMsg +//------------------------- +/** + * Constructor + */ +NetPacketRouterAckCommandMsg::NetPacketRouterAckCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_PACKETROUTERACK; +} + +/** + * Destructor + */ +NetPacketRouterAckCommandMsg::~NetPacketRouterAckCommandMsg() { +} + +//------------------------- +// NetDisconnectChatCommandMsg +//------------------------- +/** + * Constructor + */ +NetDisconnectChatCommandMsg::NetDisconnectChatCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_DISCONNECTCHAT; +} + +/** + * Destructor + */ +NetDisconnectChatCommandMsg::~NetDisconnectChatCommandMsg() { +} + +/** + * Set the chat text for this message. + */ +void NetDisconnectChatCommandMsg::setText(UnicodeString text) { + m_text = text; +} + +/** + * Get the chat text for this message. + */ +UnicodeString NetDisconnectChatCommandMsg::getText() { + return m_text; +} + +//------------------------- +// NetChatCommandMsg +//------------------------- +/** + * Constructor + */ +NetChatCommandMsg::NetChatCommandMsg() : NetCommandMsg() +{ + m_commandType = NETCOMMANDTYPE_CHAT; + //added by Sadullah Nader + //Initializations inserted + m_playerMask = 0; + // +} + +/** + * Destructor + */ +NetChatCommandMsg::~NetChatCommandMsg() +{ +} + +/** + * Set the chat text for this message. + */ +void NetChatCommandMsg::setText(UnicodeString text) +{ + m_text = text; +} + +/** + * Get the chat text for this message. + */ +UnicodeString NetChatCommandMsg::getText() +{ + return m_text; +} + +/** + * Get the bitmask of chat recipients from this message. + */ +Int NetChatCommandMsg::getPlayerMask() +{ + return m_playerMask; +} + +/** + * Set a bitmask of chat recipients in this message. + */ +void NetChatCommandMsg::setPlayerMask( Int playerMask ) +{ + m_playerMask = playerMask; +} + +//------------------------- +// NetDisconnectVoteCommandMsg +//------------------------- +/** + * Constructor + */ +NetDisconnectVoteCommandMsg::NetDisconnectVoteCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_DISCONNECTVOTE; + m_slot = 0; +} + +/** + * Destructor + */ +NetDisconnectVoteCommandMsg::~NetDisconnectVoteCommandMsg() { +} + +/** + * Set the slot that is being voted for. + */ +void NetDisconnectVoteCommandMsg::setSlot(UnsignedByte slot) { + m_slot = slot; +} + +/** + * Get the slot that is being voted for. + */ +UnsignedByte NetDisconnectVoteCommandMsg::getSlot() { + return m_slot; +} + +/** + * Get the vote frame. + */ +UnsignedInt NetDisconnectVoteCommandMsg::getVoteFrame() { + return m_voteFrame; +} + +/** + * Set the vote frame. + */ +void NetDisconnectVoteCommandMsg::setVoteFrame(UnsignedInt voteFrame) { + m_voteFrame = voteFrame; +} + +//------------------------- +// NetProgressCommandMsg +//------------------------- +NetProgressCommandMsg::NetProgressCommandMsg( void ) : NetCommandMsg() +{ + m_commandType = NETCOMMANDTYPE_PROGRESS; + m_percent = 0; +} + +NetProgressCommandMsg::~NetProgressCommandMsg( void ) {} + +UnsignedByte NetProgressCommandMsg::getPercentage() +{ + return m_percent; +} + +void NetProgressCommandMsg::setPercentage( UnsignedByte percent ) +{ + m_percent = percent; +} + +//------------------------- +// NetWrapperCommandMsg +//------------------------- +NetWrapperCommandMsg::NetWrapperCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_WRAPPER; + m_numChunks = 0; + m_data = NULL; + m_totalDataLength = 0; + m_chunkNumber = 0; + m_dataLength = 0; + m_dataOffset = 0; + m_wrappedCommandID = 0; +} + +NetWrapperCommandMsg::~NetWrapperCommandMsg() { + if (m_data != NULL) { + delete m_data; + m_data = NULL; + } +} + +UnsignedByte * NetWrapperCommandMsg::getData() { + return m_data; +} + +void NetWrapperCommandMsg::setData(UnsignedByte *data, UnsignedInt dataLength) +{ + if (m_data != NULL) { + delete m_data; + m_data = NULL; + } + + m_data = NEW UnsignedByte[dataLength]; // pool[]ify + memcpy(m_data, data, dataLength); + m_dataLength = dataLength; +} + +UnsignedInt NetWrapperCommandMsg::getDataLength() { + return m_dataLength; +} + +UnsignedInt NetWrapperCommandMsg::getDataOffset() { + return m_dataOffset; +} + +void NetWrapperCommandMsg::setDataOffset(UnsignedInt offset) { + m_dataOffset = offset; +} + +UnsignedInt NetWrapperCommandMsg::getChunkNumber() { + return m_chunkNumber; +} + +void NetWrapperCommandMsg::setChunkNumber(UnsignedInt chunkNumber) { + m_chunkNumber = chunkNumber; +} + +UnsignedInt NetWrapperCommandMsg::getNumChunks() { + return m_numChunks; +} + +void NetWrapperCommandMsg::setNumChunks(UnsignedInt numChunks) { + m_numChunks = numChunks; +} + +UnsignedInt NetWrapperCommandMsg::getTotalDataLength() { + return m_totalDataLength; +} + +void NetWrapperCommandMsg::setTotalDataLength(UnsignedInt totalDataLength) { + m_totalDataLength = totalDataLength; +} + +UnsignedShort NetWrapperCommandMsg::getWrappedCommandID() { + return m_wrappedCommandID; +} + +void NetWrapperCommandMsg::setWrappedCommandID(UnsignedShort wrappedCommandID) { + m_wrappedCommandID = wrappedCommandID; +} + +//------------------------- +// NetFileCommandMsg +//------------------------- +NetFileCommandMsg::NetFileCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_FILE; + m_data = NULL; + m_portableFilename.clear(); + m_dataLength = 0; +} + +NetFileCommandMsg::~NetFileCommandMsg() { + if (m_data != NULL) { + delete[] m_data; + m_data = NULL; + } +} + +AsciiString NetFileCommandMsg::getRealFilename() +{ + return TheGameState->portableMapPathToRealMapPath(m_portableFilename); +} + +void NetFileCommandMsg::setRealFilename(AsciiString filename) +{ + m_portableFilename = TheGameState->realMapPathToPortableMapPath(filename); +} + +UnsignedInt NetFileCommandMsg::getFileLength() { + return m_dataLength; +} + +UnsignedByte * NetFileCommandMsg::getFileData() { + return m_data; +} + +void NetFileCommandMsg::setFileData(UnsignedByte *data, UnsignedInt dataLength) +{ + m_dataLength = dataLength; + m_data = NEW UnsignedByte[dataLength]; // pool[]ify + memcpy(m_data, data, dataLength); +} + +//------------------------- +// NetFileAnnounceCommandMsg +//------------------------- +NetFileAnnounceCommandMsg::NetFileAnnounceCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_FILEANNOUNCE; + m_portableFilename.clear(); + m_fileID = 0; + m_playerMask = 0; +} + +NetFileAnnounceCommandMsg::~NetFileAnnounceCommandMsg() { +} + +AsciiString NetFileAnnounceCommandMsg::getRealFilename() +{ + return TheGameState->portableMapPathToRealMapPath(m_portableFilename); +} + +void NetFileAnnounceCommandMsg::setRealFilename(AsciiString filename) +{ + m_portableFilename = TheGameState->realMapPathToPortableMapPath(filename); +} + +UnsignedShort NetFileAnnounceCommandMsg::getFileID() { + return m_fileID; +} + +void NetFileAnnounceCommandMsg::setFileID(UnsignedShort fileID) { + m_fileID = fileID; +} + +UnsignedByte NetFileAnnounceCommandMsg::getPlayerMask(void) { + return m_playerMask; +} + +void NetFileAnnounceCommandMsg::setPlayerMask(UnsignedByte playerMask) { + m_playerMask = playerMask; +} + + +//------------------------- +// NetFileProgressCommandMsg +//------------------------- +NetFileProgressCommandMsg::NetFileProgressCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_FILEPROGRESS; + m_fileID = 0; + m_progress = 0; +} + +NetFileProgressCommandMsg::~NetFileProgressCommandMsg() { +} + +UnsignedShort NetFileProgressCommandMsg::getFileID() { + return m_fileID; +} + +void NetFileProgressCommandMsg::setFileID(UnsignedShort val) { + m_fileID = val; +} + +Int NetFileProgressCommandMsg::getProgress() { + return m_progress; +} + +void NetFileProgressCommandMsg::setProgress(Int val) { + m_progress = val; +} + +//------------------------- +// NetDisconnectFrameCommandMsg +//------------------------- +NetDisconnectFrameCommandMsg::NetDisconnectFrameCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_DISCONNECTFRAME; + m_disconnectFrame = 0; +} + +NetDisconnectFrameCommandMsg::~NetDisconnectFrameCommandMsg() { +} + +UnsignedInt NetDisconnectFrameCommandMsg::getDisconnectFrame() { + return m_disconnectFrame; +} + +void NetDisconnectFrameCommandMsg::setDisconnectFrame(UnsignedInt disconnectFrame) { + m_disconnectFrame = disconnectFrame; +} + +//------------------------- +// NetDisconnectScreenOffCommandMsg +//------------------------- +NetDisconnectScreenOffCommandMsg::NetDisconnectScreenOffCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_DISCONNECTSCREENOFF; + m_newFrame = 0; +} + +NetDisconnectScreenOffCommandMsg::~NetDisconnectScreenOffCommandMsg() { +} + +UnsignedInt NetDisconnectScreenOffCommandMsg::getNewFrame() { + return m_newFrame; +} + +void NetDisconnectScreenOffCommandMsg::setNewFrame(UnsignedInt newFrame) { + m_newFrame = newFrame; +} + +//------------------------- +// NetFrameResendRequestCommandMsg +//------------------------- +NetFrameResendRequestCommandMsg::NetFrameResendRequestCommandMsg() : NetCommandMsg() { + m_commandType = NETCOMMANDTYPE_FRAMERESENDREQUEST; + m_frameToResend = 0; +} + +NetFrameResendRequestCommandMsg::~NetFrameResendRequestCommandMsg() { +} + +UnsignedInt NetFrameResendRequestCommandMsg::getFrameToResend() { + return m_frameToResend; +} + +void NetFrameResendRequestCommandMsg::setFrameToResend(UnsignedInt frame) { + m_frameToResend = frame; +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetPacket.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetPacket.cpp index 9b839f2e02a..b860cd8c62b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetPacket.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetPacket.cpp @@ -1,5854 +1,6096 @@ -/* -** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -////////// NetPacket.cpp /////////////////////////// - -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - -#include "GameNetwork/NetPacket.h" -#include "GameNetwork/NetCommandMsg.h" -#include "GameNetwork/NetworkDefs.h" -#include "GameNetwork/networkutil.h" -#include "GameNetwork/GameMessageParser.h" - - -// This function assumes that all of the fields are either of default value or are -// present in the raw data. -NetCommandRef * NetPacket::ConstructNetCommandMsgFromRawData(UnsignedByte *data, UnsignedShort dataLength) { - NetCommandType commandType = NETCOMMANDTYPE_GAMECOMMAND; - UnsignedShort commandID = 0; - UnsignedInt frame = 0; - UnsignedByte playerID = 0; - UnsignedByte relay = 0; - - Int offset = 0; - Bool notDone = TRUE; - NetCommandRef *ref = NULL; - NetCommandMsg *msg = NULL; - - while ((offset < (Int)dataLength) && notDone) { - if (data[offset] == 'T') { - ++offset; - memcpy(&commandType, data + offset, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - } else if (data[offset] == 'R') { - ++offset; - memcpy(&relay, data + offset, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - } else if (data[offset] == 'P') { - ++offset; - memcpy(&playerID, data + offset, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - } else if (data[offset] == 'C') { - ++offset; - memcpy(&commandID, data + offset, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - } else if (data[offset] == 'F') { - ++offset; - memcpy(&frame, data + offset, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - } else if (data[offset] == 'D') { - ++offset; - if (commandType == NETCOMMANDTYPE_GAMECOMMAND) { - msg = readGameMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_ACKBOTH) { - msg = readAckBothMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_ACKSTAGE1) { - msg = readAckStage1Message(data, offset); - } else if (commandType == NETCOMMANDTYPE_ACKSTAGE2) { - msg = readAckStage2Message(data, offset); - } else if (commandType == NETCOMMANDTYPE_FRAMEINFO) { - msg = readFrameMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_PLAYERLEAVE) { - msg = readPlayerLeaveMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_RUNAHEADMETRICS) { - msg = readRunAheadMetricsMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_RUNAHEAD) { - msg = readRunAheadMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DESTROYPLAYER) { - msg = readDestroyPlayerMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_KEEPALIVE) { - msg = readKeepAliveMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DISCONNECTKEEPALIVE) { - msg = readDisconnectKeepAliveMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DISCONNECTPLAYER) { - msg = readDisconnectPlayerMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_PACKETROUTERQUERY) { - msg = readPacketRouterQueryMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_PACKETROUTERACK) { - msg = readPacketRouterAckMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DISCONNECTCHAT) { - msg = readDisconnectChatMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DISCONNECTVOTE) { - msg = readDisconnectVoteMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_CHAT) { - msg = readChatMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_PROGRESS) { - msg = readProgressMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_LOADCOMPLETE) { - msg = readLoadCompleteMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_TIMEOUTSTART) { - msg = readTimeOutGameStartMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_WRAPPER) { - msg = readWrapperMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_FILE) { - msg = readFileMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_FILEANNOUNCE) { - msg = readFileAnnounceMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_FILEPROGRESS) { - msg = readFileProgressMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DISCONNECTFRAME) { - msg = readDisconnectFrameMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_DISCONNECTSCREENOFF) { - msg = readDisconnectScreenOffMessage(data, offset); - } else if (commandType == NETCOMMANDTYPE_FRAMERESENDREQUEST) { - msg = readFrameResendRequestMessage(data, offset); - } - - msg->setExecutionFrame(frame); - msg->setID(commandID); - msg->setPlayerID(playerID); - msg->setNetCommandType(commandType); - - ref = NEW_NETCOMMANDREF(msg); - - ref->setRelay(relay); - - msg->detach(); - msg = NULL; - - notDone = FALSE; - } - } - - return ref; -} - -NetPacketList NetPacket::ConstructBigCommandPacketList(NetCommandRef *ref) { - // if we don't have a unique command ID, then the wrapped command cannot - // be identified. Therefore don't allow commands without a unique ID to - // be wrapped. - NetCommandMsg *msg = ref->getCommand(); - - if (!DoesCommandRequireACommandID(msg->getNetCommandType())) { - DEBUG_CRASH(("Trying to wrap a command that doesn't have a unique command ID")); - return NetPacketList(); - } - - UnsignedInt bufferSize = GetBufferSizeNeededForCommand(msg); // need to implement. I have a drinking problem. - UnsignedByte *bigPacketData = NULL; - - NetPacketList packetList; - - // create the buffer for the huge message and fill the buffer with that message. - UnsignedInt bigPacketCurrentOffset = 0; - bigPacketData = NEW UnsignedByte[bufferSize]; - FillBufferWithCommand(bigPacketData, ref); - - // create the wrapper command message we'll be using. - NetWrapperCommandMsg *wrapperMsg = newInstance(NetWrapperCommandMsg); - // get the amount of space needed for the wrapper message, not including the wrapped command data. - UnsignedInt wrapperSize = GetBufferSizeNeededForCommand(wrapperMsg); - UnsignedInt commandSizePerPacket = MAX_PACKET_SIZE - wrapperSize; - - UnsignedInt numChunks = bufferSize / commandSizePerPacket; - if ((bufferSize % commandSizePerPacket) > 0) { - ++numChunks; - } - UnsignedInt currentChunk = 0; - - // create the packets and the wrapper messages. - while (currentChunk < numChunks) { - NetPacket *packet = newInstance(NetPacket); - - UnsignedShort dataSizeThisPacket = commandSizePerPacket; - if ((bufferSize - bigPacketCurrentOffset) < dataSizeThisPacket) { - dataSizeThisPacket = bufferSize - bigPacketCurrentOffset; - } - - if (DoesCommandRequireACommandID(wrapperMsg->getNetCommandType())) { - wrapperMsg->setID(GenerateNextCommandID()); - } - wrapperMsg->setPlayerID(msg->getPlayerID()); - wrapperMsg->setExecutionFrame(msg->getExecutionFrame()); - - wrapperMsg->setChunkNumber(currentChunk); - wrapperMsg->setNumChunks(numChunks); - wrapperMsg->setDataOffset(bigPacketCurrentOffset); - wrapperMsg->setData(bigPacketData + bigPacketCurrentOffset, dataSizeThisPacket); - wrapperMsg->setTotalDataLength(bufferSize); - wrapperMsg->setWrappedCommandID(msg->getID()); - - bigPacketCurrentOffset += dataSizeThisPacket; - - NetCommandRef * ref = NEW_NETCOMMANDREF(wrapperMsg); - ref->setRelay(ref->getRelay()); - - if (packet->addCommand(ref) == FALSE) { - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::BeginBigCommandPacketList - failed to add a wrapper command to the packet")); // I still have a drinking problem. - } - - packetList.push_back(packet); - - deleteInstance(ref); - ref = NULL; - - ++currentChunk; - } - wrapperMsg->detach(); - wrapperMsg = NULL; - - delete[] bigPacketData; - bigPacketData = NULL; - - return packetList; -} - -UnsignedInt NetPacket::GetBufferSizeNeededForCommand(NetCommandMsg *msg) { - // This is where the fun begins... - - if (msg == NULL) { - return TRUE; // There was nothing to add, so it was successful. - } - - switch(msg->getNetCommandType()) - { - case NETCOMMANDTYPE_GAMECOMMAND: - return GetGameCommandSize(msg); - case NETCOMMANDTYPE_ACKSTAGE1: - case NETCOMMANDTYPE_ACKSTAGE2: - case NETCOMMANDTYPE_ACKBOTH: - return GetAckCommandSize(msg); - case NETCOMMANDTYPE_FRAMEINFO: - return GetFrameCommandSize(msg); - case NETCOMMANDTYPE_PLAYERLEAVE: - return GetPlayerLeaveCommandSize(msg); - case NETCOMMANDTYPE_RUNAHEADMETRICS: - return GetRunAheadMetricsCommandSize(msg); - case NETCOMMANDTYPE_RUNAHEAD: - return GetRunAheadCommandSize(msg); - case NETCOMMANDTYPE_DESTROYPLAYER: - return GetDestroyPlayerCommandSize(msg); - case NETCOMMANDTYPE_KEEPALIVE: - return GetKeepAliveCommandSize(msg); - case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: - return GetDisconnectKeepAliveCommandSize(msg); - case NETCOMMANDTYPE_DISCONNECTPLAYER: - return GetDisconnectPlayerCommandSize(msg); - case NETCOMMANDTYPE_PACKETROUTERQUERY: - return GetPacketRouterQueryCommandSize(msg); - case NETCOMMANDTYPE_PACKETROUTERACK: - return GetPacketRouterAckCommandSize(msg); - case NETCOMMANDTYPE_DISCONNECTCHAT: - return GetDisconnectChatCommandSize(msg); - case NETCOMMANDTYPE_DISCONNECTVOTE: - return GetDisconnectVoteCommandSize(msg); - case NETCOMMANDTYPE_CHAT: - return GetChatCommandSize(msg); - case NETCOMMANDTYPE_PROGRESS: - return GetProgressMessageSize(msg); - case NETCOMMANDTYPE_LOADCOMPLETE: - return GetLoadCompleteMessageSize(msg); - case NETCOMMANDTYPE_TIMEOUTSTART: - return GetTimeOutGameStartMessageSize(msg); - case NETCOMMANDTYPE_WRAPPER: - return GetWrapperCommandSize(msg); - case NETCOMMANDTYPE_FILE: - return GetFileCommandSize(msg); - case NETCOMMANDTYPE_FILEANNOUNCE: - return GetFileAnnounceCommandSize(msg); - case NETCOMMANDTYPE_FILEPROGRESS: - return GetFileProgressCommandSize(msg); - case NETCOMMANDTYPE_DISCONNECTFRAME: - return GetDisconnectFrameCommandSize(msg); - case NETCOMMANDTYPE_DISCONNECTSCREENOFF: - return GetDisconnectScreenOffCommandSize(msg); - case NETCOMMANDTYPE_FRAMERESENDREQUEST: - return GetFrameResendRequestCommandSize(msg); - default: - DEBUG_CRASH(("Unknown NETCOMMANDTYPE %d", msg->getNetCommandType())); - break; - } - - return 0; -} - -UnsignedInt NetPacket::GetGameCommandSize(NetCommandMsg *msg) { - NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)msg; - - UnsignedShort msglen = 0; - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); // frame number - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // relay - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // command type - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); // command ID - msglen += sizeof(UnsignedByte); // the 'D' for the data section. - - GameMessage *gmsg = cmdMsg->constructGameMessage(); - GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); - - msglen += sizeof(GameMessage::Type); - msglen += sizeof(UnsignedByte); -// Int numTypes = parser->getNumTypes(); - GameMessageParserArgumentType *arg = parser->getFirstArgumentType(); - while (arg != NULL) { - msglen += 2 * sizeof(UnsignedByte); // for the type and number of args of that type declaration. - GameMessageArgumentDataType type = arg->getType(); - if (type == ARGUMENTDATATYPE_INTEGER) { - msglen += arg->getArgCount() * sizeof(Int); - } else if (type == ARGUMENTDATATYPE_REAL) { - msglen += arg->getArgCount() * sizeof(Real); - } else if (type == ARGUMENTDATATYPE_BOOLEAN) { - msglen += arg->getArgCount() * sizeof(Bool); - } else if (type == ARGUMENTDATATYPE_OBJECTID) { - msglen += arg->getArgCount() * sizeof(ObjectID); - } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { - msglen += arg->getArgCount() * sizeof(DrawableID); - } else if (type == ARGUMENTDATATYPE_TEAMID) { - msglen += arg->getArgCount() * sizeof(UnsignedInt); - } else if (type == ARGUMENTDATATYPE_LOCATION) { - msglen += arg->getArgCount() * sizeof(Coord3D); - } else if (type == ARGUMENTDATATYPE_PIXEL) { - msglen += arg->getArgCount() * sizeof(ICoord2D); - } else if (type == ARGUMENTDATATYPE_PIXELREGION) { - msglen += arg->getArgCount() * sizeof(IRegion2D); - } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { - msglen += arg->getArgCount() * sizeof(UnsignedInt); - } else if (type == ARGUMENTDATATYPE_WIDECHAR) { - msglen += arg->getArgCount() * sizeof(WideChar); - } - arg = arg->getNext(); - } - - deleteInstance(parser); - parser = NULL; - - deleteInstance(gmsg); - gmsg = NULL; - - return msglen; -} - -UnsignedInt NetPacket::GetAckCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - ++msglen; - msglen += sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; - msglen += sizeof(UnsignedShort); - msglen += sizeof(UnsignedByte); - - return msglen; -} - -UnsignedInt NetPacket::GetFrameCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; - msglen += sizeof(UnsignedShort); - - return msglen; -} - -UnsignedInt NetPacket::GetPlayerLeaveCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; - msglen += sizeof(UnsignedByte); - - return msglen; -} - -UnsignedInt NetPacket::GetRunAheadMetricsCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(Real); - - return msglen; -} - -UnsignedInt NetPacket::GetRunAheadCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; - msglen += sizeof(UnsignedShort); - msglen += sizeof(UnsignedByte); - - return msglen; -} - -UnsignedInt NetPacket::GetDestroyPlayerCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; - msglen += sizeof(UnsignedInt); - - return msglen; -} - -UnsignedInt NetPacket::GetKeepAliveCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // For the 'D' - - return msglen; -} - -UnsignedInt NetPacket::GetDisconnectKeepAliveCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // For the 'D' - - return msglen; -} - -UnsignedInt NetPacket::GetDisconnectPlayerCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; // the 'D' - msglen += sizeof(UnsignedByte); // slot number - msglen += sizeof(UnsignedInt); // disconnect frame - - return msglen; -} - -UnsignedInt NetPacket::GetPacketRouterQueryCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // the 'D' - - return msglen; -} - -UnsignedInt NetPacket::GetPacketRouterAckCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // the 'D' - - return msglen; -} - -UnsignedInt NetPacket::GetDisconnectChatCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg); - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // the 'D' - msglen += sizeof(UnsignedByte); // string msglength - UnsignedByte textmsglen = cmdMsg->getText().getLength(); - msglen += textmsglen * sizeof(UnsignedShort); - - return msglen; -} - -UnsignedInt NetPacket::GetDisconnectVoteCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; // the 'D' - msglen += sizeof(UnsignedByte); // slot number - msglen += sizeof(UnsignedInt); // vote frame. - - return msglen; -} - -UnsignedInt NetPacket::GetChatCommandSize(NetCommandMsg *msg) { - Int msglen = 0; - NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg); - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - - ++msglen; // the 'D' - msglen += sizeof(UnsignedByte); // string msglength - UnsignedByte textmsglen = cmdMsg->getText().getLength(); - msglen += textmsglen * sizeof(UnsignedShort); - msglen += sizeof(Int); // playerMask - - return msglen; -} - -UnsignedInt NetPacket::GetProgressMessageSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // For the 'D' - ++msglen; // percentage - - return msglen; -} - -UnsignedInt NetPacket::GetLoadCompleteMessageSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // For the 'D' - - return msglen; -} - -UnsignedInt NetPacket::GetTimeOutGameStartMessageSize(NetCommandMsg *msg) { - Int msglen = 0; - - ++msglen; - msglen += sizeof(UnsignedByte); - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - ++msglen; - msglen += sizeof(UnsignedByte); - - ++msglen; // For the 'D' - - return msglen; -} - -// type, player, ID, relay, Data -UnsignedInt NetPacket::GetWrapperCommandSize(NetCommandMsg *msg) { - UnsignedInt msglen = 0; - - ++msglen; // 'T' - msglen += sizeof(UnsignedByte); // command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - ++msglen; // 'D' - - msglen += sizeof(UnsignedShort); // m_wrappedCommandID - msglen += sizeof(UnsignedInt); // m_chunkNumber - msglen += sizeof(UnsignedInt); // m_numChunks - msglen += sizeof(UnsignedInt); // m_totalDataLength - msglen += sizeof(UnsignedInt); // m_dataLength - msglen += sizeof(UnsignedInt); // m_dataOffset - - return msglen; -} - -UnsignedInt NetPacket::GetFileCommandSize(NetCommandMsg *msg) { - NetFileCommandMsg *filemsg = (NetFileCommandMsg *)msg; - UnsignedInt msglen = 0; - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - - ++msglen; // 'D' - - msglen += filemsg->getPortableFilename().getLength() + 1; // PORTABLE filename and the terminating 0 - msglen += sizeof(UnsignedInt); // file data length - msglen += filemsg->getFileLength(); // the file data - - return msglen; -} - -UnsignedInt NetPacket::GetFileAnnounceCommandSize(NetCommandMsg *msg) { - NetFileAnnounceCommandMsg *filemsg = (NetFileAnnounceCommandMsg *)msg; - UnsignedInt msglen = 0; - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - - ++msglen; // 'D' - - msglen += filemsg->getPortableFilename().getLength() + 1; // PORTABLE filename and the terminating 0 - msglen += sizeof(UnsignedShort); // m_fileID - msglen += sizeof(UnsignedByte); // m_playerMask - - return msglen; -} - -UnsignedInt NetPacket::GetFileProgressCommandSize(NetCommandMsg *msg) { - UnsignedInt msglen = 0; - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - - ++msglen; // 'D' - - msglen += sizeof(UnsignedShort); // m_fileID - msglen += sizeof(Int); // m_progress - - return msglen; -} - -UnsignedInt NetPacket::GetDisconnectFrameCommandSize(NetCommandMsg *msg) { - UnsignedInt msglen = 0; - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - - ++msglen; // 'D' - msglen += sizeof(UnsignedInt); // disconnect frame - - return msglen; -} - -UnsignedInt NetPacket::GetDisconnectScreenOffCommandSize(NetCommandMsg *msg) { - UnsignedInt msglen = 0; - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - - ++msglen; // 'D' - msglen += sizeof(UnsignedInt); // new frame - - return msglen; -} - -UnsignedInt NetPacket::GetFrameResendRequestCommandSize(NetCommandMsg *msg) { - UnsignedInt msglen = 0; - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay - - ++msglen; // 'D' - msglen += sizeof(UnsignedInt); // frame to resend - - return msglen; -} - -// this function assumes that buffer is already the correct size. -void NetPacket::FillBufferWithCommand(UnsignedByte *buffer, NetCommandRef *ref) { - NetCommandMsg *msg = ref->getCommand(); - - switch(msg->getNetCommandType()) - { - case NETCOMMANDTYPE_GAMECOMMAND: - FillBufferWithGameCommand(buffer, ref); - break; - case NETCOMMANDTYPE_ACKSTAGE1: - case NETCOMMANDTYPE_ACKSTAGE2: - case NETCOMMANDTYPE_ACKBOTH: - FillBufferWithAckCommand(buffer, ref); - break; - case NETCOMMANDTYPE_FRAMEINFO: - FillBufferWithFrameCommand(buffer, ref); - break; - case NETCOMMANDTYPE_PLAYERLEAVE: - FillBufferWithPlayerLeaveCommand(buffer, ref); - break; - case NETCOMMANDTYPE_RUNAHEADMETRICS: - FillBufferWithRunAheadMetricsCommand(buffer, ref); - break; - case NETCOMMANDTYPE_RUNAHEAD: - FillBufferWithRunAheadCommand(buffer, ref); - break; - case NETCOMMANDTYPE_DESTROYPLAYER: - FillBufferWithDestroyPlayerCommand(buffer, ref); - break; - case NETCOMMANDTYPE_KEEPALIVE: - FillBufferWithKeepAliveCommand(buffer, ref); - break; - case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: - FillBufferWithDisconnectKeepAliveCommand(buffer, ref); - break; - case NETCOMMANDTYPE_DISCONNECTPLAYER: - FillBufferWithDisconnectPlayerCommand(buffer, ref); - break; - case NETCOMMANDTYPE_PACKETROUTERQUERY: - FillBufferWithPacketRouterQueryCommand(buffer, ref); - break; - case NETCOMMANDTYPE_PACKETROUTERACK: - FillBufferWithPacketRouterAckCommand(buffer, ref); - break; - case NETCOMMANDTYPE_DISCONNECTCHAT: - FillBufferWithDisconnectChatCommand(buffer, ref); - break; - case NETCOMMANDTYPE_DISCONNECTVOTE: - FillBufferWithDisconnectVoteCommand(buffer, ref); - break; - case NETCOMMANDTYPE_CHAT: - FillBufferWithChatCommand(buffer, ref); - break; - case NETCOMMANDTYPE_PROGRESS: - FillBufferWithProgressMessage(buffer, ref); - break; - case NETCOMMANDTYPE_LOADCOMPLETE: - FillBufferWithLoadCompleteMessage(buffer, ref); - break; - case NETCOMMANDTYPE_TIMEOUTSTART: - FillBufferWithTimeOutGameStartMessage(buffer, ref); - break; - case NETCOMMANDTYPE_FILE: - FillBufferWithFileMessage(buffer, ref); - break; - case NETCOMMANDTYPE_FILEANNOUNCE: - FillBufferWithFileAnnounceMessage(buffer, ref); - break; - case NETCOMMANDTYPE_FILEPROGRESS: - FillBufferWithFileProgressMessage(buffer, ref); - break; - case NETCOMMANDTYPE_DISCONNECTFRAME: - FillBufferWithDisconnectFrameMessage(buffer, ref); - break; - case NETCOMMANDTYPE_DISCONNECTSCREENOFF: - FillBufferWithDisconnectScreenOffMessage(buffer, ref); - break; - case NETCOMMANDTYPE_FRAMERESENDREQUEST: - FillBufferWithFrameResendRequestMessage(buffer, ref); - break; - default: - DEBUG_CRASH(("Unknown NETCOMMANDTYPE %d", msg->getNetCommandType())); - break; - } -} - -void NetPacket::FillBufferWithGameCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - // get the game message from the NetCommandMsg - GameMessage *gmsg = cmdMsg->constructGameMessage(); - - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::FillBufferWithGameCommand for command ID %d", cmdMsg->getID())); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // If necessary, put the execution frame into the packet. - buffer[offset] = 'F'; - ++offset; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - - // If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - // If necessary, put the playerID into the packet. - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - - buffer[offset] = 'D'; - ++offset; - - // Now copy the GameMessage type into the packet. - GameMessage::Type newType = gmsg->getType(); - memcpy(buffer + offset, &newType, sizeof(GameMessage::Type)); - offset += sizeof(GameMessage::Type); - - - GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); - UnsignedByte numTypes = parser->getNumTypes(); - memcpy(buffer + offset, &numTypes, sizeof(numTypes)); - offset += sizeof(numTypes); - - GameMessageParserArgumentType *argType = parser->getFirstArgumentType(); - while (argType != NULL) { - UnsignedByte type = (UnsignedByte)(argType->getType()); - memcpy(buffer + offset, &type, sizeof(type)); - offset += sizeof(type); - - UnsignedByte argTypeCount = argType->getArgCount(); - memcpy(buffer + offset, &argTypeCount, sizeof(argTypeCount)); - offset += sizeof(argTypeCount); - - argType = argType->getNext(); - } - - Int numArgs = gmsg->getArgumentCount(); - for (Int i = 0; i < numArgs; ++i) { - GameMessageArgumentDataType type = gmsg->getArgumentDataType(i); - GameMessageArgumentType arg = *(gmsg->getArgument(i)); -// writeGameMessageArgumentToPacket(type, arg); - - if (type == ARGUMENTDATATYPE_INTEGER) { - memcpy(buffer + offset, &(arg.integer), sizeof(arg.integer)); - offset += sizeof(arg.integer); - } else if (type == ARGUMENTDATATYPE_REAL) { - memcpy(buffer + offset, &(arg.real), sizeof(arg.real)); - offset += sizeof(arg.real); - } else if (type == ARGUMENTDATATYPE_BOOLEAN) { - memcpy(buffer + offset, &(arg.boolean), sizeof(arg.boolean)); - offset += sizeof(arg.boolean); - } else if (type == ARGUMENTDATATYPE_OBJECTID) { - memcpy(buffer + offset, &(arg.objectID), sizeof(arg.objectID)); - offset += sizeof(arg.objectID); - } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { - memcpy(buffer + offset, &(arg.drawableID), sizeof(arg.drawableID)); - offset += sizeof(arg.drawableID); - } else if (type == ARGUMENTDATATYPE_TEAMID) { - memcpy(buffer + offset, &(arg.teamID), sizeof(arg.teamID)); - offset += sizeof(arg.teamID); - } else if (type == ARGUMENTDATATYPE_LOCATION) { - memcpy(buffer + offset, &(arg.location), sizeof(arg.location)); - offset += sizeof(arg.location); - } else if (type == ARGUMENTDATATYPE_PIXEL) { - memcpy(buffer + offset, &(arg.pixel), sizeof(arg.pixel)); - offset += sizeof(arg.pixel); - } else if (type == ARGUMENTDATATYPE_PIXELREGION) { - memcpy(buffer + offset, &(arg.pixelRegion), sizeof(arg.pixelRegion)); - offset += sizeof(arg.pixelRegion); - } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { - memcpy(buffer + offset, &(arg.timestamp), sizeof(arg.timestamp)); - offset += sizeof(arg.timestamp); - } else if (type == ARGUMENTDATATYPE_WIDECHAR) { - memcpy(buffer + offset, &(arg.wChar), sizeof(arg.wChar)); - offset += sizeof(arg.wChar); - } - } - - deleteInstance(parser); - parser = NULL; - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addGameMessage - added game message, frame %d, player %d, command ID %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); - - if (gmsg) - deleteInstance(gmsg); - gmsg = NULL; -} - -void NetPacket::FillBufferWithAckCommand(UnsignedByte *buffer, NetCommandRef *msg) { -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::FillBufferWithAckCommand - adding ack for command %d for player %d", cmdMsg->getCommandID(), msg->getCommand()->getPlayerID())); - - NetCommandMsg *cmdMsg = msg->getCommand(); - UnsignedShort offset = 0; - - UnsignedShort commandID = 0; - UnsignedByte originalPlayerID = 0; - - if (cmdMsg->getNetCommandType() == NETCOMMANDTYPE_ACKBOTH) { - NetAckBothCommandMsg *ackmsg = (NetAckBothCommandMsg *)msg; - commandID = ackmsg->getCommandID(); - originalPlayerID = ackmsg->getOriginalPlayerID(); - } else if (cmdMsg->getNetCommandType() == NETCOMMANDTYPE_ACKSTAGE1) { - NetAckStage1CommandMsg *ackmsg = (NetAckStage1CommandMsg *)msg; - commandID = ackmsg->getCommandID(); - originalPlayerID = ackmsg->getOriginalPlayerID(); - } else if (cmdMsg->getNetCommandType() == NETCOMMANDTYPE_ACKSTAGE2) { - NetAckStage2CommandMsg *ackmsg = (NetAckStage2CommandMsg *)msg; - commandID = ackmsg->getCommandID(); - originalPlayerID = ackmsg->getOriginalPlayerID(); - } - - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // Put in the command id of the command we are acking. - buffer[offset] = 'D'; - ++offset; - memcpy(buffer + offset, &commandID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - memcpy(buffer + offset, &originalPlayerID, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - // DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added ACK, original player %d, command id %d", origPlayerID, cmdID)); -} - -void NetPacket::FillBufferWithFrameCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetFrameCommandMsg *cmdMsg = (NetFrameCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - // DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addFrameCommand - adding frame command for frame %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getCommandCount(), cmdMsg->getID())); - - // TODO_NGMP: Revisit this, some of this can be stripped outside of LAN -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the execution frame into the packet. - buffer[offset] = 'F'; - ++offset; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnsignedShort cmdCount = cmdMsg->getCommandCount(); - memcpy(buffer + offset, &cmdCount, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - - // frameinfodebug -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added frame %d, player %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getPlayerID(), cmdMsg->getCommandCount(), cmdMsg->getID())); -} - -void NetPacket::FillBufferWithPlayerLeaveCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetPlayerLeaveCommandMsg *cmdMsg = (NetPlayerLeaveCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPlayerLeaveCommand - adding player leave command for player %d", cmdMsg->getLeavingPlayerID())); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// If necessary, put the execution frame into the packet. - buffer[offset] = 'F'; - ++offset; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnsignedByte leavingPlayerID = cmdMsg->getLeavingPlayerID(); - memcpy(buffer + offset, &leavingPlayerID, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); -} - -void NetPacket::FillBufferWithRunAheadMetricsCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetRunAheadMetricsCommandMsg *cmdMsg = (NetRunAheadMetricsCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadMetricsCommand - adding run ahead metrics for player %d, fps = %d, latency = %f", cmdMsg->getPlayerID(), cmdMsg->getAverageFps(), cmdMsg->getAverageLatency())); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - // write the average latency - Real averageLatency = cmdMsg->getAverageLatency(); - memcpy(buffer + offset, &averageLatency, sizeof(averageLatency)); - offset += sizeof(averageLatency); - // write the average fps - UnsignedShort averageFps = (UnsignedShort)(cmdMsg->getAverageFps()); - memcpy(buffer + offset, &averageFps, sizeof(averageFps)); - offset += sizeof(averageFps); -} - -void NetPacket::FillBufferWithRunAheadCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetRunAheadCommandMsg *cmdMsg = (NetRunAheadCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::FillBufferWithRunAheadCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - // If necessary, put the execution frame into the packet. - buffer[offset] = 'F'; - ++offset; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnsignedShort newRunAhead = cmdMsg->getRunAhead(); - memcpy(buffer + offset, &newRunAhead, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - - UnsignedByte newFrameRate = cmdMsg->getFrameRate(); - memcpy(buffer + offset, &newFrameRate, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added run ahead command, frame %d, player id %d command id %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); -} - -void NetPacket::FillBufferWithDestroyPlayerCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetDestroyPlayerCommandMsg *cmdMsg = (NetDestroyPlayerCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadCommand - adding run ahead command")); - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// If necessary, put the execution frame into the packet. - buffer[offset] = 'F'; - ++offset; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnsignedInt newVal = cmdMsg->getPlayerIndex(); - memcpy(buffer + offset, &newVal, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); -} - -void NetPacket::FillBufferWithKeepAliveCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetKeepAliveCommandMsg *cmdMsg = (NetKeepAliveCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - buffer[offset] = 'D'; - ++offset; -} - -void NetPacket::FillBufferWithDisconnectKeepAliveCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetDisconnectKeepAliveCommandMsg *cmdMsg = (NetDisconnectKeepAliveCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - - // Put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // Put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - // Put the player ID into the packet. - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - buffer[offset] = 'D'; - ++offset; -} - -void NetPacket::FillBufferWithDisconnectPlayerCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetDisconnectPlayerCommandMsg *cmdMsg = (NetDisconnectPlayerCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - - // DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnsignedByte slot = cmdMsg->getDisconnectSlot(); - memcpy(buffer + offset, &slot, sizeof(slot)); - offset += sizeof(slot); - - UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); - memcpy(buffer + offset, &disconnectFrame, sizeof(disconnectFrame)); - offset += sizeof(disconnectFrame); -} - -void NetPacket::FillBufferWithPacketRouterQueryCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetPacketRouterQueryCommandMsg *cmdMsg = (NetPacketRouterQueryCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterQueryCommand - adding packet router query command")); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - buffer[offset] = 'D'; - ++offset; -} - -void NetPacket::FillBufferWithPacketRouterAckCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetPacketRouterAckCommandMsg *cmdMsg = (NetPacketRouterAckCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterAckCommand - adding packet router query command")); - - // If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - buffer[offset] = 'D'; - ++offset; -} - -void NetPacket::FillBufferWithDisconnectChatCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - buffer[offset] = 'D'; - ++offset; - UnicodeString unitext = cmdMsg->getText(); - UnsignedByte length = unitext.getLength(); - memcpy(buffer + offset, &length, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - memcpy(buffer + offset, unitext.str(), length * sizeof(UnsignedShort)); - offset += length * sizeof(UnsignedShort); -} - -void NetPacket::FillBufferWithDisconnectVoteCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetDisconnectVoteCommandMsg *cmdMsg = (NetDisconnectVoteCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - adding run ahead command")); - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnsignedByte slot = cmdMsg->getSlot(); - memcpy(buffer + offset, &slot, sizeof(slot)); - offset += sizeof(slot); - - UnsignedInt voteFrame = cmdMsg->getVoteFrame(); - memcpy(buffer + offset, &voteFrame, sizeof(voteFrame)); - offset += sizeof(voteFrame); -} - -void NetPacket::FillBufferWithChatCommand(UnsignedByte *buffer, NetCommandRef *msg) { - NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the execution frame into the packet. - buffer[offset] = 'F'; - ++offset; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); - offset += sizeof(UnsignedInt); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - buffer[offset] = 'D'; - ++offset; - UnicodeString unitext = cmdMsg->getText(); - UnsignedByte length = unitext.getLength(); - Int playerMask = cmdMsg->getPlayerMask(); - memcpy(buffer + offset, &length, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - memcpy(buffer + offset, unitext.str(), length * sizeof(UnsignedShort)); - offset += length * sizeof(UnsignedShort); - - memcpy(buffer + offset, &playerMask, sizeof(Int)); - offset += sizeof(Int); -} - -void NetPacket::FillBufferWithProgressMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetProgressCommandMsg *cmdMsg = (NetProgressCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - -// Put the player ID into the packet. - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - buffer[offset] = 'D'; - ++offset; - - buffer[offset] = cmdMsg->getPercentage(); - ++offset; -} - -void NetPacket::FillBufferWithLoadCompleteMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - - buffer[offset] = 'D'; - ++offset; -} - -void NetPacket::FillBufferWithTimeOutGameStartMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); - UnsignedShort offset = 0; - -// If necessary, put the NetCommandType into the packet. - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - -// If necessary, put the relay into the packet. - buffer[offset] = 'R'; - ++offset; - UnsignedByte newRelay = msg->getRelay(); - memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); - offset += sizeof(UnsignedByte); - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - -// If necessary, specify the command ID of this command. - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); - offset += sizeof(UnsignedShort); - - buffer[offset] = 'D'; - ++offset; -} - -void NetPacket::FillBufferWithFileMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetFileCommandMsg *cmdMsg = (NetFileCommandMsg *)(msg->getCommand()); - UnsignedInt offset = 0; - - // command type - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // relay - buffer[offset] = 'R'; - ++offset; - buffer[offset] = msg->getRelay(); - offset += sizeof(UnsignedByte); - - // player ID - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // command ID - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(newID)); - offset += sizeof(newID); - - // data - buffer[offset] = 'D'; - ++offset; - - AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE - for (Int i = 0; i < filename.getLength(); ++i) { - buffer[offset] = filename.getCharAt(i); - ++offset; - } - buffer[offset] = 0; - ++offset; - - UnsignedInt newInt = cmdMsg->getFileLength(); - memcpy(buffer + offset, &newInt, sizeof(newInt)); - offset += sizeof(newInt); - - memcpy(buffer + offset, cmdMsg->getFileData(), cmdMsg->getFileLength()); - offset += cmdMsg->getFileLength(); -} - -void NetPacket::FillBufferWithFileAnnounceMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetFileAnnounceCommandMsg *cmdMsg = (NetFileAnnounceCommandMsg *)(msg->getCommand()); - UnsignedInt offset = 0; - - // command type - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // relay - buffer[offset] = 'R'; - ++offset; - buffer[offset] = msg->getRelay(); - offset += sizeof(UnsignedByte); - - // player ID - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // command ID - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(newID)); - offset += sizeof(newID); - - // data - buffer[offset] = 'D'; - ++offset; - - AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE - for (Int i = 0; i < filename.getLength(); ++i) { - buffer[offset] = filename.getCharAt(i); - ++offset; - } - buffer[offset] = 0; - ++offset; - - UnsignedShort fileID = cmdMsg->getFileID(); - memcpy(buffer + offset, &fileID, sizeof(fileID)); - offset += sizeof(fileID); - - UnsignedByte playerMask = cmdMsg->getPlayerMask(); - memcpy(buffer + offset, &playerMask, sizeof(playerMask)); - offset += sizeof(playerMask); -} - -void NetPacket::FillBufferWithFileProgressMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetFileProgressCommandMsg *cmdMsg = (NetFileProgressCommandMsg *)(msg->getCommand()); - UnsignedInt offset = 0; - - // command type - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // relay - buffer[offset] = 'R'; - ++offset; - buffer[offset] = msg->getRelay(); - offset += sizeof(UnsignedByte); - - // player ID - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // command ID - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(newID)); - offset += sizeof(newID); - - // data - buffer[offset] = 'D'; - ++offset; - - UnsignedShort fileID = cmdMsg->getFileID(); - memcpy(buffer + offset, &fileID, sizeof(fileID)); - offset += sizeof(fileID); - - Int progress = cmdMsg->getProgress(); - memcpy(buffer + offset, &progress, sizeof(progress)); - offset += sizeof(progress); -} - -void NetPacket::FillBufferWithDisconnectFrameMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetDisconnectFrameCommandMsg *cmdMsg = (NetDisconnectFrameCommandMsg *)(msg->getCommand()); - UnsignedInt offset = 0; - - // command type - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // relay - buffer[offset] = 'R'; - ++offset; - buffer[offset] = msg->getRelay(); - offset += sizeof(UnsignedByte); - - // player ID - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // command ID - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(newID)); - offset += sizeof(newID); - - // data - buffer[offset] = 'D'; - ++offset; - - UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); - memcpy(buffer + offset, &disconnectFrame, sizeof(disconnectFrame)); - offset += sizeof(disconnectFrame); -} - -void NetPacket::FillBufferWithDisconnectScreenOffMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetDisconnectScreenOffCommandMsg *cmdMsg = (NetDisconnectScreenOffCommandMsg *)(msg->getCommand()); - UnsignedInt offset = 0; - - // command type - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // relay - buffer[offset] = 'R'; - ++offset; - buffer[offset] = msg->getRelay(); - offset += sizeof(UnsignedByte); - - // player ID - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // command ID - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(newID)); - offset += sizeof(newID); - - // data - buffer[offset] = 'D'; - ++offset; - - UnsignedInt newFrame = cmdMsg->getNewFrame(); - memcpy(buffer + offset, &newFrame, sizeof(newFrame)); - offset += sizeof(newFrame); -} - -void NetPacket::FillBufferWithFrameResendRequestMessage(UnsignedByte *buffer, NetCommandRef *msg) { - NetFrameResendRequestCommandMsg *cmdMsg = (NetFrameResendRequestCommandMsg *)(msg->getCommand()); - UnsignedInt offset = 0; - - // command type - buffer[offset] = 'T'; - ++offset; - buffer[offset] = cmdMsg->getNetCommandType(); - offset += sizeof(UnsignedByte); - - // relay - buffer[offset] = 'R'; - ++offset; - buffer[offset] = msg->getRelay(); - offset += sizeof(UnsignedByte); - - // player ID - buffer[offset] = 'P'; - ++offset; - buffer[offset] = cmdMsg->getPlayerID(); - offset += sizeof(UnsignedByte); - - // command ID - buffer[offset] = 'C'; - ++offset; - UnsignedShort newID = cmdMsg->getID(); - memcpy(buffer + offset, &newID, sizeof(newID)); - offset += sizeof(newID); - - // data - buffer[offset] = 'D'; - ++offset; - - UnsignedInt frameToResend = cmdMsg->getFrameToResend(); - memcpy(buffer + offset, &frameToResend, sizeof(frameToResend)); - offset += sizeof(frameToResend); -} - - -/** - * Constructor - */ -NetPacket::NetPacket() { - init(); -} - -/** - * Constructor given raw transport data. - */ -NetPacket::NetPacket(TransportMessage *msg) { - init(); - m_packetLen = msg->length; - memcpy(m_packet, msg->data, MAX_PACKET_SIZE); - m_numCommands = -1; - m_addr = msg->addr; - m_port = msg->port; -} - -/** - * Destructor - */ -NetPacket::~NetPacket() { - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } -} - -/** - * Initialize all the member variable values. - */ -void NetPacket::init() { - m_addr = 0; - m_port = 0; - m_numCommands = 0; - m_packetLen = 0; - m_packet[0] = 0; - - m_lastPlayerID = 0; - m_lastFrame = 0; - m_lastCommandID = 0; - m_lastCommandType = 0; - m_lastRelay = 0; - - m_lastCommand = NULL; -} - -void NetPacket::reset() { - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - init(); -} - -/** - * Set the address to which this packet is to be sent. - */ -void NetPacket::setAddress(Int addr, Int port) { - m_addr = addr; - m_port = port; -} - -/** - * Adds this command to the packet. Returns false if there wasn't enough room - * in the packet for this message, true otherwise. - */ -Bool NetPacket::addCommand(NetCommandRef *msg) { - // This is where the fun begins... - - NetCommandMsg *cmdMsg = msg->getCommand(); - - if (msg == NULL) { - return TRUE; // There was nothing to add, so it was successful. - } - - switch(cmdMsg->getNetCommandType()) - { - case NETCOMMANDTYPE_GAMECOMMAND: - return addGameCommand(msg); - case NETCOMMANDTYPE_ACKSTAGE1: - return addAckStage1Command(msg); - case NETCOMMANDTYPE_ACKSTAGE2: - return addAckStage2Command(msg); - case NETCOMMANDTYPE_ACKBOTH: - return addAckBothCommand(msg); - case NETCOMMANDTYPE_FRAMEINFO: - return addFrameCommand(msg); - case NETCOMMANDTYPE_PLAYERLEAVE: - return addPlayerLeaveCommand(msg); - case NETCOMMANDTYPE_RUNAHEADMETRICS: - return addRunAheadMetricsCommand(msg); - case NETCOMMANDTYPE_RUNAHEAD: - return addRunAheadCommand(msg); - case NETCOMMANDTYPE_DESTROYPLAYER: - return addDestroyPlayerCommand(msg); - case NETCOMMANDTYPE_KEEPALIVE: - return addKeepAliveCommand(msg); - case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: - return addDisconnectKeepAliveCommand(msg); - case NETCOMMANDTYPE_DISCONNECTPLAYER: - return addDisconnectPlayerCommand(msg); - case NETCOMMANDTYPE_PACKETROUTERQUERY: - return addPacketRouterQueryCommand(msg); - case NETCOMMANDTYPE_PACKETROUTERACK: - return addPacketRouterAckCommand(msg); - case NETCOMMANDTYPE_DISCONNECTCHAT: - return addDisconnectChatCommand(msg); - case NETCOMMANDTYPE_DISCONNECTVOTE: - return addDisconnectVoteCommand(msg); - case NETCOMMANDTYPE_CHAT: - return addChatCommand(msg); - case NETCOMMANDTYPE_PROGRESS: - return addProgressMessage(msg); - case NETCOMMANDTYPE_LOADCOMPLETE: - return addLoadCompleteMessage(msg); - case NETCOMMANDTYPE_TIMEOUTSTART: - return addTimeOutGameStartMessage(msg); - case NETCOMMANDTYPE_WRAPPER: - return addWrapperCommand(msg); - case NETCOMMANDTYPE_FILE: - return addFileCommand(msg); - case NETCOMMANDTYPE_FILEANNOUNCE: - return addFileAnnounceCommand(msg); - case NETCOMMANDTYPE_FILEPROGRESS: - return addFileProgressCommand(msg); - case NETCOMMANDTYPE_DISCONNECTFRAME: - return addDisconnectFrameCommand(msg); - case NETCOMMANDTYPE_DISCONNECTSCREENOFF: - return addDisconnectScreenOffCommand(msg); - case NETCOMMANDTYPE_FRAMERESENDREQUEST: - return addFrameResendRequestCommand(msg); - default: - DEBUG_CRASH(("Unknown NETCOMMANDTYPE %d", cmdMsg->getNetCommandType())); - break; - } - - return TRUE; -} - -/* -T = Net command type -F = Execution frame -P = Player ID -C = Command ID -R = Relay -D = Command Data -Z = Repeat last command -*/ -Bool NetPacket::addFrameResendRequestCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForFrameResendRequestMessage(msg)) { - NetFrameResendRequestCommandMsg *cmdMsg = (NetFrameResendRequestCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - UnsignedInt frameToResend = cmdMsg->getFrameToResend(); - memcpy(m_packet + m_packetLen, &frameToResend, sizeof(frameToResend)); - m_packetLen += sizeof(frameToResend); - - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addFrameResendRequest - added frame resend request command from player %d for frame %d, command id = %d", m_lastPlayerID, frameToResend, m_lastCommandID)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForFrameResendRequestMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetFrameResendRequestCommandMsg *cmdMsg = (NetFrameResendRequestCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedInt); // for the frame to be resent - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addDisconnectScreenOffCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForDisconnectScreenOffMessage(msg)) { - NetDisconnectScreenOffCommandMsg *cmdMsg = (NetDisconnectScreenOffCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - UnsignedInt newFrame = cmdMsg->getNewFrame(); - memcpy(m_packet + m_packetLen, &newFrame, sizeof(newFrame)); - m_packetLen += sizeof(newFrame); - - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectScreenOff - added disconnect screen off command from player %d for frame %d, command id = %d", m_lastPlayerID, newFrame, m_lastCommandID)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForDisconnectScreenOffMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetDisconnectScreenOffCommandMsg *cmdMsg = (NetDisconnectScreenOffCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedInt); // for the disconnect frame - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addDisconnectFrameCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForDisconnectFrameMessage(msg)) { - NetDisconnectFrameCommandMsg *cmdMsg = (NetDisconnectFrameCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); - memcpy(m_packet + m_packetLen, &disconnectFrame, sizeof(disconnectFrame)); - m_packetLen += sizeof(disconnectFrame); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectFrame - added disconnect frame command from player %d for frame %d, command id = %d", m_lastPlayerID, disconnectFrame, m_lastCommandID)); - - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForDisconnectFrameMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetDisconnectFrameCommandMsg *cmdMsg = (NetDisconnectFrameCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedInt); // for the disconnect frame - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addFileCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForFileMessage(msg)) { - NetFileCommandMsg *cmdMsg = (NetFileCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE - strcpy((char *)(m_packet + m_packetLen), filename.str()); - m_packetLen += filename.getLength() + 1; - - UnsignedInt fileLength = cmdMsg->getFileLength(); - memcpy(m_packet + m_packetLen, &fileLength, sizeof(fileLength)); - m_packetLen += sizeof(fileLength); - - memcpy(m_packet + m_packetLen, cmdMsg->getFileData(), fileLength); - m_packetLen += fileLength; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForFileMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetFileCommandMsg *cmdMsg = (NetFileCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedByte) + sizeof(UnsignedShort); - } - - ++len; // 'D' - len += cmdMsg->getPortableFilename().getLength() + 1; // PORTABLE filename + the terminating 0 - len += sizeof(UnsignedInt); // filedata length - len += cmdMsg->getFileLength(); - - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - - return TRUE; -} - -Bool NetPacket::addFileAnnounceCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForFileAnnounceMessage(msg)) { - NetFileAnnounceCommandMsg *cmdMsg = (NetFileAnnounceCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE - strcpy((char *)(m_packet + m_packetLen), filename.str()); - m_packetLen += filename.getLength() + 1; - - UnsignedShort fileID = cmdMsg->getFileID(); - memcpy(m_packet + m_packetLen, &fileID, sizeof(fileID)); - m_packetLen += sizeof(fileID); - - UnsignedByte playerMask = cmdMsg->getPlayerMask(); - memcpy(m_packet + m_packetLen, &playerMask, sizeof(playerMask)); - m_packetLen += sizeof(playerMask); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Adding file announce message for fileID %d, ID %d to packet", - cmdMsg->getFileID(), cmdMsg->getID())); - return TRUE; - } - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("No room to add file announce message to packet")); - return FALSE; -} - -Bool NetPacket::isRoomForFileAnnounceMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetFileAnnounceCommandMsg *cmdMsg = (NetFileAnnounceCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedByte) + sizeof(UnsignedShort); - } - - ++len; // 'D' - len += cmdMsg->getPortableFilename().getLength() + 1; // PORTABLE filename + the terminating 0 - len += sizeof(UnsignedShort); // m_fileID - len += sizeof(UnsignedByte); // m_playerMask - - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - - return TRUE; -} - -Bool NetPacket::addFileProgressCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForFileProgressMessage(msg)) { - NetFileProgressCommandMsg *cmdMsg = (NetFileProgressCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - UnsignedShort fileID = cmdMsg->getFileID(); - memcpy(m_packet + m_packetLen, &fileID, sizeof(fileID)); - m_packetLen += sizeof(fileID); - - Int progress = cmdMsg->getProgress(); - memcpy(m_packet + m_packetLen, &progress, sizeof(progress)); - m_packetLen += sizeof(progress); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForFileProgressMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetFileProgressCommandMsg *cmdMsg = (NetFileProgressCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedByte) + sizeof(UnsignedShort); - } - - ++len; // 'D' - len += sizeof(UnsignedShort); // m_fileID - len += sizeof(Int); // m_progress - - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - - return TRUE; -} - -Bool NetPacket::addWrapperCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForWrapperMessage(msg)) { - NetWrapperCommandMsg *cmdMsg = (NetWrapperCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary put the player ID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - // wrapped command ID - UnsignedShort wrappedCommandID = cmdMsg->getWrappedCommandID(); - memcpy(m_packet + m_packetLen, &wrappedCommandID, sizeof(wrappedCommandID)); - m_packetLen += sizeof(wrappedCommandID); - - // chunk number -// m_packet[m_packetLen] = cmdMsg->getChunkNumber(); -// ++m_packetLen; - UnsignedInt chunkNumber = cmdMsg->getChunkNumber(); - memcpy(m_packet + m_packetLen, &chunkNumber, sizeof(chunkNumber)); - m_packetLen += sizeof(chunkNumber); - - // number of chunks -// m_packet[m_packetLen] = cmdMsg->getNumChunks(); -// ++m_packetLen; - UnsignedInt numChunks = cmdMsg->getNumChunks(); - memcpy(m_packet + m_packetLen, &numChunks, sizeof(numChunks)); - m_packetLen += sizeof(numChunks); - - // total length of data for all chunks - UnsignedInt totalDataLength = cmdMsg->getTotalDataLength(); - memcpy(m_packet + m_packetLen, &totalDataLength, sizeof(totalDataLength)); - m_packetLen += sizeof(totalDataLength); - - // data length for this chunk - UnsignedInt dataLength = cmdMsg->getDataLength(); - memcpy(m_packet + m_packetLen, &dataLength, sizeof(dataLength)); - m_packetLen += sizeof(dataLength); - - // the offset into the data of this chunk - UnsignedInt dataOffset = cmdMsg->getDataOffset(); - memcpy(m_packet + m_packetLen, &dataOffset, sizeof(dataOffset)); - m_packetLen += sizeof(dataOffset); - - // the data for this chunk - UnsignedByte *data = cmdMsg->getData(); - memcpy(m_packet + m_packetLen, data, dataLength); - m_packetLen += dataLength; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForWrapperMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetWrapperCommandMsg *cmdMsg = (NetWrapperCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedByte) + sizeof(UnsignedShort); - } - - ++len; // 'D' - len += sizeof(UnsignedShort); // wrapped command ID - len += sizeof(UnsignedInt); // chunk number - len += sizeof(UnsignedInt); // number of chunks - len += sizeof(UnsignedInt); // total data length - len += sizeof(UnsignedInt); // data length of this chunk - len += sizeof(UnsignedInt); // offset of this chunk - len += cmdMsg->getDataLength(); // for the data of this chunk - - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - - return TRUE; -} - -/** - * Add a TimeOutGameStart to the packet. Returns true if successful. - */ -Bool NetPacket::addTimeOutGameStartMessage(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForLoadCompleteMessage(msg)) { - NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); - - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room in the packet for this command. - */ -Bool NetPacket::isRoomForTimeOutGameStartMessage(NetCommandRef *msg) { - Int len = 0; - NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // For the 'D' - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - - - -/** - * Add a Progress command to the packet. Returns true if successful. - */ -Bool NetPacket::addLoadCompleteMessage(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForLoadCompleteMessage(msg)) { - NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); - - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room in the packet for this command. - */ -Bool NetPacket::isRoomForLoadCompleteMessage(NetCommandRef *msg) { - Int len = 0; - NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // For the 'D' - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - - - - -/** - * Add a Progress command to the packet. Returns true if successful. - */ -Bool NetPacket::addProgressMessage(NetCommandRef *msg) { - if (isRoomForProgressMessage(msg)) { - NetProgressCommandMsg *cmdMsg = (NetProgressCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - m_packet[m_packetLen] = cmdMsg->getPercentage(); - ++m_packetLen; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); - - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room in the packet for this command. - */ -Bool NetPacket::isRoomForProgressMessage(NetCommandRef *msg) { - Int len = 0; - NetProgressCommandMsg *cmdMsg = (NetProgressCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // For the 'D' - ++len; // percentage - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - - - -Bool NetPacket::addDisconnectVoteCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - entering...")); - // need type, player id, relay, command id, slot number - if (isRoomForDisconnectVoteMessage(msg)) { - NetDisconnectVoteCommandMsg *cmdMsg = (NetDisconnectVoteCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnsignedByte slot = cmdMsg->getSlot(); - memcpy(m_packet + m_packetLen, &slot, sizeof(slot)); - m_packetLen += sizeof(slot); - - UnsignedInt voteFrame = cmdMsg->getVoteFrame(); - memcpy(m_packet + m_packetLen, &voteFrame, sizeof(voteFrame)); - m_packetLen += sizeof(voteFrame); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - added disconnect vote command, player id %d command id %d, voted slot %d", m_lastPlayerID, m_lastCommandID, slot)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room for this player disconnect command in this packet. - */ -Bool NetPacket::isRoomForDisconnectVoteMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetDisconnectVoteCommandMsg *cmdMsg = (NetDisconnectVoteCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // the 'D' - len += sizeof(UnsignedByte); // slot number - len += sizeof(UnsignedInt); // vote frame - - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addDisconnectChatCommand(NetCommandRef *msg) { - // type, player, id, relay, data - // data format: 1 byte string length, string (two bytes per character) -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - Entering...")); - if (isRoomForDisconnectChatMessage(msg)) { - NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnicodeString unitext = cmdMsg->getText(); - UnsignedByte length = unitext.getLength(); - memcpy(m_packet + m_packetLen, &length, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - memcpy(m_packet + m_packetLen, unitext.str(), length * sizeof(UnsignedShort)); - m_packetLen += length * sizeof(UnsignedShort); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added disconnect chat command")); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForDisconnectChatMessage(NetCommandRef *msg) { - Int len = 0; - NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // the 'D' - len += sizeof(UnsignedByte); // string length - UnsignedByte textLen = cmdMsg->getText().getLength(); - len += textLen * sizeof(UnsignedShort); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addChatCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForChatMessage(msg)) { - NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnicodeString unitext = cmdMsg->getText(); - UnsignedByte length = unitext.getLength(); - Int playerMask = cmdMsg->getPlayerMask(); - memcpy(m_packet + m_packetLen, &length, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - memcpy(m_packet + m_packetLen, unitext.str(), length * sizeof(UnsignedShort)); - m_packetLen += length * sizeof(UnsignedShort); - - memcpy(m_packet + m_packetLen, &playerMask, sizeof(Int)); - m_packetLen += sizeof(Int); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added chat command")); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -Bool NetPacket::isRoomForChatMessage(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - Int len = 0; - NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // the 'D' - len += sizeof(UnsignedByte); // string length - UnsignedByte textLen = cmdMsg->getText().getLength(); - len += textLen * sizeof(UnsignedShort); - len += sizeof(Int); // playerMask - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addPacketRouterAckCommand(NetCommandRef *msg) { - // need type, player id, relay, command id, slot number - if (isRoomForPacketRouterAckMessage(msg)) { - NetPacketRouterAckCommandMsg *cmdMsg = (NetPacketRouterAckCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterAckCommand - adding packet router query command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added packet router ack command, player id %d", m_lastPlayerID)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room for this packet router ack command in this packet. - */ -Bool NetPacket::isRoomForPacketRouterAckMessage(NetCommandRef *msg) { - Int len = 0; - NetPacketRouterAckCommandMsg *cmdMsg = (NetPacketRouterAckCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // the 'D' - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addPacketRouterQueryCommand(NetCommandRef *msg) { - // need type, player id, relay, command id, slot number - if (isRoomForPacketRouterQueryMessage(msg)) { - NetPacketRouterQueryCommandMsg *cmdMsg = (NetPacketRouterQueryCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterQueryCommand - adding packet router query command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added packet router query command, player id %d", m_lastPlayerID)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room for this packet router query command in this packet. - */ -Bool NetPacket::isRoomForPacketRouterQueryMessage(NetCommandRef *msg) { - Int len = 0; - NetPacketRouterQueryCommandMsg *cmdMsg = (NetPacketRouterQueryCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // the 'D' - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::addDisconnectPlayerCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - entering...")); - // need type, player id, relay, command id, slot number - if (isRoomForDisconnectPlayerMessage(msg)) { - NetDisconnectPlayerCommandMsg *cmdMsg = (NetDisconnectPlayerCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnsignedByte slot = cmdMsg->getDisconnectSlot(); - memcpy(m_packet + m_packetLen, &slot, sizeof(slot)); - m_packetLen += sizeof(slot); - - UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); - memcpy(m_packet + m_packetLen, &disconnectFrame, sizeof(disconnectFrame)); - m_packetLen += sizeof(disconnectFrame); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - added disconnect player command, player id %d command id %d, disconnecting slot %d", m_lastPlayerID, m_lastCommandID, slot)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room for this player disconnect command in this packet. - */ -Bool NetPacket::isRoomForDisconnectPlayerMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetDisconnectPlayerCommandMsg *cmdMsg = (NetDisconnectPlayerCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // the 'D' - len += sizeof(UnsignedByte); // slot number - len += sizeof(UnsignedInt); // disconnectFrame - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - - -/** - * Add a keep alive command to the packet. Returns true if successful. - */ -Bool NetPacket::addDisconnectKeepAliveCommand(NetCommandRef *msg) { - if (isRoomForDisconnectKeepAliveMessage(msg)) { - NetDisconnectKeepAliveCommandMsg *cmdMsg = (NetDisconnectKeepAliveCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); - - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room in the packet for this command. - */ -Bool NetPacket::isRoomForDisconnectKeepAliveMessage(NetCommandRef *msg) { - Int len = 0; - NetDisconnectKeepAliveCommandMsg *cmdMsg = (NetDisconnectKeepAliveCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // For the 'D' - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -/** - * Add a keep alive command to the packet. Returns true if successful. - */ -Bool NetPacket::addKeepAliveCommand(NetCommandRef *msg) { - if (isRoomForKeepAliveMessage(msg)) { - NetKeepAliveCommandMsg *cmdMsg = (NetKeepAliveCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); - - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room in the packet for this command. - */ -Bool NetPacket::isRoomForKeepAliveMessage(NetCommandRef *msg) { - Int len = 0; - NetKeepAliveCommandMsg *cmdMsg = (NetKeepAliveCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // For the 'D' - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -/** - * Add a run ahead command to the packet. Returns true if successful. - */ -Bool NetPacket::addRunAheadCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForRunAheadMessage(msg)) { - NetRunAheadCommandMsg *cmdMsg = (NetRunAheadCommandMsg *)(msg->getCommand()); - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadCommand - adding run ahead command")); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnsignedShort newRunAhead = cmdMsg->getRunAhead(); - memcpy(m_packet + m_packetLen, &newRunAhead, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - - UnsignedByte newFrameRate = cmdMsg->getFrameRate(); - memcpy(m_packet + m_packetLen, &newFrameRate, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added run ahead command, frame %d, player id %d command id %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room for this run ahead command in this packet. - */ -Bool NetPacket::isRoomForRunAheadMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetRunAheadCommandMsg *cmdMsg = (NetRunAheadCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedShort); - len += sizeof(UnsignedByte); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -/** - * Add a DestroyPlayer command to the packet. Returns true if successful. - */ -Bool NetPacket::addDestroyPlayerCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForDestroyPlayerMessage(msg)) { - NetDestroyPlayerCommandMsg *cmdMsg = (NetDestroyPlayerCommandMsg *)(msg->getCommand()); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnsignedInt newVal = cmdMsg->getPlayerIndex(); - memcpy(m_packet + m_packetLen, &newVal, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added CRC:0x%8.8X info command, frame %d, player id %d command id %d", newCRC, m_lastFrame, m_lastPlayerID, m_lastCommandID)); - - ++m_numCommands; - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is room for this DestroyPlayer command in this packet. - */ -Bool NetPacket::isRoomForDestroyPlayerMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetDestroyPlayerCommandMsg *cmdMsg = (NetDestroyPlayerCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedInt); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -/** - * Add a run ahead metrics command to the packet. Returns true if successful. - */ -Bool NetPacket::addRunAheadMetricsCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForRunAheadMetricsMessage(msg)) { - NetRunAheadMetricsCommandMsg *cmdMsg = (NetRunAheadMetricsCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadMetricsCommand - adding run ahead metrics for player %d, fps = %d, latency = %f", cmdMsg->getPlayerID(), cmdMsg->getAverageFps(), cmdMsg->getAverageLatency())); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - // write the average latency - Real averageLatency = cmdMsg->getAverageLatency(); - memcpy(m_packet + m_packetLen, &averageLatency, sizeof(averageLatency)); - m_packetLen += sizeof(averageLatency); - // write the average fps - UnsignedShort averageFps = (UnsignedShort)(cmdMsg->getAverageFps()); - memcpy(m_packet + m_packetLen, &averageFps, sizeof(averageFps)); - m_packetLen += sizeof(averageFps); - - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - ++m_numCommands; - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is enough room in the packet to fit this message. - */ -Bool NetPacket::isRoomForRunAheadMetricsMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetRunAheadMetricsCommandMsg *cmdMsg = (NetRunAheadMetricsCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // 'D' - len += sizeof(UnsignedShort); - len += sizeof(Real); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - - -/** - * Add a player leave command to the packet. Returns true if successful. - */ -Bool NetPacket::addPlayerLeaveCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isRoomForPlayerLeaveMessage(msg)) { - NetPlayerLeaveCommandMsg *cmdMsg = (NetPlayerLeaveCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPlayerLeaveCommand - adding player leave command for player %d", cmdMsg->getLeavingPlayerID())); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnsignedByte leavingPlayerID = cmdMsg->getLeavingPlayerID(); - memcpy(m_packet + m_packetLen, &leavingPlayerID, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - ++m_numCommands; - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is enough room in the packet to fit this message. - */ -Bool NetPacket::isRoomForPlayerLeaveMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetPlayerLeaveCommandMsg *cmdMsg = (NetPlayerLeaveCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedByte); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -/** - * Add this frame command message. Returns true if successful. - */ -Bool NetPacket::addFrameCommand(NetCommandRef *msg) { - Bool needNewCommandID = FALSE; - if (isFrameRepeat(msg)) { - if (m_packetLen >= MAX_PACKET_SIZE) { - return FALSE; - } - m_packet[m_packetLen] = 'Z'; - ++m_packetLen; - m_lastCommandID = msg->getCommand()->getID(); - deleteInstance(m_lastCommand); - m_lastCommand = newInstance(NetCommandRef)(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - ++m_lastFrame; // need this cause we're actually advancing to the next frame by adding this command. - ++m_numCommands; - // frameinfodebug -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added frame %d, player %d, command count = %d, command id = %d, repeat", m_lastFrame, m_lastPlayerID, 0, m_lastCommandID)); - return TRUE; - } - if (isRoomForFrameMessage(msg)) { - NetFrameCommandMsg *cmdMsg = (NetFrameCommandMsg *)(msg->getCommand()); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addFrameCommand - adding frame command for frame %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getCommandCount(), cmdMsg->getID())); - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - needNewCommandID = TRUE; - } - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - UnsignedShort cmdCount = cmdMsg->getCommandCount(); - memcpy(m_packet + m_packetLen, &cmdCount, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - - // frameinfodebug -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added frame %d, player %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getPlayerID(), cmdMsg->getCommandCount(), cmdMsg->getID())); - - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - ++m_numCommands; - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is enough room in this packet for this frame message. - */ -Bool NetPacket::isRoomForFrameMessage(NetCommandRef *msg) { - Int len = 0; - Bool needNewCommandID = FALSE; - NetFrameCommandMsg *cmdMsg = (NetFrameCommandMsg *)(msg->getCommand()); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - len += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastRelay != msg->getRelay()) { - len += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - len += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedShort); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::isFrameRepeat(NetCommandRef *msg) { - if (m_lastCommand == NULL) { - return FALSE; - } - if (m_lastCommand->getCommand()->getNetCommandType() != NETCOMMANDTYPE_FRAMEINFO) { - return FALSE; - } - NetFrameCommandMsg *framemsg = (NetFrameCommandMsg *)(msg->getCommand()); - NetFrameCommandMsg *lastmsg = (NetFrameCommandMsg *)(m_lastCommand->getCommand()); - if (framemsg->getCommandCount() != 0) { - return FALSE; - } - if (framemsg->getExecutionFrame() != (lastmsg->getExecutionFrame() + 1)) { - return FALSE; - } - if (msg->getRelay() != m_lastCommand->getRelay()) { - return FALSE; - } - if (framemsg->getID() != (lastmsg->getID() + 1)) { - return FALSE; - } - return TRUE; -} - -/** - * Add an ack "both" command. - */ -Bool NetPacket::addAckBothCommand(NetCommandRef *msg) { - NetAckBothCommandMsg *ackmsg = (NetAckBothCommandMsg *)(msg->getCommand()); - return addAckCommand(msg, ackmsg->getCommandID(), ackmsg->getOriginalPlayerID()); -} - -/** - * Add an ack stage 1 command. - */ -Bool NetPacket::addAckStage1Command(NetCommandRef *msg) { - NetAckStage1CommandMsg *ackmsg = (NetAckStage1CommandMsg *)(msg->getCommand()); - return addAckCommand(msg, ackmsg->getCommandID(), ackmsg->getOriginalPlayerID()); -} - -/** - * Add an ack stage 2 command. - */ -Bool NetPacket::addAckStage2Command(NetCommandRef *msg) { - NetAckStage2CommandMsg *ackmsg = (NetAckStage2CommandMsg *)(msg->getCommand()); - return addAckCommand(msg, ackmsg->getCommandID(), ackmsg->getOriginalPlayerID()); -} - -/** - * Add this ack command to the packet. Returns true if successful. - */ -Bool NetPacket::addAckCommand(NetCommandRef *msg, UnsignedShort commandID, UnsignedByte originalPlayerID) { - if (isAckRepeat(msg)) { - if (m_packetLen >= MAX_PACKET_SIZE) { - return FALSE; - } - m_packet[m_packetLen] = 'Z'; - ++m_packetLen; - ++m_numCommands; - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - return TRUE; - } - if (isRoomForAckMessage(msg)) { - NetCommandMsg *cmdMsg = msg->getCommand(); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addAckCommand - adding ack for command %d for player %d", cmdMsg->getCommandID(), msg->getCommand()->getPlayerID())); - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - - // Put in the command id of the command we are acking. - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - memcpy(m_packet + m_packetLen, &commandID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - memcpy(m_packet + m_packetLen, &originalPlayerID, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added ACK, original player %d, command id %d", origPlayerID, cmdID)); - ++m_numCommands; - return TRUE; - } - return FALSE; -} - -/** - * Returns true if there is enough room in the packet for this ack message. - */ -Bool NetPacket::isRoomForAckMessage(NetCommandRef *msg) { - Int len = 0; - NetCommandMsg *cmdMsg = msg->getCommand(); - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - ++len; - len += sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - ++len; - len += sizeof(UnsignedByte); - } - - ++len; // for 'D' - len += sizeof(UnsignedShort); - len += sizeof(UnsignedByte); - if ((len + m_packetLen) > MAX_PACKET_SIZE) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::isAckRepeat(NetCommandRef *msg) { - if (m_lastCommand == NULL) { - return FALSE; - } - if (m_lastCommand->getCommand()->getNetCommandType() != msg->getCommand()->getNetCommandType()) { - return FALSE; - } - if (msg->getCommand()->getNetCommandType() == NETCOMMANDTYPE_ACKBOTH) { - return isAckBothRepeat(msg); - } - if (msg->getCommand()->getNetCommandType() == NETCOMMANDTYPE_ACKSTAGE1) { - return isAckStage1Repeat(msg); - } - if (msg->getCommand()->getNetCommandType() == NETCOMMANDTYPE_ACKSTAGE2) { - return isAckStage2Repeat(msg); - } - return FALSE; -} - -Bool NetPacket::isAckBothRepeat(NetCommandRef *msg) { - NetAckBothCommandMsg *ack = (NetAckBothCommandMsg *)(msg->getCommand()); - NetAckBothCommandMsg *lastAck = (NetAckBothCommandMsg *)(m_lastCommand->getCommand()); - if (lastAck->getCommandID() != (ack->getCommandID() - 1)) { - return FALSE; - } - if (lastAck->getOriginalPlayerID() != ack->getOriginalPlayerID()) { - return FALSE; - } - if (msg->getRelay() != m_lastCommand->getRelay()) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::isAckStage1Repeat(NetCommandRef *msg) { - NetAckStage2CommandMsg *ack = (NetAckStage2CommandMsg *)(msg->getCommand()); - NetAckStage2CommandMsg *lastAck = (NetAckStage2CommandMsg *)(m_lastCommand->getCommand()); - if (lastAck->getCommandID() != (ack->getCommandID() - 1)) { - return FALSE; - } - if (lastAck->getOriginalPlayerID() != ack->getOriginalPlayerID()) { - return FALSE; - } - if (msg->getRelay() != m_lastCommand->getRelay()) { - return FALSE; - } - return TRUE; -} - -Bool NetPacket::isAckStage2Repeat(NetCommandRef *msg) { - NetAckStage2CommandMsg *ack = (NetAckStage2CommandMsg *)(msg->getCommand()); - NetAckStage2CommandMsg *lastAck = (NetAckStage2CommandMsg *)(m_lastCommand->getCommand()); - if (lastAck->getCommandID() != (ack->getCommandID() - 1)) { - return FALSE; - } - if (lastAck->getOriginalPlayerID() != ack->getOriginalPlayerID()) { - return FALSE; - } - if (msg->getRelay() != m_lastCommand->getRelay()) { - return FALSE; - } - return TRUE; -} - -/** - * Adds this game command to the packet. Returns true if successful. - */ -Bool NetPacket::addGameCommand(NetCommandRef *msg) { - Bool retval = FALSE; - NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)(msg->getCommand()); - // get the game message from the NetCommandMsg - GameMessage *gmsg = cmdMsg->constructGameMessage(); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addGameCommand for command ID %d", cmdMsg->getID())); - - if (isRoomForGameMessage(msg, gmsg)) { - // Now we know there is enough room, put the new game message into the packet. - - Bool needNewCommandID = FALSE; // this is to allow us to force the starting command ID to be respecified with this command. - - // If necessary, put the NetCommandType into the packet. - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - m_packet[m_packetLen] = 'T'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getNetCommandType(); - m_packetLen += sizeof(UnsignedByte); - - m_lastCommandType = cmdMsg->getNetCommandType(); - } - - // If necessary, put the execution frame into the packet. - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - m_packet[m_packetLen] = 'F'; - ++m_packetLen; - UnsignedInt newframe = cmdMsg->getExecutionFrame(); - memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); - m_packetLen += sizeof(UnsignedInt); - - m_lastFrame = newframe; - } - - // If necessary, put the relay into the packet. - if (m_lastRelay != msg->getRelay()) { - m_packet[m_packetLen] = 'R'; - ++m_packetLen; - UnsignedByte newRelay = msg->getRelay(); - memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); - m_packetLen += sizeof(UnsignedByte); - - m_lastRelay = newRelay; - } - - // If necessary, put the playerID into the packet. - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - m_packet[m_packetLen] = 'P'; - ++m_packetLen; - m_packet[m_packetLen] = cmdMsg->getPlayerID(); - m_packetLen += sizeof(UnsignedByte); - //since if we have a new player we need to respecify the starting command ID, lets force the command id to be different. - needNewCommandID = TRUE; - - m_lastPlayerID = cmdMsg->getPlayerID(); - } - - // If necessary, specify the command ID of this command. - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - m_packet[m_packetLen] = 'C'; - ++m_packetLen; - UnsignedShort newID = cmdMsg->getID(); - memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); - m_packetLen += sizeof(UnsignedShort); - } - m_lastCommandID = cmdMsg->getID(); - - m_packet[m_packetLen] = 'D'; - ++m_packetLen; - - // Now copy the GameMessage type into the packet. - GameMessage::Type newType = gmsg->getType(); - memcpy(m_packet + m_packetLen, &newType, sizeof(GameMessage::Type)); - m_packetLen += sizeof(GameMessage::Type); - - - GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); - UnsignedByte numTypes = parser->getNumTypes(); - memcpy(m_packet + m_packetLen, &numTypes, sizeof(numTypes)); - m_packetLen += sizeof(numTypes); - - GameMessageParserArgumentType *argType = parser->getFirstArgumentType(); - while (argType != NULL) { - UnsignedByte type = (UnsignedByte)(argType->getType()); - memcpy(m_packet + m_packetLen, &type, sizeof(type)); - m_packetLen += sizeof(type); - - UnsignedByte argTypeCount = argType->getArgCount(); - memcpy(m_packet + m_packetLen, &argTypeCount, sizeof(argTypeCount)); - m_packetLen += sizeof(argTypeCount); - - argType = argType->getNext(); - } - - Int numArgs = gmsg->getArgumentCount(); - for (Int i = 0; i < numArgs; ++i) { - GameMessageArgumentDataType type = gmsg->getArgumentDataType(i); - GameMessageArgumentType arg = *(gmsg->getArgument(i)); - writeGameMessageArgumentToPacket(type, arg); - } - - deleteInstance(parser); - parser = NULL; - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addGameMessage - added game message, frame %d, player %d, command ID %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); - - ++m_numCommands; - - if (m_lastCommand != NULL) { - deleteInstance(m_lastCommand); - m_lastCommand = NULL; - } - m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); - m_lastCommand->setRelay(msg->getRelay()); - - retval = TRUE; - } - - if (gmsg) { - deleteInstance(gmsg); - gmsg = NULL; - } - - return retval; -} - -void NetPacket::writeGameMessageArgumentToPacket(GameMessageArgumentDataType type, GameMessageArgumentType arg) { - if (type == ARGUMENTDATATYPE_INTEGER) { - memcpy(m_packet + m_packetLen, &(arg.integer), sizeof(arg.integer)); - m_packetLen += sizeof(arg.integer); - } else if (type == ARGUMENTDATATYPE_REAL) { - memcpy(m_packet + m_packetLen, &(arg.real), sizeof(arg.real)); - m_packetLen += sizeof(arg.real); - } else if (type == ARGUMENTDATATYPE_BOOLEAN) { - memcpy(m_packet + m_packetLen, &(arg.boolean), sizeof(arg.boolean)); - m_packetLen += sizeof(arg.boolean); - } else if (type == ARGUMENTDATATYPE_OBJECTID) { - memcpy(m_packet + m_packetLen, &(arg.objectID), sizeof(arg.objectID)); - m_packetLen += sizeof(arg.objectID); - } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { - memcpy(m_packet + m_packetLen, &(arg.drawableID), sizeof(arg.drawableID)); - m_packetLen += sizeof(arg.drawableID); - } else if (type == ARGUMENTDATATYPE_TEAMID) { - memcpy(m_packet + m_packetLen, &(arg.teamID), sizeof(arg.teamID)); - m_packetLen += sizeof(arg.teamID); - } else if (type == ARGUMENTDATATYPE_LOCATION) { - memcpy(m_packet + m_packetLen, &(arg.location), sizeof(arg.location)); - m_packetLen += sizeof(arg.location); - } else if (type == ARGUMENTDATATYPE_PIXEL) { - memcpy(m_packet + m_packetLen, &(arg.pixel), sizeof(arg.pixel)); - m_packetLen += sizeof(arg.pixel); - } else if (type == ARGUMENTDATATYPE_PIXELREGION) { - memcpy(m_packet + m_packetLen, &(arg.pixelRegion), sizeof(arg.pixelRegion)); - m_packetLen += sizeof(arg.pixelRegion); - } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { - memcpy(m_packet + m_packetLen, &(arg.timestamp), sizeof(arg.timestamp)); - m_packetLen += sizeof(arg.timestamp); - } else if (type == ARGUMENTDATATYPE_WIDECHAR) { - memcpy(m_packet + m_packetLen, &(arg.wChar), sizeof(arg.wChar)); - m_packetLen += sizeof(arg.wChar); - } -} - -/** - * Returns true if there is enough room in this packet for this message. - */ -Bool NetPacket::isRoomForGameMessage(NetCommandRef *msg, GameMessage *gmsg) { - // Calculate how much space the NetCommandMsg will take in this packet. - Int msglen = 0; - - NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)(msg->getCommand()); - - Bool needNewCommandID = FALSE; - - if (m_lastFrame != cmdMsg->getExecutionFrame()) { - msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); - } - if (m_lastPlayerID != cmdMsg->getPlayerID()) { - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - needNewCommandID = TRUE; - } - if (m_lastRelay != msg->getRelay()) { - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (m_lastCommandType != cmdMsg->getNetCommandType()) { - msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); - } - if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { - msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); - } - - GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); - - ++msglen; // for 'D' - msglen += sizeof(GameMessage::Type); - msglen += sizeof(UnsignedByte); -// Int numTypes = parser->getNumTypes(); - GameMessageParserArgumentType *arg = parser->getFirstArgumentType(); - while (arg != NULL) { - msglen += 2 * sizeof(UnsignedByte); // for the type and number of args of that type declaration. - GameMessageArgumentDataType type = arg->getType(); - if (type == ARGUMENTDATATYPE_INTEGER) { - msglen += arg->getArgCount() * sizeof(Int); - } else if (type == ARGUMENTDATATYPE_REAL) { - msglen += arg->getArgCount() * sizeof(Real); - } else if (type == ARGUMENTDATATYPE_BOOLEAN) { - msglen += arg->getArgCount() * sizeof(Bool); - } else if (type == ARGUMENTDATATYPE_OBJECTID) { - msglen += arg->getArgCount() * sizeof(ObjectID); - } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { - msglen += arg->getArgCount() * sizeof(DrawableID); - } else if (type == ARGUMENTDATATYPE_TEAMID) { - msglen += arg->getArgCount() * sizeof(UnsignedInt); - } else if (type == ARGUMENTDATATYPE_LOCATION) { - msglen += arg->getArgCount() * sizeof(Coord3D); - } else if (type == ARGUMENTDATATYPE_PIXEL) { - msglen += arg->getArgCount() * sizeof(ICoord2D); - } else if (type == ARGUMENTDATATYPE_PIXELREGION) { - msglen += arg->getArgCount() * sizeof(IRegion2D); - } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { - msglen += arg->getArgCount() * sizeof(UnsignedInt); - } else if (type == ARGUMENTDATATYPE_WIDECHAR) { - msglen += arg->getArgCount() * sizeof(WideChar); - } - arg = arg->getNext(); - } - - deleteInstance(parser); - parser = NULL; - - // Is there enough room in the packet for this message? - if (msglen > (MAX_PACKET_SIZE - m_packetLen)) { - return FALSE; // there isn't enough room in this packet for this new message. - } - return TRUE; -} - -/** - * Returns the list of commands that are in this packet. - */ -NetCommandList * NetPacket::getCommandList() { - NetCommandList *retval = newInstance(NetCommandList); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList, packet length = %d", m_packetLen)); - retval->init(); - - // These need to be the same as the default values for m_lastPlayerID, m_lastFrame, etc. - UnsignedByte playerID = 0; - UnsignedInt frame = 0; - UnsignedShort commandID = 1; // The first command is going to be - UnsignedByte commandType = 0; - UnsignedByte relay = 0; - NetCommandRef *lastCommand = NULL; - - Int i = 0; - while (i < m_packetLen) { - if (m_packet[i] == 'T') { - ++i; - memcpy(&commandType, m_packet + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - } else if (m_packet[i] == 'F') { - ++i; - memcpy(&frame, m_packet + i, sizeof(UnsignedInt)); - i += sizeof(UnsignedInt); - } else if (m_packet[i] == 'P') { - ++i; - memcpy(&playerID, m_packet + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - } else if (m_packet[i] == 'R') { - ++i; - memcpy(&relay, m_packet + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - } else if (m_packet[i] == 'C') { - ++i; - memcpy(&commandID, m_packet + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - } else if (m_packet[i] == 'D') { - ++i; - - NetCommandMsg *msg = NULL; - - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList() - command of type %d(%s)", commandType, GetNetCommandTypeAsString((NetCommandType)commandType))); - - switch((NetCommandType)commandType) - { - case NETCOMMANDTYPE_GAMECOMMAND: - msg = readGameMessage(m_packet, i); - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read game command from player %d for frame %d", playerID, frame)); - break; - case NETCOMMANDTYPE_ACKBOTH: - msg = readAckBothMessage(m_packet, i); - break; - case NETCOMMANDTYPE_ACKSTAGE1: - msg = readAckStage1Message(m_packet, i); - break; - case NETCOMMANDTYPE_ACKSTAGE2: - msg = readAckStage2Message(m_packet, i); - break; - case NETCOMMANDTYPE_FRAMEINFO: - msg = readFrameMessage(m_packet, i); - // frameinfodebug - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read frame %d from player %d, command count = %d, relay = 0x%X", frame, playerID, ((NetFrameCommandMsg *)msg)->getCommandCount(), relay)); - break; - case NETCOMMANDTYPE_PLAYERLEAVE: - msg = readPlayerLeaveMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read player leave message from player %d for execution on frame %d", playerID, frame)); - break; - case NETCOMMANDTYPE_RUNAHEADMETRICS: - msg = readRunAheadMetricsMessage(m_packet, i); - break; - case NETCOMMANDTYPE_RUNAHEAD: - msg = readRunAheadMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read run ahead message from player %d for execution on frame %d", playerID, frame)); - break; - case NETCOMMANDTYPE_DESTROYPLAYER: - msg = readDestroyPlayerMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read CRC info message from player %d for execution on frame %d", playerID, frame)); - break; - case NETCOMMANDTYPE_KEEPALIVE: - msg = readKeepAliveMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read keep alive message from player %d", playerID)); - break; - case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: - msg = readDisconnectKeepAliveMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read keep alive message from player %d", playerID)); - break; - case NETCOMMANDTYPE_DISCONNECTPLAYER: - msg = readDisconnectPlayerMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect player message from player %d", playerID)); - break; - case NETCOMMANDTYPE_PACKETROUTERQUERY: - msg = readPacketRouterQueryMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read packet router query message from player %d", playerID)); - break; - case NETCOMMANDTYPE_PACKETROUTERACK: - msg = readPacketRouterAckMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read packet router ack message from player %d", playerID)); - break; - case NETCOMMANDTYPE_DISCONNECTCHAT: - msg = readDisconnectChatMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect chat message from player %d", playerID)); - break; - case NETCOMMANDTYPE_DISCONNECTVOTE: - msg = readDisconnectVoteMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect vote message from player %d", playerID)); - break; - case NETCOMMANDTYPE_CHAT: - msg = readChatMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read chat message from player %d", playerID)); - break; - case NETCOMMANDTYPE_PROGRESS: - msg = readProgressMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read Progress message from player %d", playerID)); - break; - case NETCOMMANDTYPE_LOADCOMPLETE: - msg = readLoadCompleteMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read LoadComplete message from player %d", playerID)); - break; - case NETCOMMANDTYPE_TIMEOUTSTART: - msg = readTimeOutGameStartMessage(m_packet, i); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read TimeOutGameStart message from player %d", playerID)); - break; - case NETCOMMANDTYPE_WRAPPER: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read Wrapper message from player %d", playerID)); - msg = readWrapperMessage(m_packet, i); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Done reading Wrapper message from player %d - wrapped command was %d", playerID, - ((NetWrapperCommandMsg *)msg)->getWrappedCommandID())); - break; - case NETCOMMANDTYPE_FILE: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read file message from player %d", playerID)); - msg = readFileMessage(m_packet, i); - break; - case NETCOMMANDTYPE_FILEANNOUNCE: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read file announce message from player %d", playerID)); - msg = readFileAnnounceMessage(m_packet, i); - break; - case NETCOMMANDTYPE_FILEPROGRESS: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read file progress message from player %d", playerID)); - msg = readFileProgressMessage(m_packet, i); - break; - case NETCOMMANDTYPE_DISCONNECTFRAME: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect frame message from player %d", playerID)); - msg = readDisconnectFrameMessage(m_packet, i); - break; - case NETCOMMANDTYPE_DISCONNECTSCREENOFF: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect screen off message from player %d", playerID)); - msg = readDisconnectScreenOffMessage(m_packet, i); - break; - case NETCOMMANDTYPE_FRAMERESENDREQUEST: - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read frame resend request message from player %d", playerID)); - msg = readFrameResendRequestMessage(m_packet, i); - break; - } - - if (msg == NULL) { - DEBUG_CRASH(("Didn't read a message from the packet. Things are about to go wrong.")); - continue; - } - - // set the info - msg->setExecutionFrame(frame); - msg->setPlayerID(playerID); - msg->setNetCommandType((NetCommandType)commandType); - msg->setID(commandID); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("frame = %d, player = %d, command type = %d, id = %d", frame, playerID, commandType, commandID)); - - // increment to the next command ID. - if (DoesCommandRequireACommandID((NetCommandType)commandType)) { - ++commandID; - } - - // add the message to the list. - NetCommandRef *ref = retval->addMessage(msg); - if (ref != NULL) { - ref->setRelay(relay); - } else { - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList - failed to set relay for message %d", msg->getID())); - } - - if (lastCommand != NULL) { - deleteInstance(lastCommand); - lastCommand = NULL; - } - lastCommand = newInstance(NetCommandRef)(msg); - - msg->detach(); // Need to detach from new NetCommandMsg created by the "readXMessage" above. - - // since the message is part of the list now, we don't have to keep track of it. So we'll just set it to NULL. - msg = NULL; - } else if (m_packet[i] == 'Z') { - ++i; - // Repeat the last command, doing some funky cool byte-saving stuff - if (lastCommand == NULL) { - DEBUG_CRASH(("Got a repeat command with no command to repeat.")); - } - NetCommandMsg *msg = NULL; - if (commandType == NETCOMMANDTYPE_ACKSTAGE1) { - msg = newInstance(NetAckStage1CommandMsg)(); - NetAckStage1CommandMsg *last = (NetAckStage1CommandMsg *)(lastCommand->getCommand()); - ((NetAckStage1CommandMsg *)msg)->setCommandID(last->getCommandID() + 1); - ((NetAckStage1CommandMsg *)msg)->setOriginalPlayerID(last->getOriginalPlayerID()); - } else if (commandType == NETCOMMANDTYPE_ACKSTAGE2) { - msg = newInstance(NetAckStage2CommandMsg)(); - NetAckStage2CommandMsg *last = (NetAckStage2CommandMsg *)(lastCommand->getCommand()); - ((NetAckStage2CommandMsg *)msg)->setCommandID(last->getCommandID() + 1); - ((NetAckStage2CommandMsg *)msg)->setOriginalPlayerID(last->getOriginalPlayerID()); - } else if (commandType == NETCOMMANDTYPE_ACKBOTH) { - msg = newInstance(NetAckBothCommandMsg)(); - NetAckBothCommandMsg *last = (NetAckBothCommandMsg *)(lastCommand->getCommand()); - ((NetAckBothCommandMsg *)msg)->setCommandID(last->getCommandID() + 1); - ((NetAckBothCommandMsg *)msg)->setOriginalPlayerID(last->getOriginalPlayerID()); - } else if (commandType == NETCOMMANDTYPE_FRAMEINFO) { - msg = newInstance(NetFrameCommandMsg)(); - ++frame; // this is set below. - ((NetFrameCommandMsg *)msg)->setCommandCount(0); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Read a repeated frame command, frame = %d, player = %d, commandID = %d", frame, playerID, commandID)); - } else { - DEBUG_CRASH(("Trying to repeat a command that shouldn't be repeated.")); - continue; - } - - msg->setExecutionFrame(frame); - msg->setPlayerID(playerID); - msg->setNetCommandType((NetCommandType)commandType); - msg->setID(commandID); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("frame = %d, player = %d, command type = %d, id = %d", frame, playerID, commandType, commandID)); - - // increment to the next command ID. - if (DoesCommandRequireACommandID((NetCommandType)commandType)) { - ++commandID; - } - - // add the message to the list. - NetCommandRef *ref = retval->addMessage(msg); - if (ref != NULL) { - ref->setRelay(relay); - } - - deleteInstance(lastCommand); - lastCommand = NULL; -// lastCommand = newInstance(NetCommandRef)(msg); - lastCommand = NEW_NETCOMMANDREF(msg); - - msg->detach(); // Need to detach from new NetCommandMsg created by the "readXMessage" above. - - // since the message is part of the list now, we don't have to keep track of it. So we'll just set it to NULL. - msg = NULL; - } else { - // we don't recognize this command, but we have to increment i so we don't fall into an infinite loop. - DEBUG_CRASH(("Unrecognized packet entry, ignoring.")); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList - Unrecognized packet entry at index %d", i)); - dumpPacketToLog(); - ++i; - } - } - - if (lastCommand != NULL) { - deleteInstance(lastCommand); - lastCommand = NULL; - } - return retval; -} - -/** - * Reads the data portion of a game message from the given position in the packet. - */ -NetCommandMsg * NetPacket::readGameMessage(UnsignedByte *data, Int &i) -{ - NetGameCommandMsg *msg = newInstance(NetGameCommandMsg); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readGameMessage")); - - // Get the GameMessage command type. - GameMessage::Type newType; - memcpy(&newType, data + i, sizeof(GameMessage::Type)); - i += sizeof(GameMessage::Type); - msg->setGameMessageType(newType); - - // Get the number of argument types - UnsignedByte numArgTypes = 0; - memcpy(&numArgTypes, data + i, sizeof(numArgTypes)); - i += sizeof(numArgTypes); - - // Get the types and the number of arguments of those types. - Int totalArgCount = 0; - GameMessageParser *parser = newInstance(GameMessageParser)(); - Int j = 0; - for (; j < numArgTypes; ++j) { - UnsignedByte type = (UnsignedByte)ARGUMENTDATATYPE_UNKNOWN; - memcpy(&type, data + i, sizeof(type)); - i += sizeof(type); - - UnsignedByte argCount = 0; - memcpy(&argCount, data + i, sizeof(argCount)); - i += sizeof(argCount); - - parser->addArgType((GameMessageArgumentDataType)type, argCount); - totalArgCount += argCount; - } - - GameMessageParserArgumentType *parserArgType = parser->getFirstArgumentType(); - GameMessageArgumentDataType lasttype = ARGUMENTDATATYPE_UNKNOWN; - Int argsLeftForType = 0; - if (parserArgType != NULL) { - lasttype = parserArgType->getType(); - argsLeftForType = parserArgType->getArgCount(); - } - for (j = 0; j < totalArgCount; ++j) { - readGameMessageArgumentFromPacket(lasttype, msg, data, i); - - --argsLeftForType; - if (argsLeftForType == 0) { - DEBUG_ASSERTCRASH(parserArgType != NULL, ("parserArgType was NULL when it shouldn't have been.")); - if (parserArgType == NULL) { - return NULL; - } - - parserArgType = parserArgType->getNext(); - // parserArgType is allowed to be NULL here - if (parserArgType != NULL) { - argsLeftForType = parserArgType->getArgCount(); - lasttype = parserArgType->getType(); - } - } - } - - deleteInstance(parser); - parser = NULL; - - return (NetCommandMsg *)msg; -} - -void NetPacket::readGameMessageArgumentFromPacket(GameMessageArgumentDataType type, NetGameCommandMsg *msg, UnsignedByte *data, Int &i) { - if (type == ARGUMENTDATATYPE_INTEGER) { - GameMessageArgumentType arg; - Int theint; - memcpy(&theint, data + i, sizeof(theint)); - i += sizeof(theint); - arg.integer = theint; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_REAL) { - GameMessageArgumentType arg; - Real thereal; - memcpy(&thereal, data + i, sizeof(thereal)); - i += sizeof(thereal); - arg.real = thereal; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_BOOLEAN) { - GameMessageArgumentType arg; - Bool thebool; - memcpy(&thebool, data + i, sizeof(thebool)); - i += sizeof(thebool); - arg.boolean = thebool; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_OBJECTID) { - GameMessageArgumentType arg; - ObjectID theint; - memcpy(&theint, data + i, sizeof(theint)); - i += sizeof(theint); - arg.objectID = theint; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_DRAWABLEID) { - GameMessageArgumentType arg; - DrawableID theint; - memcpy(&theint, data + i, sizeof(theint)); - i += sizeof(theint); - arg.drawableID = theint; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_TEAMID) { - GameMessageArgumentType arg; - UnsignedInt theint; - memcpy(&theint, data + i, sizeof(theint)); - i += sizeof(theint); - arg.teamID = theint; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_LOCATION) { - GameMessageArgumentType arg; - Coord3D coord; - memcpy(&coord, data + i, sizeof(coord)); - i += sizeof(coord); - arg.location = coord; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_PIXEL) { - GameMessageArgumentType arg; - ICoord2D pixel; - memcpy(&pixel, data + i, sizeof(pixel)); - i += sizeof(pixel); - arg.pixel = pixel; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_PIXELREGION) { - GameMessageArgumentType arg; - IRegion2D reg; - memcpy(®, data + i, sizeof(reg)); - i += sizeof(reg); - arg.pixelRegion = reg; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_TIMESTAMP) { - GameMessageArgumentType arg; - UnsignedInt stamp; - memcpy(&stamp, data + i, sizeof(stamp)); - i += sizeof(stamp); - arg.timestamp = stamp; - msg->addArgument(type, arg); - } else if (type == ARGUMENTDATATYPE_WIDECHAR) { - GameMessageArgumentType arg; - WideChar c; - memcpy(&c, data + i, sizeof(c)); - i += sizeof(c); - arg.wChar = c; - msg->addArgument(type, arg); - } -} - -/** - * Reads the data portion of the ack message at this position in the packet. - */ -NetCommandMsg * NetPacket::readAckBothMessage(UnsignedByte *data, Int &i) { - NetAckBothCommandMsg *msg = newInstance(NetAckBothCommandMsg); - - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readAckMessage, ")); - UnsignedShort cmdID = 0; - - memcpy(&cmdID, data + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - msg->setCommandID(cmdID); - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("commandID = %d, ", cmdID)); - - UnsignedByte origPlayerID = 0; - memcpy(&origPlayerID, data + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - msg->setOriginalPlayerID(origPlayerID); - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("original player id = %d", origPlayerID)); - - return msg; -} - -/** - * Reads the data portion of the ack message at this position in the packet. - */ -NetCommandMsg * NetPacket::readAckStage1Message(UnsignedByte *data, Int &i) { - NetAckStage1CommandMsg *msg = newInstance(NetAckStage1CommandMsg); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readAckMessage, ")); - UnsignedShort cmdID = 0; - - memcpy(&cmdID, data + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - msg->setCommandID(cmdID); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("commandID = %d, ", cmdID)); - - UnsignedByte origPlayerID = 0; - memcpy(&origPlayerID, data + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - msg->setOriginalPlayerID(origPlayerID); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("original player id = %d", origPlayerID)); - - return msg; -} - -/** - * Reads the data portion of the ack message at this position in the packet. - */ -NetCommandMsg * NetPacket::readAckStage2Message(UnsignedByte *data, Int &i) { - NetAckStage2CommandMsg *msg = newInstance(NetAckStage2CommandMsg); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readAckMessage, ")); - UnsignedShort cmdID = 0; - - memcpy(&cmdID, data + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - msg->setCommandID(cmdID); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("commandID = %d, ", cmdID)); - - UnsignedByte origPlayerID = 0; - memcpy(&origPlayerID, data + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - msg->setOriginalPlayerID(origPlayerID); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("original player id = %d", origPlayerID)); - - return msg; -} - -/** - * Reads the data portion of the frame message at this position in the packet. - */ -NetCommandMsg * NetPacket::readFrameMessage(UnsignedByte *data, Int &i) { - NetFrameCommandMsg *msg = newInstance(NetFrameCommandMsg); - -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readFrameMessage, ")); - UnsignedShort cmdCount = 0; - - memcpy(&cmdCount, data + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - msg->setCommandCount(cmdCount); -// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command count = %d, ", cmdCount)); - - return msg; -} - -/** - * Reads the player leave message at this position in the packet. - */ -NetCommandMsg * NetPacket::readPlayerLeaveMessage(UnsignedByte *data, Int &i) { - NetPlayerLeaveCommandMsg *msg = newInstance(NetPlayerLeaveCommandMsg); - - UnsignedByte leavingPlayerID = 0; - - memcpy(&leavingPlayerID, data + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - msg->setLeavingPlayerID(leavingPlayerID); - - return msg; -} - -/** - * Reads the run ahead metrics message at this position in the packet. - */ -NetCommandMsg * NetPacket::readRunAheadMetricsMessage(UnsignedByte *data, Int &i) { - NetRunAheadMetricsCommandMsg *msg = newInstance(NetRunAheadMetricsCommandMsg); - - Real averageLatency = (Real)0.2; - UnsignedShort averageFps = 30; - - memcpy(&averageLatency, data + i, sizeof(Real)); - i += sizeof(Real); - msg->setAverageLatency(averageLatency); - - memcpy(&averageFps, data + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - msg->setAverageFps((Int)averageFps); - return msg; -} - -/** - * Reads the run ahead message at this position in the packet. - */ -NetCommandMsg * NetPacket::readRunAheadMessage(UnsignedByte *data, Int &i) { - NetRunAheadCommandMsg *msg = newInstance(NetRunAheadCommandMsg); - - UnsignedShort newRunAhead = 20; - memcpy(&newRunAhead, data + i, sizeof(UnsignedShort)); - i += sizeof(UnsignedShort); - msg->setRunAhead(newRunAhead); - - UnsignedByte newFrameRate = 30; - memcpy(&newFrameRate, data + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - msg->setFrameRate(newFrameRate); - - return msg; -} - -/** - * Reads the CRC info message at this position in the packet. - */ -NetCommandMsg * NetPacket::readDestroyPlayerMessage(UnsignedByte *data, Int &i) { - NetDestroyPlayerCommandMsg *msg = newInstance(NetDestroyPlayerCommandMsg); - - UnsignedInt newVal = 0; - memcpy(&newVal, data + i, sizeof(UnsignedInt)); - i += sizeof(UnsignedInt); - msg->setPlayerIndex(newVal); - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Saw CRC of 0x%8.8X", newCRC)); - - return msg; -} - -/** - * Reads the keep alive data, of which there is none. - */ -NetCommandMsg * NetPacket::readKeepAliveMessage(UnsignedByte *data, Int &i) { - NetKeepAliveCommandMsg *msg = newInstance(NetKeepAliveCommandMsg); - - return msg; -} - -/** - * Reads the disconnect keep alive data, of which there is none. - */ -NetCommandMsg * NetPacket::readDisconnectKeepAliveMessage(UnsignedByte *data, Int &i) { - NetDisconnectKeepAliveCommandMsg *msg = newInstance(NetDisconnectKeepAliveCommandMsg); - - return msg; -} - -/** - * Reads the disconnect player data. Which is the slot number of the player being disconnected. - */ -NetCommandMsg * NetPacket::readDisconnectPlayerMessage(UnsignedByte *data, Int &i) { - NetDisconnectPlayerCommandMsg *msg = newInstance(NetDisconnectPlayerCommandMsg); - - UnsignedByte slot = 0; - memcpy(&slot, data + i, sizeof(slot)); - i += sizeof(slot); - msg->setDisconnectSlot(slot); - - UnsignedInt disconnectFrame = 0; - memcpy(&disconnectFrame, data + i, sizeof(disconnectFrame)); - i += sizeof(disconnectFrame); - msg->setDisconnectFrame(disconnectFrame); - - return msg; -} - -/** - * Reads the packet router query data, of which there is none. - */ -NetCommandMsg * NetPacket::readPacketRouterQueryMessage(UnsignedByte *data, Int &i) { - NetPacketRouterQueryCommandMsg *msg = newInstance(NetPacketRouterQueryCommandMsg); - - return msg; -} - -/** - * Reads the packet router ack data, of which there is none. - */ -NetCommandMsg * NetPacket::readPacketRouterAckMessage(UnsignedByte *data, Int &i) { - NetPacketRouterAckCommandMsg *msg = newInstance(NetPacketRouterAckCommandMsg); - - return msg; -} - -/** - * Reads the disconnect chat data, which is just the string. - */ -NetCommandMsg * NetPacket::readDisconnectChatMessage(UnsignedByte *data, Int &i) { - NetDisconnectChatCommandMsg *msg = newInstance(NetDisconnectChatCommandMsg); - - WideChar text[256]; - UnsignedByte length; - memcpy(&length, data + i, sizeof(UnsignedByte)); - ++i; - memcpy(text, data + i, length * sizeof(WideChar)); - i += length * sizeof(WideChar); - text[length] = 0; - - UnicodeString unitext; - unitext.set(text); - - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readDisconnectChatMessage - read message, message is %ls", unitext.str())); - - msg->setText(unitext); - return msg; -} - -/** - * Reads the chat data, which is just the string. - */ -NetCommandMsg * NetPacket::readChatMessage(UnsignedByte *data, Int &i) { - NetChatCommandMsg *msg = newInstance(NetChatCommandMsg); - - WideChar text[256]; - UnsignedByte length; - Int playerMask; - memcpy(&length, data + i, sizeof(UnsignedByte)); - ++i; - memcpy(text, data + i, length * sizeof(WideChar)); - i += length * sizeof(WideChar); - text[length] = 0; - memcpy(&playerMask, data + i, sizeof(Int)); - i += sizeof(Int); - - - UnicodeString unitext; - unitext.set(text); - - //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readChatMessage - read message, message is %ls", unitext.str())); - - msg->setText(unitext); - msg->setPlayerMask(playerMask); - return msg; -} - -/** - * Reads the disconnect vote data. Which is the slot number of the player being disconnected. - */ -NetCommandMsg * NetPacket::readDisconnectVoteMessage(UnsignedByte *data, Int &i) { - NetDisconnectVoteCommandMsg *msg = newInstance(NetDisconnectVoteCommandMsg); - - UnsignedByte slot = 0; - memcpy(&slot, data + i, sizeof(slot)); - i += sizeof(slot); - msg->setSlot(slot); - - UnsignedInt voteFrame = 0; - memcpy(&voteFrame, data + i, sizeof(voteFrame)); - i += sizeof(voteFrame); - msg->setVoteFrame(voteFrame); - - return msg; -} - -/** - * Reads the Progress data. Which is the slot number of the player being disconnected. - */ -NetCommandMsg * NetPacket::readProgressMessage(UnsignedByte *data, Int &i) { - NetProgressCommandMsg *msg = newInstance(NetProgressCommandMsg); - - UnsignedByte percentage = 0; - memcpy(&percentage, data + i, sizeof(UnsignedByte)); - i += sizeof(UnsignedByte); - msg->setPercentage(percentage); - - return msg; -} - -NetCommandMsg * NetPacket::readLoadCompleteMessage(UnsignedByte *data, Int &i) { - NetCommandMsg *msg = newInstance(NetCommandMsg); - return msg; -} - -NetCommandMsg * NetPacket::readTimeOutGameStartMessage(UnsignedByte *data, Int &i) { - NetCommandMsg *msg = newInstance(NetCommandMsg); - return msg; -} - -NetCommandMsg * NetPacket::readWrapperMessage(UnsignedByte *data, Int &i) { - NetWrapperCommandMsg *msg = newInstance(NetWrapperCommandMsg); - - // get the wrapped command ID - UnsignedShort wrappedCommandID = 0; - memcpy(&wrappedCommandID, data + i, sizeof(wrappedCommandID)); - msg->setWrappedCommandID(wrappedCommandID); - i += sizeof(wrappedCommandID); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - wrapped command ID == %d", wrappedCommandID)); - - // get the chunk number. - UnsignedInt chunkNumber = 0; - memcpy(&chunkNumber, data + i, sizeof(chunkNumber)); - msg->setChunkNumber(chunkNumber); - i += sizeof(chunkNumber); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - chunk number = %d", chunkNumber)); - - // get the number of chunks - UnsignedInt numChunks = 0; - memcpy(&numChunks, data + i, sizeof(numChunks)); - msg->setNumChunks(numChunks); - i += sizeof(numChunks); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - number of chunks = %d", numChunks)); - - // get the total data length - UnsignedInt totalDataLength = 0; - memcpy(&totalDataLength, data + i, sizeof(totalDataLength)); - msg->setTotalDataLength(totalDataLength); - i += sizeof(totalDataLength); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - total data length = %d", totalDataLength)); - - // get the data length for this chunk - UnsignedInt dataLength = 0; - memcpy(&dataLength, data + i, sizeof(dataLength)); - i += sizeof(dataLength); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - data length = %d", dataLength)); - - UnsignedInt dataOffset = 0; - memcpy(&dataOffset, data + i, sizeof(dataOffset)); - msg->setDataOffset(dataOffset); - i += sizeof(dataOffset); - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - data offset = %d", dataOffset)); - - msg->setData(data + i, dataLength); - i += dataLength; - - return msg; -} - -NetCommandMsg * NetPacket::readFileMessage(UnsignedByte *data, Int &i) { - NetFileCommandMsg *msg = newInstance(NetFileCommandMsg); - char filename[_MAX_PATH]; - char *c = filename; - - while (data[i] != 0) { - *c = data[i]; - ++c; - ++i; - } - *c = 0; - ++i; - msg->setPortableFilename(AsciiString(filename)); // it's transferred as a portable filename - - UnsignedInt dataLength = 0; - memcpy(&dataLength, data + i, sizeof(dataLength)); - i += sizeof(dataLength); - - UnsignedByte *buf = NEW UnsignedByte[dataLength]; - memcpy(buf, data + i, dataLength); - i += dataLength; - - msg->setFileData(buf, dataLength); - - return msg; -} - -NetCommandMsg * NetPacket::readFileAnnounceMessage(UnsignedByte *data, Int &i) { - NetFileAnnounceCommandMsg *msg = newInstance(NetFileAnnounceCommandMsg); - char filename[_MAX_PATH]; - char *c = filename; - - while (data[i] != 0) { - *c = data[i]; - ++c; - ++i; - } - *c = 0; - ++i; - msg->setPortableFilename(AsciiString(filename)); // it's transferred as a portable filename - - UnsignedShort fileID = 0; - memcpy(&fileID, data + i, sizeof(fileID)); - i += sizeof(fileID); - msg->setFileID(fileID); - - UnsignedByte playerMask = 0; - memcpy(&playerMask, data + i, sizeof(playerMask)); - i += sizeof(playerMask); - msg->setPlayerMask(playerMask); - - return msg; -} - -NetCommandMsg * NetPacket::readFileProgressMessage(UnsignedByte *data, Int &i) { - NetFileProgressCommandMsg *msg = newInstance(NetFileProgressCommandMsg); - - UnsignedShort fileID = 0; - memcpy(&fileID, data + i, sizeof(fileID)); - i += sizeof(fileID); - msg->setFileID(fileID); - - Int progress = 0; - memcpy(&progress, data + i, sizeof(progress)); - i += sizeof(progress); - msg->setProgress(progress); - - return msg; -} - -NetCommandMsg * NetPacket::readDisconnectFrameMessage(UnsignedByte *data, Int &i) { - NetDisconnectFrameCommandMsg *msg = newInstance(NetDisconnectFrameCommandMsg); - - UnsignedInt disconnectFrame = 0; - memcpy(&disconnectFrame, data + i, sizeof(disconnectFrame)); - i += sizeof(disconnectFrame); - msg->setDisconnectFrame(disconnectFrame); - - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readDisconnectFrameMessage - read disconnect frame for frame %d", disconnectFrame)); - - return msg; -} - -NetCommandMsg * NetPacket::readDisconnectScreenOffMessage(UnsignedByte *data, Int &i) { - NetDisconnectScreenOffCommandMsg *msg = newInstance(NetDisconnectScreenOffCommandMsg); - - UnsignedInt newFrame = 0; - memcpy(&newFrame, data + i, sizeof(newFrame)); - i += sizeof(newFrame); - msg->setNewFrame(newFrame); - - return msg; -} - -NetCommandMsg * NetPacket::readFrameResendRequestMessage(UnsignedByte *data, Int &i) { - NetFrameResendRequestCommandMsg *msg = newInstance(NetFrameResendRequestCommandMsg); - - UnsignedInt frameToResend = 0; - memcpy(&frameToResend, data + i, sizeof(frameToResend)); - i += sizeof(frameToResend); - msg->setFrameToResend(frameToResend); - - return msg; -} - -/** - * Returns the number of commands in this packet. Only valid if the packet is locally constructed. - */ -Int NetPacket::getNumCommands() { - return m_numCommands; -} - -/** - * Returns the address that this packet is to be sent to. Only valid if the packet is locally constructed. - */ -UnsignedInt NetPacket::getAddr() { - return m_addr; -} - -/** - * Returns the port that this packet is to be sent to. Only valid if the packet is locally constructed. - */ -UnsignedShort NetPacket::getPort() { - return m_port; -} - -/** - * Returns the data of this packet. - */ -UnsignedByte * NetPacket::getData() { - return m_packet; -} - -/** - * Returns the length of the packet. - */ -Int NetPacket::getLength() { - return m_packetLen; -} - -/** - * Dumps the packet to the debug log file - */ -void NetPacket::dumpPacketToLog() { - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::dumpPacketToLog() - packet is %d bytes", m_packetLen)); - Int numLines = m_packetLen / 8; - if ((m_packetLen % 8) != 0) { - ++numLines; - } - for (Int dumpindex = 0; dumpindex < numLines; ++dumpindex) { - DEBUG_LOG_LEVEL_RAW(DEBUG_LEVEL_NET, ("\t%d\t", dumpindex*8)); - for (Int dumpindex2 = 0; (dumpindex2 < 8) && ((dumpindex*8 + dumpindex2) < m_packetLen); ++dumpindex2) { - DEBUG_LOG_LEVEL_RAW(DEBUG_LEVEL_NET, ("%02x '%c' ", m_packet[dumpindex*8 + dumpindex2], m_packet[dumpindex*8 + dumpindex2])); - } - DEBUG_LOG_LEVEL_RAW(DEBUG_LEVEL_NET, ("\n")); - } - DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("End of packet dump")); -} +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 Electronic Arts Inc. +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +//////////////////////////////////////////////////////////////////////////////// +// // +// (c) 2001-2003 Electronic Arts Inc. // +// // +//////////////////////////////////////////////////////////////////////////////// + +////////// NetPacket.cpp /////////////////////////// + +#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine + +#include "GameNetwork/NetPacket.h" +#include "GameNetwork/NetCommandMsg.h" +#include "GameNetwork/NetworkDefs.h" +#include "GameNetwork/networkutil.h" +#include "GameNetwork/GameMessageParser.h" + + +// This function assumes that all of the fields are either of default value or are +// present in the raw data. +NetCommandRef * NetPacket::ConstructNetCommandMsgFromRawData(UnsignedByte *data, UnsignedShort dataLength) { + + NetCommandType commandType = NETCOMMANDTYPE_GAMECOMMAND; + UnsignedShort commandID = 0; + UnsignedInt frame = 0; + UnsignedByte playerID = 0; + UnsignedByte relay = 0; + + Int offset = 0; + Bool notDone = TRUE; + NetCommandRef *ref = NULL; + NetCommandMsg *msg = NULL; + + while ((offset < (Int)dataLength) && notDone) { + + switch (data[offset]) { + + case 'T': + ++offset; + memcpy(&commandType, data + offset, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + break; + + case 'R': + ++offset; + memcpy(&relay, data + offset, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + break; + + case 'P': + ++offset; + memcpy(&playerID, data + offset, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + break; + + case 'C': + ++offset; + memcpy(&commandID, data + offset, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + break; + + case 'F': + ++offset; + memcpy(&frame, data + offset, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + break; + + case 'D': + ++offset; + + switch (commandType) { + + case NETCOMMANDTYPE_GAMECOMMAND: + msg = readGameMessage(data, offset); + break; + case NETCOMMANDTYPE_ACKBOTH: + msg = readAckBothMessage(data, offset); + break; + case NETCOMMANDTYPE_ACKSTAGE1: + msg = readAckStage1Message(data, offset); + break; + case NETCOMMANDTYPE_ACKSTAGE2: + msg = readAckStage2Message(data, offset); + break; + case NETCOMMANDTYPE_FRAMEINFO: + msg = readFrameMessage(data, offset); + break; + case NETCOMMANDTYPE_PLAYERLEAVE: + msg = readPlayerLeaveMessage(data, offset); + break; + case NETCOMMANDTYPE_RUNAHEADMETRICS: + msg = readRunAheadMetricsMessage(data, offset); + break; + case NETCOMMANDTYPE_RUNAHEAD: + msg = readRunAheadMessage(data, offset); + break; + case NETCOMMANDTYPE_DESTROYPLAYER: + msg = readDestroyPlayerMessage(data, offset); + break; + case NETCOMMANDTYPE_KEEPALIVE: + msg = readKeepAliveMessage(data, offset); + break; + case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: + msg = readDisconnectKeepAliveMessage(data, offset); + break; + case NETCOMMANDTYPE_DISCONNECTPLAYER: + msg = readDisconnectPlayerMessage(data, offset); + break; + case NETCOMMANDTYPE_PACKETROUTERQUERY: + msg = readPacketRouterQueryMessage(data, offset); + break; + case NETCOMMANDTYPE_PACKETROUTERACK: + msg = readPacketRouterAckMessage(data, offset); + break; + case NETCOMMANDTYPE_DISCONNECTCHAT: + msg = readDisconnectChatMessage(data, offset); + break; + case NETCOMMANDTYPE_DISCONNECTVOTE: + msg = readDisconnectVoteMessage(data, offset); + break; + case NETCOMMANDTYPE_CHAT: + msg = readChatMessage(data, offset); + break; + case NETCOMMANDTYPE_PROGRESS: + msg = readProgressMessage(data, offset); + break; + case NETCOMMANDTYPE_LOADCOMPLETE: + msg = readLoadCompleteMessage(data, offset); + break; + case NETCOMMANDTYPE_TIMEOUTSTART: + msg = readTimeOutGameStartMessage(data, offset); + break; + case NETCOMMANDTYPE_WRAPPER: + msg = readWrapperMessage(data, offset); + break; + case NETCOMMANDTYPE_FILE: + msg = readFileMessage(data, offset); + break; + case NETCOMMANDTYPE_FILEANNOUNCE: + msg = readFileAnnounceMessage(data, offset); + break; + case NETCOMMANDTYPE_FILEPROGRESS: + msg = readFileProgressMessage(data, offset); + break; + case NETCOMMANDTYPE_DISCONNECTFRAME: + msg = readDisconnectFrameMessage(data, offset); + break; + case NETCOMMANDTYPE_DISCONNECTSCREENOFF: + msg = readDisconnectScreenOffMessage(data, offset); + break; + case NETCOMMANDTYPE_FRAMERESENDREQUEST: + msg = readFrameResendRequestMessage(data, offset); + break; + + } // switch (commandType) + + msg->setExecutionFrame(frame); + msg->setID(commandID); + msg->setPlayerID(playerID); + msg->setNetCommandType(commandType); + + ref = NEW_NETCOMMANDREF(msg); + + ref->setRelay(relay); + + msg->detach(); + msg = NULL; + + notDone = FALSE; + break; // case 'D' + + } // switch (data[offset]) + + } // while + + return ref; +} + +NetPacketList NetPacket::ConstructBigCommandPacketList(NetCommandRef *ref) { + // if we don't have a unique command ID, then the wrapped command cannot + // be identified. Therefore don't allow commands without a unique ID to + // be wrapped. + NetCommandMsg *msg = ref->getCommand(); + + if (!DoesCommandRequireACommandID(msg->getNetCommandType())) { + DEBUG_CRASH(("Trying to wrap a command that doesn't have a unique command ID")); + return NetPacketList(); + } + + UnsignedInt bufferSize = GetBufferSizeNeededForCommand(msg); // need to implement. I have a drinking problem. + UnsignedByte *bigPacketData = NULL; + + NetPacketList packetList; + + // create the buffer for the huge message and fill the buffer with that message. + UnsignedInt bigPacketCurrentOffset = 0; + bigPacketData = NEW UnsignedByte[bufferSize]; + FillBufferWithCommand(bigPacketData, ref); + + // create the wrapper command message we'll be using. + NetWrapperCommandMsg *wrapperMsg = newInstance(NetWrapperCommandMsg); + // get the amount of space needed for the wrapper message, not including the wrapped command data. + UnsignedInt wrapperSize = GetBufferSizeNeededForCommand(wrapperMsg); + UnsignedInt commandSizePerPacket = MAX_PACKET_SIZE - wrapperSize; + + UnsignedInt numChunks = bufferSize / commandSizePerPacket; + if ((bufferSize % commandSizePerPacket) > 0) { + ++numChunks; + } + UnsignedInt currentChunk = 0; + + // create the packets and the wrapper messages. + while (currentChunk < numChunks) { + NetPacket *packet = newInstance(NetPacket); + + UnsignedShort dataSizeThisPacket = commandSizePerPacket; + if ((bufferSize - bigPacketCurrentOffset) < dataSizeThisPacket) { + dataSizeThisPacket = bufferSize - bigPacketCurrentOffset; + } + + if (DoesCommandRequireACommandID(wrapperMsg->getNetCommandType())) { + wrapperMsg->setID(GenerateNextCommandID()); + } + wrapperMsg->setPlayerID(msg->getPlayerID()); + wrapperMsg->setExecutionFrame(msg->getExecutionFrame()); + + wrapperMsg->setChunkNumber(currentChunk); + wrapperMsg->setNumChunks(numChunks); + wrapperMsg->setDataOffset(bigPacketCurrentOffset); + wrapperMsg->setData(bigPacketData + bigPacketCurrentOffset, dataSizeThisPacket); + wrapperMsg->setTotalDataLength(bufferSize); + wrapperMsg->setWrappedCommandID(msg->getID()); + + bigPacketCurrentOffset += dataSizeThisPacket; + + NetCommandRef * ref = NEW_NETCOMMANDREF(wrapperMsg); + ref->setRelay(ref->getRelay()); + + if (packet->addCommand(ref) == FALSE) { + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::BeginBigCommandPacketList - failed to add a wrapper command to the packet")); // I still have a drinking problem. + } + + packetList.push_back(packet); + + deleteInstance(ref); + ref = NULL; + + ++currentChunk; + } + wrapperMsg->detach(); + wrapperMsg = NULL; + + delete[] bigPacketData; + bigPacketData = NULL; + + return packetList; +} + +UnsignedInt NetPacket::GetBufferSizeNeededForCommand(NetCommandMsg *msg) { + // This is where the fun begins... + + if (msg == NULL) { + return TRUE; // There was nothing to add, so it was successful. + } + + switch(msg->getNetCommandType()) + { + case NETCOMMANDTYPE_GAMECOMMAND: + return GetGameCommandSize(msg); + case NETCOMMANDTYPE_ACKSTAGE1: + FALLTHROUGH; + case NETCOMMANDTYPE_ACKSTAGE2: + FALLTHROUGH; + case NETCOMMANDTYPE_ACKBOTH: + return GetAckCommandSize(msg); + case NETCOMMANDTYPE_FRAMEINFO: + return GetFrameCommandSize(msg); + case NETCOMMANDTYPE_PLAYERLEAVE: + return GetPlayerLeaveCommandSize(msg); + case NETCOMMANDTYPE_RUNAHEADMETRICS: + return GetRunAheadMetricsCommandSize(msg); + case NETCOMMANDTYPE_RUNAHEAD: + return GetRunAheadCommandSize(msg); + case NETCOMMANDTYPE_DESTROYPLAYER: + return GetDestroyPlayerCommandSize(msg); + case NETCOMMANDTYPE_KEEPALIVE: + return GetKeepAliveCommandSize(msg); + case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: + return GetDisconnectKeepAliveCommandSize(msg); + case NETCOMMANDTYPE_DISCONNECTPLAYER: + return GetDisconnectPlayerCommandSize(msg); + case NETCOMMANDTYPE_PACKETROUTERQUERY: + return GetPacketRouterQueryCommandSize(msg); + case NETCOMMANDTYPE_PACKETROUTERACK: + return GetPacketRouterAckCommandSize(msg); + case NETCOMMANDTYPE_DISCONNECTCHAT: + return GetDisconnectChatCommandSize(msg); + case NETCOMMANDTYPE_DISCONNECTVOTE: + return GetDisconnectVoteCommandSize(msg); + case NETCOMMANDTYPE_CHAT: + return GetChatCommandSize(msg); + case NETCOMMANDTYPE_PROGRESS: + return GetProgressMessageSize(msg); + case NETCOMMANDTYPE_LOADCOMPLETE: + return GetLoadCompleteMessageSize(msg); + case NETCOMMANDTYPE_TIMEOUTSTART: + return GetTimeOutGameStartMessageSize(msg); + case NETCOMMANDTYPE_WRAPPER: + return GetWrapperCommandSize(msg); + case NETCOMMANDTYPE_FILE: + return GetFileCommandSize(msg); + case NETCOMMANDTYPE_FILEANNOUNCE: + return GetFileAnnounceCommandSize(msg); + case NETCOMMANDTYPE_FILEPROGRESS: + return GetFileProgressCommandSize(msg); + case NETCOMMANDTYPE_DISCONNECTFRAME: + return GetDisconnectFrameCommandSize(msg); + case NETCOMMANDTYPE_DISCONNECTSCREENOFF: + return GetDisconnectScreenOffCommandSize(msg); + case NETCOMMANDTYPE_FRAMERESENDREQUEST: + return GetFrameResendRequestCommandSize(msg); + default: + DEBUG_CRASH(("Unknown NETCOMMANDTYPE %d", msg->getNetCommandType())); + break; + } + + return 0; +} + +UnsignedInt NetPacket::GetGameCommandSize(NetCommandMsg *msg) { + NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)msg; + + UnsignedShort msglen = 0; + msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); // frame number + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // player ID + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // relay + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // command type + msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); // command ID + msglen += sizeof(UnsignedByte); // the 'D' for the data section. + + GameMessage *gmsg = cmdMsg->constructGameMessage(); + GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); + + msglen += sizeof(GameMessage::Type); + msglen += sizeof(UnsignedByte); +// Int numTypes = parser->getNumTypes(); + GameMessageParserArgumentType *arg = parser->getFirstArgumentType(); + while (arg != NULL) { + msglen += 2 * sizeof(UnsignedByte); // for the type and number of args of that type declaration. + GameMessageArgumentDataType type = arg->getType(); + + switch (type) { + + case ARGUMENTDATATYPE_INTEGER: + msglen += arg->getArgCount() * sizeof(Int); + break; + case ARGUMENTDATATYPE_REAL: + msglen += arg->getArgCount() * sizeof(Real); + break; + case ARGUMENTDATATYPE_BOOLEAN: + msglen += arg->getArgCount() * sizeof(Bool); + break; + case ARGUMENTDATATYPE_OBJECTID: + msglen += arg->getArgCount() * sizeof(ObjectID); + break; + case ARGUMENTDATATYPE_DRAWABLEID: + msglen += arg->getArgCount() * sizeof(DrawableID); + break; + case ARGUMENTDATATYPE_TEAMID: + msglen += arg->getArgCount() * sizeof(UnsignedInt); + break; + case ARGUMENTDATATYPE_LOCATION: + msglen += arg->getArgCount() * sizeof(Coord3D); + break; + case ARGUMENTDATATYPE_PIXEL: + msglen += arg->getArgCount() * sizeof(ICoord2D); + break; + case ARGUMENTDATATYPE_PIXELREGION: + msglen += arg->getArgCount() * sizeof(IRegion2D); + break; + case ARGUMENTDATATYPE_TIMESTAMP: + msglen += arg->getArgCount() * sizeof(UnsignedInt); + break; + case ARGUMENTDATATYPE_WIDECHAR: + msglen += arg->getArgCount() * sizeof(WideChar); + break; + + } + + arg = arg->getNext(); + } + + deleteInstance(parser); + parser = NULL; + + deleteInstance(gmsg); + gmsg = NULL; + + return msglen; +} + +UnsignedInt NetPacket::GetAckCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + //++msglen; + //msglen += sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; + //msglen += sizeof(UnsignedShort); + //msglen += sizeof(UnsignedByte); + + //return msglen; + + // TheSuperHackers @info AckCommandSize = 8; + return 8; +} + +UnsignedInt NetPacket::GetFrameCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; + //msglen += sizeof(UnsignedShort); + + //return msglen; + + // TheSuperHackers @info GetFrameCommandSize = 17; + return 17; +} + +UnsignedInt NetPacket::GetPlayerLeaveCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; + //msglen += sizeof(UnsignedByte); + + //return msglen; + + // TheSuperHackers @info GetPlayerLeaveCommandSize = 16; + return 16; +} + +UnsignedInt NetPacket::GetRunAheadMetricsCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(Real); + + //return msglen; + + // TheSuperHackers @info GetRunAheadMetricsCommandSize = 15; + return 16; +} + +UnsignedInt NetPacket::GetRunAheadCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; + //msglen += sizeof(UnsignedShort); + //msglen += sizeof(UnsignedByte); + + //return msglen; + + // TheSuperHackers @info GetRunAheadCommandSize = 18; + return 18; +} + +UnsignedInt NetPacket::GetDestroyPlayerCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; + //msglen += sizeof(UnsignedInt); + + //return msglen; + + // TheSuperHackers @info GetDestroyPlayerCommandSize = 19; + return 19; +} + +UnsignedInt NetPacket::GetKeepAliveCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // For the 'D' + + //return msglen; + + // TheSuperHackers @info GetKeepAliveCommandSize = 7; + return 7; +} + +UnsignedInt NetPacket::GetDisconnectKeepAliveCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // For the 'D' + + //return msglen; + + // TheSuperHackers @info GetDisconnectKeepAliveCommandSize = 7; + return 7; +} + +UnsignedInt NetPacket::GetDisconnectPlayerCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; // the 'D' + //msglen += sizeof(UnsignedByte); // slot number + //msglen += sizeof(UnsignedInt); // disconnect frame + + //return msglen; + + // TheSuperHackers @info GetDisconnectPlayerCommandSize = 15; + return 15; +} + +UnsignedInt NetPacket::GetPacketRouterQueryCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // the 'D' + + //return msglen; + + // TheSuperHackers @info GetPacketRouterQueryCommandSize = 7; + return 7; +} + +UnsignedInt NetPacket::GetPacketRouterAckCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // the 'D' + + //return msglen; + + // TheSuperHackers @info GetPacketRouterAckCommandSize = 7; + return 7; +} + +UnsignedInt NetPacket::GetDisconnectChatCommandSize(NetCommandMsg *msg) { + Int msglen = 0; + NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg); + + ++msglen; + msglen += sizeof(UnsignedByte); + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + ++msglen; + msglen += sizeof(UnsignedByte); + + ++msglen; // the 'D' + msglen += sizeof(UnsignedByte); // string msglength + UnsignedByte textmsglen = cmdMsg->getText().getLength(); + msglen += textmsglen * sizeof(UnsignedShort); + + return msglen; + + // TheSuperHackers @info GetDisconnectChatCommandSize = variable; +} + +UnsignedInt NetPacket::GetDisconnectVoteCommandSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + //++msglen; // the 'D' + //msglen += sizeof(UnsignedByte); // slot number + //msglen += sizeof(UnsignedInt); // vote frame. + + //return msglen; + + // TheSuperHackers @info GetDisconnectVoteCommandSize = 15; + return 15; +} + +UnsignedInt NetPacket::GetChatCommandSize(NetCommandMsg *msg) { + Int msglen = 0; + NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg); + + ++msglen; + msglen += sizeof(UnsignedByte); + msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + ++msglen; + msglen += sizeof(UnsignedByte); + msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + + ++msglen; // the 'D' + msglen += sizeof(UnsignedByte); // string msglength + UnsignedByte textmsglen = cmdMsg->getText().getLength(); + msglen += textmsglen * sizeof(UnsignedShort); + msglen += sizeof(Int); // playerMask + + return msglen; + + // TheSuperHackers @info GetChatCommandSize = variable; +} + +UnsignedInt NetPacket::GetProgressMessageSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // For the 'D' + //++msglen; // percentage + + //return msglen; + + // TheSuperHackers @info GetProgressMessageSize = 8; + return 8; +} + +UnsignedInt NetPacket::GetLoadCompleteMessageSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // For the 'D' + + //return msglen; + + // TheSuperHackers @info GetLoadCompleteMessageSize = 7; + return 7; +} + +UnsignedInt NetPacket::GetTimeOutGameStartMessageSize(NetCommandMsg *msg) { + //Int msglen = 0; + + //++msglen; + //msglen += sizeof(UnsignedByte); + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + //++msglen; + //msglen += sizeof(UnsignedByte); + + //++msglen; // For the 'D' + + //return msglen; + + // TheSuperHackers @info GetTimeOutGameStartMessageSize = 7; + return 7; +} + +// type, player, ID, relay, Data +UnsignedInt NetPacket::GetWrapperCommandSize(NetCommandMsg *msg) { + //UnsignedInt msglen = 0; + + //++msglen; // 'T' + //msglen += sizeof(UnsignedByte); // command type + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + //++msglen; // 'D' + + //msglen += sizeof(UnsignedShort); // m_wrappedCommandID + //msglen += sizeof(UnsignedInt); // m_chunkNumber + //msglen += sizeof(UnsignedInt); // m_numChunks + //msglen += sizeof(UnsignedInt); // m_totalDataLength + //msglen += sizeof(UnsignedInt); // m_dataLength + //msglen += sizeof(UnsignedInt); // m_dataOffset + + //return msglen; + + // TheSuperHackers @info GetWrapperCommandSize = 32; + return 32; +} + +UnsignedInt NetPacket::GetFileCommandSize(NetCommandMsg *msg) { + NetFileCommandMsg *filemsg = (NetFileCommandMsg *)msg; + UnsignedInt msglen = 0; + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + + ++msglen; // 'D' + + msglen += filemsg->getPortableFilename().getLength() + 1; // PORTABLE filename and the terminating 0 + msglen += sizeof(UnsignedInt); // file data length + msglen += filemsg->getFileLength(); // the file data + + return msglen; + + // TheSuperHackers @info GetFileCommandSize = variable; +} + +UnsignedInt NetPacket::GetFileAnnounceCommandSize(NetCommandMsg *msg) { + NetFileAnnounceCommandMsg *filemsg = (NetFileAnnounceCommandMsg *)msg; + UnsignedInt msglen = 0; + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + + ++msglen; // 'D' + + msglen += filemsg->getPortableFilename().getLength() + 1; // PORTABLE filename and the terminating 0 + msglen += sizeof(UnsignedShort); // m_fileID + msglen += sizeof(UnsignedByte); // m_playerMask + + return msglen; + + // TheSuperHackers @info GetFileAnnounceCommandSize = variable; +} + +UnsignedInt NetPacket::GetFileProgressCommandSize(NetCommandMsg *msg) { + //UnsignedInt msglen = 0; + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + + //++msglen; // 'D' + + //msglen += sizeof(UnsignedShort); // m_fileID + //msglen += sizeof(Int); // m_progress + + //return msglen; + + // TheSuperHackers @info GetFileProgressCommandSize = 16; + return 16; +} + +UnsignedInt NetPacket::GetDisconnectFrameCommandSize(NetCommandMsg *msg) { + //UnsignedInt msglen = 0; + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + + //++msglen; // 'D' + //msglen += sizeof(UnsignedInt); // disconnect frame + + //return msglen; + + // TheSuperHackers @info GetDisconnectFrameCommandSize = 14; + return 14; +} + +UnsignedInt NetPacket::GetDisconnectScreenOffCommandSize(NetCommandMsg *msg) { + //UnsignedInt msglen = 0; + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + + //++msglen; // 'D' + //msglen += sizeof(UnsignedInt); // new frame + + //return msglen; + + // TheSuperHackers @info GetDisconnectScreenOffCommandSize = 14; + return 14; +} + +UnsignedInt NetPacket::GetFrameResendRequestCommandSize(NetCommandMsg *msg) { + //UnsignedInt msglen = 0; + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'T' and command type + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'P' and player ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedShort); // 'C' and command ID + //msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); // 'R' and relay + + //++msglen; // 'D' + //msglen += sizeof(UnsignedInt); // frame to resend + + //return msglen; + + // TheSuperHackers @info GetFrameResendRequestCommandSize = 14; + return 14; +} + +// this function assumes that buffer is already the correct size. +void NetPacket::FillBufferWithCommand(UnsignedByte *buffer, NetCommandRef *ref) { + NetCommandMsg *msg = ref->getCommand(); + + switch(msg->getNetCommandType()) + { + case NETCOMMANDTYPE_GAMECOMMAND: + FillBufferWithGameCommand(buffer, ref); + break; + case NETCOMMANDTYPE_ACKSTAGE1: + FALLTHROUGH; + case NETCOMMANDTYPE_ACKSTAGE2: + FALLTHROUGH; + case NETCOMMANDTYPE_ACKBOTH: + FillBufferWithAckCommand(buffer, ref); + break; + case NETCOMMANDTYPE_FRAMEINFO: + FillBufferWithFrameCommand(buffer, ref); + break; + case NETCOMMANDTYPE_PLAYERLEAVE: + FillBufferWithPlayerLeaveCommand(buffer, ref); + break; + case NETCOMMANDTYPE_RUNAHEADMETRICS: + FillBufferWithRunAheadMetricsCommand(buffer, ref); + break; + case NETCOMMANDTYPE_RUNAHEAD: + FillBufferWithRunAheadCommand(buffer, ref); + break; + case NETCOMMANDTYPE_DESTROYPLAYER: + FillBufferWithDestroyPlayerCommand(buffer, ref); + break; + case NETCOMMANDTYPE_KEEPALIVE: + FillBufferWithKeepAliveCommand(buffer, ref); + break; + case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: + FillBufferWithDisconnectKeepAliveCommand(buffer, ref); + break; + case NETCOMMANDTYPE_DISCONNECTPLAYER: + FillBufferWithDisconnectPlayerCommand(buffer, ref); + break; + case NETCOMMANDTYPE_PACKETROUTERQUERY: + FillBufferWithPacketRouterQueryCommand(buffer, ref); + break; + case NETCOMMANDTYPE_PACKETROUTERACK: + FillBufferWithPacketRouterAckCommand(buffer, ref); + break; + case NETCOMMANDTYPE_DISCONNECTCHAT: + FillBufferWithDisconnectChatCommand(buffer, ref); + break; + case NETCOMMANDTYPE_DISCONNECTVOTE: + FillBufferWithDisconnectVoteCommand(buffer, ref); + break; + case NETCOMMANDTYPE_CHAT: + FillBufferWithChatCommand(buffer, ref); + break; + case NETCOMMANDTYPE_PROGRESS: + FillBufferWithProgressMessage(buffer, ref); + break; + case NETCOMMANDTYPE_LOADCOMPLETE: + FillBufferWithLoadCompleteMessage(buffer, ref); + break; + case NETCOMMANDTYPE_TIMEOUTSTART: + FillBufferWithTimeOutGameStartMessage(buffer, ref); + break; + case NETCOMMANDTYPE_FILE: + FillBufferWithFileMessage(buffer, ref); + break; + case NETCOMMANDTYPE_FILEANNOUNCE: + FillBufferWithFileAnnounceMessage(buffer, ref); + break; + case NETCOMMANDTYPE_FILEPROGRESS: + FillBufferWithFileProgressMessage(buffer, ref); + break; + case NETCOMMANDTYPE_DISCONNECTFRAME: + FillBufferWithDisconnectFrameMessage(buffer, ref); + break; + case NETCOMMANDTYPE_DISCONNECTSCREENOFF: + FillBufferWithDisconnectScreenOffMessage(buffer, ref); + break; + case NETCOMMANDTYPE_FRAMERESENDREQUEST: + FillBufferWithFrameResendRequestMessage(buffer, ref); + break; + default: + DEBUG_CRASH(("Unknown NETCOMMANDTYPE %d", msg->getNetCommandType())); + break; + } +} + +void NetPacket::FillBufferWithGameCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + // get the game message from the NetCommandMsg + GameMessage *gmsg = cmdMsg->constructGameMessage(); + + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::FillBufferWithGameCommand for command ID %d", cmdMsg->getID())); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // If necessary, put the execution frame into the packet. + buffer[offset] = 'F'; + ++offset; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + + // If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + // If necessary, put the playerID into the packet. + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + + buffer[offset] = 'D'; + ++offset; + + // Now copy the GameMessage type into the packet. + GameMessage::Type newType = gmsg->getType(); + memcpy(buffer + offset, &newType, sizeof(GameMessage::Type)); + offset += sizeof(GameMessage::Type); + + + GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); + UnsignedByte numTypes = parser->getNumTypes(); + memcpy(buffer + offset, &numTypes, sizeof(numTypes)); + offset += sizeof(numTypes); + + GameMessageParserArgumentType *argType = parser->getFirstArgumentType(); + while (argType != NULL) { + UnsignedByte type = (UnsignedByte)(argType->getType()); + memcpy(buffer + offset, &type, sizeof(type)); + offset += sizeof(type); + + UnsignedByte argTypeCount = argType->getArgCount(); + memcpy(buffer + offset, &argTypeCount, sizeof(argTypeCount)); + offset += sizeof(argTypeCount); + + argType = argType->getNext(); + } + + Int numArgs = gmsg->getArgumentCount(); + for (Int i = 0; i < numArgs; ++i) { + GameMessageArgumentDataType type = gmsg->getArgumentDataType(i); + GameMessageArgumentType arg = *(gmsg->getArgument(i)); + + switch (type) { + + case ARGUMENTDATATYPE_INTEGER: + memcpy(buffer + offset, &(arg.integer), sizeof(arg.integer)); + offset += sizeof(arg.integer); + break; + case ARGUMENTDATATYPE_REAL: + memcpy(buffer + offset, &(arg.real), sizeof(arg.real)); + offset += sizeof(arg.real); + break; + case ARGUMENTDATATYPE_BOOLEAN: + memcpy(buffer + offset, &(arg.boolean), sizeof(arg.boolean)); + offset += sizeof(arg.boolean); + break; + case ARGUMENTDATATYPE_OBJECTID: + memcpy(buffer + offset, &(arg.objectID), sizeof(arg.objectID)); + offset += sizeof(arg.objectID); + break; + case ARGUMENTDATATYPE_DRAWABLEID: + memcpy(buffer + offset, &(arg.drawableID), sizeof(arg.drawableID)); + offset += sizeof(arg.drawableID); + break; + case ARGUMENTDATATYPE_TEAMID: + memcpy(buffer + offset, &(arg.teamID), sizeof(arg.teamID)); + offset += sizeof(arg.teamID); + break; + case ARGUMENTDATATYPE_LOCATION: + memcpy(buffer + offset, &(arg.location), sizeof(arg.location)); + offset += sizeof(arg.location); + break; + case ARGUMENTDATATYPE_PIXEL: + memcpy(buffer + offset, &(arg.pixel), sizeof(arg.pixel)); + offset += sizeof(arg.pixel); + break; + case ARGUMENTDATATYPE_PIXELREGION: + memcpy(buffer + offset, &(arg.pixelRegion), sizeof(arg.pixelRegion)); + offset += sizeof(arg.pixelRegion); + break; + case ARGUMENTDATATYPE_TIMESTAMP: + memcpy(buffer + offset, &(arg.timestamp), sizeof(arg.timestamp)); + offset += sizeof(arg.timestamp); + break; + case ARGUMENTDATATYPE_WIDECHAR: + memcpy(buffer + offset, &(arg.wChar), sizeof(arg.wChar)); + offset += sizeof(arg.wChar); + break; + } + + } + + deleteInstance(parser); + parser = NULL; + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addGameMessage - added game message, frame %d, player %d, command ID %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); + + if (gmsg) + deleteInstance(gmsg); + gmsg = NULL; +} + +void NetPacket::FillBufferWithAckCommand(UnsignedByte *buffer, NetCommandRef *msg) { +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::FillBufferWithAckCommand - adding ack for command %d for player %d", cmdMsg->getCommandID(), msg->getCommand()->getPlayerID())); + + NetCommandMsg *cmdMsg = msg->getCommand(); + UnsignedShort offset = 0; + + UnsignedShort commandID = 0; + UnsignedByte originalPlayerID = 0; + + NetCommandType type = cmdMsg->getNetCommandType(); + + switch (type) { + + case NETCOMMANDTYPE_ACKBOTH: { + NetAckBothCommandMsg* ackbothmsg = (NetAckBothCommandMsg*)msg; + commandID = ackbothmsg->getCommandID(); + originalPlayerID = ackbothmsg->getOriginalPlayerID(); + break; + } + + case NETCOMMANDTYPE_ACKSTAGE1: { + NetAckStage1CommandMsg* ackstageonemsg = (NetAckStage1CommandMsg*)msg; + commandID = ackstageonemsg->getCommandID(); + originalPlayerID = ackstageonemsg->getOriginalPlayerID(); + break; + } + + case NETCOMMANDTYPE_ACKSTAGE2: { + NetAckStage2CommandMsg* ackstagetwomsg = (NetAckStage2CommandMsg*)msg; + commandID = ackstagetwomsg->getCommandID(); + originalPlayerID = ackstagetwomsg->getOriginalPlayerID(); + break; + } + + } + + buffer[offset] = 'T'; + ++offset; + buffer[offset] = type; + offset += sizeof(UnsignedByte); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // Put in the command id of the command we are acking. + buffer[offset] = 'D'; + ++offset; + memcpy(buffer + offset, &commandID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + memcpy(buffer + offset, &originalPlayerID, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + // DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added ACK, original player %d, command id %d", origPlayerID, cmdID)); +} + +void NetPacket::FillBufferWithFrameCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetFrameCommandMsg *cmdMsg = (NetFrameCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + // DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addFrameCommand - adding frame command for frame %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getCommandCount(), cmdMsg->getID())); + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the execution frame into the packet. + buffer[offset] = 'F'; + ++offset; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnsignedShort cmdCount = cmdMsg->getCommandCount(); + memcpy(buffer + offset, &cmdCount, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + + // frameinfodebug +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added frame %d, player %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getPlayerID(), cmdMsg->getCommandCount(), cmdMsg->getID())); +} + +void NetPacket::FillBufferWithPlayerLeaveCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetPlayerLeaveCommandMsg *cmdMsg = (NetPlayerLeaveCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPlayerLeaveCommand - adding player leave command for player %d", cmdMsg->getLeavingPlayerID())); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// If necessary, put the execution frame into the packet. + buffer[offset] = 'F'; + ++offset; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnsignedByte leavingPlayerID = cmdMsg->getLeavingPlayerID(); + memcpy(buffer + offset, &leavingPlayerID, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); +} + +void NetPacket::FillBufferWithRunAheadMetricsCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetRunAheadMetricsCommandMsg *cmdMsg = (NetRunAheadMetricsCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadMetricsCommand - adding run ahead metrics for player %d, fps = %d, latency = %f", cmdMsg->getPlayerID(), cmdMsg->getAverageFps(), cmdMsg->getAverageLatency())); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + // write the average latency + Real averageLatency = cmdMsg->getAverageLatency(); + memcpy(buffer + offset, &averageLatency, sizeof(averageLatency)); + offset += sizeof(averageLatency); + // write the average fps + UnsignedShort averageFps = (UnsignedShort)(cmdMsg->getAverageFps()); + memcpy(buffer + offset, &averageFps, sizeof(averageFps)); + offset += sizeof(averageFps); +} + +void NetPacket::FillBufferWithRunAheadCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetRunAheadCommandMsg *cmdMsg = (NetRunAheadCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::FillBufferWithRunAheadCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + // If necessary, put the execution frame into the packet. + buffer[offset] = 'F'; + ++offset; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnsignedShort newRunAhead = cmdMsg->getRunAhead(); + memcpy(buffer + offset, &newRunAhead, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + + UnsignedByte newFrameRate = cmdMsg->getFrameRate(); + memcpy(buffer + offset, &newFrameRate, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added run ahead command, frame %d, player id %d command id %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); +} + +void NetPacket::FillBufferWithDestroyPlayerCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetDestroyPlayerCommandMsg *cmdMsg = (NetDestroyPlayerCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadCommand - adding run ahead command")); + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// If necessary, put the execution frame into the packet. + buffer[offset] = 'F'; + ++offset; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnsignedInt newVal = cmdMsg->getPlayerIndex(); + memcpy(buffer + offset, &newVal, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); +} + +void NetPacket::FillBufferWithKeepAliveCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetKeepAliveCommandMsg *cmdMsg = (NetKeepAliveCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + buffer[offset] = 'D'; + ++offset; +} + +void NetPacket::FillBufferWithDisconnectKeepAliveCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetDisconnectKeepAliveCommandMsg *cmdMsg = (NetDisconnectKeepAliveCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + + // Put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // Put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + // Put the player ID into the packet. + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + buffer[offset] = 'D'; + ++offset; +} + +void NetPacket::FillBufferWithDisconnectPlayerCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetDisconnectPlayerCommandMsg *cmdMsg = (NetDisconnectPlayerCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + + // DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnsignedByte slot = cmdMsg->getDisconnectSlot(); + memcpy(buffer + offset, &slot, sizeof(slot)); + offset += sizeof(slot); + + UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); + memcpy(buffer + offset, &disconnectFrame, sizeof(disconnectFrame)); + offset += sizeof(disconnectFrame); +} + +void NetPacket::FillBufferWithPacketRouterQueryCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetPacketRouterQueryCommandMsg *cmdMsg = (NetPacketRouterQueryCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterQueryCommand - adding packet router query command")); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + buffer[offset] = 'D'; + ++offset; +} + +void NetPacket::FillBufferWithPacketRouterAckCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetPacketRouterAckCommandMsg *cmdMsg = (NetPacketRouterAckCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterAckCommand - adding packet router query command")); + + // If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + buffer[offset] = 'D'; + ++offset; +} + +void NetPacket::FillBufferWithDisconnectChatCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + buffer[offset] = 'D'; + ++offset; + UnicodeString unitext = cmdMsg->getText(); + UnsignedByte length = unitext.getLength(); + memcpy(buffer + offset, &length, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + memcpy(buffer + offset, unitext.str(), length * sizeof(UnsignedShort)); + offset += length * sizeof(UnsignedShort); +} + +void NetPacket::FillBufferWithDisconnectVoteCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetDisconnectVoteCommandMsg *cmdMsg = (NetDisconnectVoteCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - adding run ahead command")); + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnsignedByte slot = cmdMsg->getSlot(); + memcpy(buffer + offset, &slot, sizeof(slot)); + offset += sizeof(slot); + + UnsignedInt voteFrame = cmdMsg->getVoteFrame(); + memcpy(buffer + offset, &voteFrame, sizeof(voteFrame)); + offset += sizeof(voteFrame); +} + +void NetPacket::FillBufferWithChatCommand(UnsignedByte *buffer, NetCommandRef *msg) { + NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the execution frame into the packet. + buffer[offset] = 'F'; + ++offset; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(buffer+offset, &newframe, sizeof(UnsignedInt)); + offset += sizeof(UnsignedInt); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + buffer[offset] = 'D'; + ++offset; + UnicodeString unitext = cmdMsg->getText(); + UnsignedByte length = unitext.getLength(); + Int playerMask = cmdMsg->getPlayerMask(); + memcpy(buffer + offset, &length, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + memcpy(buffer + offset, unitext.str(), length * sizeof(UnsignedShort)); + offset += length * sizeof(UnsignedShort); + + memcpy(buffer + offset, &playerMask, sizeof(Int)); + offset += sizeof(Int); +} + +void NetPacket::FillBufferWithProgressMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetProgressCommandMsg *cmdMsg = (NetProgressCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + +// Put the player ID into the packet. + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + buffer[offset] = 'D'; + ++offset; + + buffer[offset] = cmdMsg->getPercentage(); + ++offset; +} + +void NetPacket::FillBufferWithLoadCompleteMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + + buffer[offset] = 'D'; + ++offset; +} + +void NetPacket::FillBufferWithTimeOutGameStartMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); + UnsignedShort offset = 0; + +// If necessary, put the NetCommandType into the packet. + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + +// If necessary, put the relay into the packet. + buffer[offset] = 'R'; + ++offset; + UnsignedByte newRelay = msg->getRelay(); + memcpy(buffer+offset, &newRelay, sizeof(UnsignedByte)); + offset += sizeof(UnsignedByte); + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + +// If necessary, specify the command ID of this command. + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(UnsignedShort)); + offset += sizeof(UnsignedShort); + + buffer[offset] = 'D'; + ++offset; +} + +void NetPacket::FillBufferWithFileMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetFileCommandMsg *cmdMsg = (NetFileCommandMsg *)(msg->getCommand()); + UnsignedInt offset = 0; + + // command type + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // relay + buffer[offset] = 'R'; + ++offset; + buffer[offset] = msg->getRelay(); + offset += sizeof(UnsignedByte); + + // player ID + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // command ID + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(newID)); + offset += sizeof(newID); + + // data + buffer[offset] = 'D'; + ++offset; + + AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE + for (Int i = 0; i < filename.getLength(); ++i) { + buffer[offset] = filename.getCharAt(i); + ++offset; + } + buffer[offset] = 0; + ++offset; + + UnsignedInt newInt = cmdMsg->getFileLength(); + memcpy(buffer + offset, &newInt, sizeof(newInt)); + offset += sizeof(newInt); + + memcpy(buffer + offset, cmdMsg->getFileData(), cmdMsg->getFileLength()); + offset += cmdMsg->getFileLength(); +} + +void NetPacket::FillBufferWithFileAnnounceMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetFileAnnounceCommandMsg *cmdMsg = (NetFileAnnounceCommandMsg *)(msg->getCommand()); + UnsignedInt offset = 0; + + // command type + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // relay + buffer[offset] = 'R'; + ++offset; + buffer[offset] = msg->getRelay(); + offset += sizeof(UnsignedByte); + + // player ID + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // command ID + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(newID)); + offset += sizeof(newID); + + // data + buffer[offset] = 'D'; + ++offset; + + AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE + for (Int i = 0; i < filename.getLength(); ++i) { + buffer[offset] = filename.getCharAt(i); + ++offset; + } + buffer[offset] = 0; + ++offset; + + UnsignedShort fileID = cmdMsg->getFileID(); + memcpy(buffer + offset, &fileID, sizeof(fileID)); + offset += sizeof(fileID); + + UnsignedByte playerMask = cmdMsg->getPlayerMask(); + memcpy(buffer + offset, &playerMask, sizeof(playerMask)); + offset += sizeof(playerMask); +} + +void NetPacket::FillBufferWithFileProgressMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetFileProgressCommandMsg *cmdMsg = (NetFileProgressCommandMsg *)(msg->getCommand()); + UnsignedInt offset = 0; + + // command type + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // relay + buffer[offset] = 'R'; + ++offset; + buffer[offset] = msg->getRelay(); + offset += sizeof(UnsignedByte); + + // player ID + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // command ID + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(newID)); + offset += sizeof(newID); + + // data + buffer[offset] = 'D'; + ++offset; + + UnsignedShort fileID = cmdMsg->getFileID(); + memcpy(buffer + offset, &fileID, sizeof(fileID)); + offset += sizeof(fileID); + + Int progress = cmdMsg->getProgress(); + memcpy(buffer + offset, &progress, sizeof(progress)); + offset += sizeof(progress); +} + +void NetPacket::FillBufferWithDisconnectFrameMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetDisconnectFrameCommandMsg *cmdMsg = (NetDisconnectFrameCommandMsg *)(msg->getCommand()); + UnsignedInt offset = 0; + + // command type + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // relay + buffer[offset] = 'R'; + ++offset; + buffer[offset] = msg->getRelay(); + offset += sizeof(UnsignedByte); + + // player ID + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // command ID + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(newID)); + offset += sizeof(newID); + + // data + buffer[offset] = 'D'; + ++offset; + + UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); + memcpy(buffer + offset, &disconnectFrame, sizeof(disconnectFrame)); + offset += sizeof(disconnectFrame); +} + +void NetPacket::FillBufferWithDisconnectScreenOffMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetDisconnectScreenOffCommandMsg *cmdMsg = (NetDisconnectScreenOffCommandMsg *)(msg->getCommand()); + UnsignedInt offset = 0; + + // command type + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // relay + buffer[offset] = 'R'; + ++offset; + buffer[offset] = msg->getRelay(); + offset += sizeof(UnsignedByte); + + // player ID + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // command ID + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(newID)); + offset += sizeof(newID); + + // data + buffer[offset] = 'D'; + ++offset; + + UnsignedInt newFrame = cmdMsg->getNewFrame(); + memcpy(buffer + offset, &newFrame, sizeof(newFrame)); + offset += sizeof(newFrame); +} + +void NetPacket::FillBufferWithFrameResendRequestMessage(UnsignedByte *buffer, NetCommandRef *msg) { + NetFrameResendRequestCommandMsg *cmdMsg = (NetFrameResendRequestCommandMsg *)(msg->getCommand()); + UnsignedInt offset = 0; + + // command type + buffer[offset] = 'T'; + ++offset; + buffer[offset] = cmdMsg->getNetCommandType(); + offset += sizeof(UnsignedByte); + + // relay + buffer[offset] = 'R'; + ++offset; + buffer[offset] = msg->getRelay(); + offset += sizeof(UnsignedByte); + + // player ID + buffer[offset] = 'P'; + ++offset; + buffer[offset] = cmdMsg->getPlayerID(); + offset += sizeof(UnsignedByte); + + // command ID + buffer[offset] = 'C'; + ++offset; + UnsignedShort newID = cmdMsg->getID(); + memcpy(buffer + offset, &newID, sizeof(newID)); + offset += sizeof(newID); + + // data + buffer[offset] = 'D'; + ++offset; + + UnsignedInt frameToResend = cmdMsg->getFrameToResend(); + memcpy(buffer + offset, &frameToResend, sizeof(frameToResend)); + offset += sizeof(frameToResend); +} + + +/** + * Constructor + */ +NetPacket::NetPacket() { + init(); +} + +/** + * Constructor given raw transport data. + */ +NetPacket::NetPacket(TransportMessage *msg) { + init(); + m_packetLen = msg->length; + memcpy(m_packet, msg->data, MAX_PACKET_SIZE); + m_numCommands = -1; + m_addr = msg->addr; + m_port = msg->port; +} + +/** + * Destructor + */ +NetPacket::~NetPacket() { + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } +} + +/** + * Initialize all the member variable values. + */ +void NetPacket::init() { + m_addr = 0; + m_port = 0; + m_numCommands = 0; + m_packetLen = 0; + m_packet[0] = 0; + + m_lastPlayerID = 0; + m_lastFrame = 0; + m_lastCommandID = 0; + m_lastCommandType = 0; + m_lastRelay = 0; + + m_lastCommand = NULL; +} + +void NetPacket::reset() { + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + init(); +} + +/** + * Set the address to which this packet is to be sent. + */ +void NetPacket::setAddress(Int addr, Int port) { + m_addr = addr; + m_port = port; +} + +/** + * Adds this command to the packet. Returns false if there wasn't enough room + * in the packet for this message, true otherwise. + */ +Bool NetPacket::addCommand(NetCommandRef *msg) { + // This is where the fun begins... + + NetCommandMsg *cmdMsg = msg->getCommand(); + + if (msg == NULL) { + return TRUE; // There was nothing to add, so it was successful. + } + + switch(cmdMsg->getNetCommandType()) + { + case NETCOMMANDTYPE_GAMECOMMAND: + return addGameCommand(msg); + case NETCOMMANDTYPE_ACKSTAGE1: + return addAckStage1Command(msg); + case NETCOMMANDTYPE_ACKSTAGE2: + return addAckStage2Command(msg); + case NETCOMMANDTYPE_ACKBOTH: + return addAckBothCommand(msg); + case NETCOMMANDTYPE_FRAMEINFO: + return addFrameCommand(msg); + case NETCOMMANDTYPE_PLAYERLEAVE: + return addPlayerLeaveCommand(msg); + case NETCOMMANDTYPE_RUNAHEADMETRICS: + return addRunAheadMetricsCommand(msg); + case NETCOMMANDTYPE_RUNAHEAD: + return addRunAheadCommand(msg); + case NETCOMMANDTYPE_DESTROYPLAYER: + return addDestroyPlayerCommand(msg); + case NETCOMMANDTYPE_KEEPALIVE: + return addKeepAliveCommand(msg); + case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: + return addDisconnectKeepAliveCommand(msg); + case NETCOMMANDTYPE_DISCONNECTPLAYER: + return addDisconnectPlayerCommand(msg); + case NETCOMMANDTYPE_PACKETROUTERQUERY: + return addPacketRouterQueryCommand(msg); + case NETCOMMANDTYPE_PACKETROUTERACK: + return addPacketRouterAckCommand(msg); + case NETCOMMANDTYPE_DISCONNECTCHAT: + return addDisconnectChatCommand(msg); + case NETCOMMANDTYPE_DISCONNECTVOTE: + return addDisconnectVoteCommand(msg); + case NETCOMMANDTYPE_CHAT: + return addChatCommand(msg); + case NETCOMMANDTYPE_PROGRESS: + return addProgressMessage(msg); + case NETCOMMANDTYPE_LOADCOMPLETE: + return addLoadCompleteMessage(msg); + case NETCOMMANDTYPE_TIMEOUTSTART: + return addTimeOutGameStartMessage(msg); + case NETCOMMANDTYPE_WRAPPER: + return addWrapperCommand(msg); + case NETCOMMANDTYPE_FILE: + return addFileCommand(msg); + case NETCOMMANDTYPE_FILEANNOUNCE: + return addFileAnnounceCommand(msg); + case NETCOMMANDTYPE_FILEPROGRESS: + return addFileProgressCommand(msg); + case NETCOMMANDTYPE_DISCONNECTFRAME: + return addDisconnectFrameCommand(msg); + case NETCOMMANDTYPE_DISCONNECTSCREENOFF: + return addDisconnectScreenOffCommand(msg); + case NETCOMMANDTYPE_FRAMERESENDREQUEST: + return addFrameResendRequestCommand(msg); + default: + DEBUG_CRASH(("Unknown NETCOMMANDTYPE %d", cmdMsg->getNetCommandType())); + break; + } + + return TRUE; +} + +/* +T = Net command type +F = Execution frame +P = Player ID +C = Command ID +R = Relay +D = Command Data +Z = Repeat last command +*/ +Bool NetPacket::addFrameResendRequestCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForFrameResendRequestMessage(msg)) { + NetFrameResendRequestCommandMsg *cmdMsg = (NetFrameResendRequestCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + UnsignedInt frameToResend = cmdMsg->getFrameToResend(); + memcpy(m_packet + m_packetLen, &frameToResend, sizeof(frameToResend)); + m_packetLen += sizeof(frameToResend); + + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addFrameResendRequest - added frame resend request command from player %d for frame %d, command id = %d", m_lastPlayerID, frameToResend, m_lastCommandID)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForFrameResendRequestMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetFrameResendRequestCommandMsg *cmdMsg = (NetFrameResendRequestCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedInt); // for the frame to be resent + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addDisconnectScreenOffCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForDisconnectScreenOffMessage(msg)) { + NetDisconnectScreenOffCommandMsg *cmdMsg = (NetDisconnectScreenOffCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + UnsignedInt newFrame = cmdMsg->getNewFrame(); + memcpy(m_packet + m_packetLen, &newFrame, sizeof(newFrame)); + m_packetLen += sizeof(newFrame); + + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectScreenOff - added disconnect screen off command from player %d for frame %d, command id = %d", m_lastPlayerID, newFrame, m_lastCommandID)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForDisconnectScreenOffMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetDisconnectScreenOffCommandMsg *cmdMsg = (NetDisconnectScreenOffCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedInt); // for the disconnect frame + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addDisconnectFrameCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForDisconnectFrameMessage(msg)) { + NetDisconnectFrameCommandMsg *cmdMsg = (NetDisconnectFrameCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); + memcpy(m_packet + m_packetLen, &disconnectFrame, sizeof(disconnectFrame)); + m_packetLen += sizeof(disconnectFrame); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectFrame - added disconnect frame command from player %d for frame %d, command id = %d", m_lastPlayerID, disconnectFrame, m_lastCommandID)); + + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForDisconnectFrameMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetDisconnectFrameCommandMsg *cmdMsg = (NetDisconnectFrameCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedInt); // for the disconnect frame + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addFileCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForFileMessage(msg)) { + NetFileCommandMsg *cmdMsg = (NetFileCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE + strcpy((char *)(m_packet + m_packetLen), filename.str()); + m_packetLen += filename.getLength() + 1; + + UnsignedInt fileLength = cmdMsg->getFileLength(); + memcpy(m_packet + m_packetLen, &fileLength, sizeof(fileLength)); + m_packetLen += sizeof(fileLength); + + memcpy(m_packet + m_packetLen, cmdMsg->getFileData(), fileLength); + m_packetLen += fileLength; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForFileMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetFileCommandMsg *cmdMsg = (NetFileCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedByte) + sizeof(UnsignedShort); + } + + ++len; // 'D' + len += cmdMsg->getPortableFilename().getLength() + 1; // PORTABLE filename + the terminating 0 + len += sizeof(UnsignedInt); // filedata length + len += cmdMsg->getFileLength(); + + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + + return TRUE; +} + +Bool NetPacket::addFileAnnounceCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForFileAnnounceMessage(msg)) { + NetFileAnnounceCommandMsg *cmdMsg = (NetFileAnnounceCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + AsciiString filename = cmdMsg->getPortableFilename(); // PORTABLE + strcpy((char *)(m_packet + m_packetLen), filename.str()); + m_packetLen += filename.getLength() + 1; + + UnsignedShort fileID = cmdMsg->getFileID(); + memcpy(m_packet + m_packetLen, &fileID, sizeof(fileID)); + m_packetLen += sizeof(fileID); + + UnsignedByte playerMask = cmdMsg->getPlayerMask(); + memcpy(m_packet + m_packetLen, &playerMask, sizeof(playerMask)); + m_packetLen += sizeof(playerMask); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Adding file announce message for fileID %d, ID %d to packet", + cmdMsg->getFileID(), cmdMsg->getID())); + return TRUE; + } + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("No room to add file announce message to packet")); + return FALSE; +} + +Bool NetPacket::isRoomForFileAnnounceMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetFileAnnounceCommandMsg *cmdMsg = (NetFileAnnounceCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedByte) + sizeof(UnsignedShort); + } + + ++len; // 'D' + len += cmdMsg->getPortableFilename().getLength() + 1; // PORTABLE filename + the terminating 0 + len += sizeof(UnsignedShort); // m_fileID + len += sizeof(UnsignedByte); // m_playerMask + + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + + return TRUE; +} + +Bool NetPacket::addFileProgressCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForFileProgressMessage(msg)) { + NetFileProgressCommandMsg *cmdMsg = (NetFileProgressCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + UnsignedShort fileID = cmdMsg->getFileID(); + memcpy(m_packet + m_packetLen, &fileID, sizeof(fileID)); + m_packetLen += sizeof(fileID); + + Int progress = cmdMsg->getProgress(); + memcpy(m_packet + m_packetLen, &progress, sizeof(progress)); + m_packetLen += sizeof(progress); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForFileProgressMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetFileProgressCommandMsg *cmdMsg = (NetFileProgressCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedByte) + sizeof(UnsignedShort); + } + + ++len; // 'D' + len += sizeof(UnsignedShort); // m_fileID + len += sizeof(Int); // m_progress + + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + + return TRUE; +} + +Bool NetPacket::addWrapperCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForWrapperMessage(msg)) { + NetWrapperCommandMsg *cmdMsg = (NetWrapperCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet + m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary put the player ID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + // wrapped command ID + UnsignedShort wrappedCommandID = cmdMsg->getWrappedCommandID(); + memcpy(m_packet + m_packetLen, &wrappedCommandID, sizeof(wrappedCommandID)); + m_packetLen += sizeof(wrappedCommandID); + + // chunk number +// m_packet[m_packetLen] = cmdMsg->getChunkNumber(); +// ++m_packetLen; + UnsignedInt chunkNumber = cmdMsg->getChunkNumber(); + memcpy(m_packet + m_packetLen, &chunkNumber, sizeof(chunkNumber)); + m_packetLen += sizeof(chunkNumber); + + // number of chunks +// m_packet[m_packetLen] = cmdMsg->getNumChunks(); +// ++m_packetLen; + UnsignedInt numChunks = cmdMsg->getNumChunks(); + memcpy(m_packet + m_packetLen, &numChunks, sizeof(numChunks)); + m_packetLen += sizeof(numChunks); + + // total length of data for all chunks + UnsignedInt totalDataLength = cmdMsg->getTotalDataLength(); + memcpy(m_packet + m_packetLen, &totalDataLength, sizeof(totalDataLength)); + m_packetLen += sizeof(totalDataLength); + + // data length for this chunk + UnsignedInt dataLength = cmdMsg->getDataLength(); + memcpy(m_packet + m_packetLen, &dataLength, sizeof(dataLength)); + m_packetLen += sizeof(dataLength); + + // the offset into the data of this chunk + UnsignedInt dataOffset = cmdMsg->getDataOffset(); + memcpy(m_packet + m_packetLen, &dataOffset, sizeof(dataOffset)); + m_packetLen += sizeof(dataOffset); + + // the data for this chunk + UnsignedByte *data = cmdMsg->getData(); + memcpy(m_packet + m_packetLen, data, dataLength); + m_packetLen += dataLength; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForWrapperMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetWrapperCommandMsg *cmdMsg = (NetWrapperCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedByte) + sizeof(UnsignedShort); + } + + ++len; // 'D' + len += sizeof(UnsignedShort); // wrapped command ID + len += sizeof(UnsignedInt); // chunk number + len += sizeof(UnsignedInt); // number of chunks + len += sizeof(UnsignedInt); // total data length + len += sizeof(UnsignedInt); // data length of this chunk + len += sizeof(UnsignedInt); // offset of this chunk + len += cmdMsg->getDataLength(); // for the data of this chunk + + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + + return TRUE; +} + +/** + * Add a TimeOutGameStart to the packet. Returns true if successful. + */ +Bool NetPacket::addTimeOutGameStartMessage(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForLoadCompleteMessage(msg)) { + NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); + + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room in the packet for this command. + */ +Bool NetPacket::isRoomForTimeOutGameStartMessage(NetCommandRef *msg) { + Int len = 0; + NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // For the 'D' + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + + + +/** + * Add a Progress command to the packet. Returns true if successful. + */ +Bool NetPacket::addLoadCompleteMessage(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForLoadCompleteMessage(msg)) { + NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); + + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room in the packet for this command. + */ +Bool NetPacket::isRoomForLoadCompleteMessage(NetCommandRef *msg) { + Int len = 0; + NetCommandMsg *cmdMsg = (NetCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // For the 'D' + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + + + + +/** + * Add a Progress command to the packet. Returns true if successful. + */ +Bool NetPacket::addProgressMessage(NetCommandRef *msg) { + if (isRoomForProgressMessage(msg)) { + NetProgressCommandMsg *cmdMsg = (NetProgressCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + m_packet[m_packetLen] = cmdMsg->getPercentage(); + ++m_packetLen; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); + + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room in the packet for this command. + */ +Bool NetPacket::isRoomForProgressMessage(NetCommandRef *msg) { + Int len = 0; + NetProgressCommandMsg *cmdMsg = (NetProgressCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // For the 'D' + ++len; // percentage + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + + + +Bool NetPacket::addDisconnectVoteCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - entering...")); + // need type, player id, relay, command id, slot number + if (isRoomForDisconnectVoteMessage(msg)) { + NetDisconnectVoteCommandMsg *cmdMsg = (NetDisconnectVoteCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnsignedByte slot = cmdMsg->getSlot(); + memcpy(m_packet + m_packetLen, &slot, sizeof(slot)); + m_packetLen += sizeof(slot); + + UnsignedInt voteFrame = cmdMsg->getVoteFrame(); + memcpy(m_packet + m_packetLen, &voteFrame, sizeof(voteFrame)); + m_packetLen += sizeof(voteFrame); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectVoteCommand - added disconnect vote command, player id %d command id %d, voted slot %d", m_lastPlayerID, m_lastCommandID, slot)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room for this player disconnect command in this packet. + */ +Bool NetPacket::isRoomForDisconnectVoteMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetDisconnectVoteCommandMsg *cmdMsg = (NetDisconnectVoteCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // the 'D' + len += sizeof(UnsignedByte); // slot number + len += sizeof(UnsignedInt); // vote frame + + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addDisconnectChatCommand(NetCommandRef *msg) { + // type, player, id, relay, data + // data format: 1 byte string length, string (two bytes per character) +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - Entering...")); + if (isRoomForDisconnectChatMessage(msg)) { + NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnicodeString unitext = cmdMsg->getText(); + UnsignedByte length = unitext.getLength(); + memcpy(m_packet + m_packetLen, &length, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + memcpy(m_packet + m_packetLen, unitext.str(), length * sizeof(UnsignedShort)); + m_packetLen += length * sizeof(UnsignedShort); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added disconnect chat command")); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForDisconnectChatMessage(NetCommandRef *msg) { + Int len = 0; + NetDisconnectChatCommandMsg *cmdMsg = (NetDisconnectChatCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // the 'D' + len += sizeof(UnsignedByte); // string length + UnsignedByte textLen = cmdMsg->getText().getLength(); + len += textLen * sizeof(UnsignedShort); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addChatCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForChatMessage(msg)) { + NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectChatCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnicodeString unitext = cmdMsg->getText(); + UnsignedByte length = unitext.getLength(); + Int playerMask = cmdMsg->getPlayerMask(); + memcpy(m_packet + m_packetLen, &length, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + memcpy(m_packet + m_packetLen, unitext.str(), length * sizeof(UnsignedShort)); + m_packetLen += length * sizeof(UnsignedShort); + + memcpy(m_packet + m_packetLen, &playerMask, sizeof(Int)); + m_packetLen += sizeof(Int); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added chat command")); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +Bool NetPacket::isRoomForChatMessage(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + Int len = 0; + NetChatCommandMsg *cmdMsg = (NetChatCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // the 'D' + len += sizeof(UnsignedByte); // string length + UnsignedByte textLen = cmdMsg->getText().getLength(); + len += textLen * sizeof(UnsignedShort); + len += sizeof(Int); // playerMask + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addPacketRouterAckCommand(NetCommandRef *msg) { + // need type, player id, relay, command id, slot number + if (isRoomForPacketRouterAckMessage(msg)) { + NetPacketRouterAckCommandMsg *cmdMsg = (NetPacketRouterAckCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterAckCommand - adding packet router query command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added packet router ack command, player id %d", m_lastPlayerID)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room for this packet router ack command in this packet. + */ +Bool NetPacket::isRoomForPacketRouterAckMessage(NetCommandRef *msg) { + Int len = 0; + NetPacketRouterAckCommandMsg *cmdMsg = (NetPacketRouterAckCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // the 'D' + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addPacketRouterQueryCommand(NetCommandRef *msg) { + // need type, player id, relay, command id, slot number + if (isRoomForPacketRouterQueryMessage(msg)) { + NetPacketRouterQueryCommandMsg *cmdMsg = (NetPacketRouterQueryCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPacketRouterQueryCommand - adding packet router query command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added packet router query command, player id %d", m_lastPlayerID)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room for this packet router query command in this packet. + */ +Bool NetPacket::isRoomForPacketRouterQueryMessage(NetCommandRef *msg) { + Int len = 0; + NetPacketRouterQueryCommandMsg *cmdMsg = (NetPacketRouterQueryCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // the 'D' + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::addDisconnectPlayerCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - entering...")); + // need type, player id, relay, command id, slot number + if (isRoomForDisconnectPlayerMessage(msg)) { + NetDisconnectPlayerCommandMsg *cmdMsg = (NetDisconnectPlayerCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnsignedByte slot = cmdMsg->getDisconnectSlot(); + memcpy(m_packet + m_packetLen, &slot, sizeof(slot)); + m_packetLen += sizeof(slot); + + UnsignedInt disconnectFrame = cmdMsg->getDisconnectFrame(); + memcpy(m_packet + m_packetLen, &disconnectFrame, sizeof(disconnectFrame)); + m_packetLen += sizeof(disconnectFrame); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addDisconnectPlayerCommand - added disconnect player command, player id %d command id %d, disconnecting slot %d", m_lastPlayerID, m_lastCommandID, slot)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room for this player disconnect command in this packet. + */ +Bool NetPacket::isRoomForDisconnectPlayerMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetDisconnectPlayerCommandMsg *cmdMsg = (NetDisconnectPlayerCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // the 'D' + len += sizeof(UnsignedByte); // slot number + len += sizeof(UnsignedInt); // disconnectFrame + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + + +/** + * Add a keep alive command to the packet. Returns true if successful. + */ +Bool NetPacket::addDisconnectKeepAliveCommand(NetCommandRef *msg) { + if (isRoomForDisconnectKeepAliveMessage(msg)) { + NetDisconnectKeepAliveCommandMsg *cmdMsg = (NetDisconnectKeepAliveCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); + + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room in the packet for this command. + */ +Bool NetPacket::isRoomForDisconnectKeepAliveMessage(NetCommandRef *msg) { + Int len = 0; + NetDisconnectKeepAliveCommandMsg *cmdMsg = (NetDisconnectKeepAliveCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // For the 'D' + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +/** + * Add a keep alive command to the packet. Returns true if successful. + */ +Bool NetPacket::addKeepAliveCommand(NetCommandRef *msg) { + if (isRoomForKeepAliveMessage(msg)) { + NetKeepAliveCommandMsg *cmdMsg = (NetKeepAliveCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Added keep alive command to packet.")); + + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room in the packet for this command. + */ +Bool NetPacket::isRoomForKeepAliveMessage(NetCommandRef *msg) { + Int len = 0; + NetKeepAliveCommandMsg *cmdMsg = (NetKeepAliveCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // For the 'D' + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +/** + * Add a run ahead command to the packet. Returns true if successful. + */ +Bool NetPacket::addRunAheadCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForRunAheadMessage(msg)) { + NetRunAheadCommandMsg *cmdMsg = (NetRunAheadCommandMsg *)(msg->getCommand()); + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadCommand - adding run ahead command")); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnsignedShort newRunAhead = cmdMsg->getRunAhead(); + memcpy(m_packet + m_packetLen, &newRunAhead, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + + UnsignedByte newFrameRate = cmdMsg->getFrameRate(); + memcpy(m_packet + m_packetLen, &newFrameRate, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added run ahead command, frame %d, player id %d command id %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room for this run ahead command in this packet. + */ +Bool NetPacket::isRoomForRunAheadMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetRunAheadCommandMsg *cmdMsg = (NetRunAheadCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedShort); + len += sizeof(UnsignedByte); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +/** + * Add a DestroyPlayer command to the packet. Returns true if successful. + */ +Bool NetPacket::addDestroyPlayerCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForDestroyPlayerMessage(msg)) { + NetDestroyPlayerCommandMsg *cmdMsg = (NetDestroyPlayerCommandMsg *)(msg->getCommand()); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnsignedInt newVal = cmdMsg->getPlayerIndex(); + memcpy(m_packet + m_packetLen, &newVal, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket - added CRC:0x%8.8X info command, frame %d, player id %d command id %d", newCRC, m_lastFrame, m_lastPlayerID, m_lastCommandID)); + + ++m_numCommands; + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is room for this DestroyPlayer command in this packet. + */ +Bool NetPacket::isRoomForDestroyPlayerMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetDestroyPlayerCommandMsg *cmdMsg = (NetDestroyPlayerCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedInt); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +/** + * Add a run ahead metrics command to the packet. Returns true if successful. + */ +Bool NetPacket::addRunAheadMetricsCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForRunAheadMetricsMessage(msg)) { + NetRunAheadMetricsCommandMsg *cmdMsg = (NetRunAheadMetricsCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addRunAheadMetricsCommand - adding run ahead metrics for player %d, fps = %d, latency = %f", cmdMsg->getPlayerID(), cmdMsg->getAverageFps(), cmdMsg->getAverageLatency())); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + // write the average latency + Real averageLatency = cmdMsg->getAverageLatency(); + memcpy(m_packet + m_packetLen, &averageLatency, sizeof(averageLatency)); + m_packetLen += sizeof(averageLatency); + // write the average fps + UnsignedShort averageFps = (UnsignedShort)(cmdMsg->getAverageFps()); + memcpy(m_packet + m_packetLen, &averageFps, sizeof(averageFps)); + m_packetLen += sizeof(averageFps); + + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + ++m_numCommands; + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is enough room in the packet to fit this message. + */ +Bool NetPacket::isRoomForRunAheadMetricsMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetRunAheadMetricsCommandMsg *cmdMsg = (NetRunAheadMetricsCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // 'D' + len += sizeof(UnsignedShort); + len += sizeof(Real); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + + +/** + * Add a player leave command to the packet. Returns true if successful. + */ +Bool NetPacket::addPlayerLeaveCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isRoomForPlayerLeaveMessage(msg)) { + NetPlayerLeaveCommandMsg *cmdMsg = (NetPlayerLeaveCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addPlayerLeaveCommand - adding player leave command for player %d", cmdMsg->getLeavingPlayerID())); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnsignedByte leavingPlayerID = cmdMsg->getLeavingPlayerID(); + memcpy(m_packet + m_packetLen, &leavingPlayerID, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + ++m_numCommands; + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is enough room in the packet to fit this message. + */ +Bool NetPacket::isRoomForPlayerLeaveMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetPlayerLeaveCommandMsg *cmdMsg = (NetPlayerLeaveCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedByte); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +/** + * Add this frame command message. Returns true if successful. + */ +Bool NetPacket::addFrameCommand(NetCommandRef *msg) { + Bool needNewCommandID = FALSE; + if (isFrameRepeat(msg)) { + if (m_packetLen >= MAX_PACKET_SIZE) { + return FALSE; + } + m_packet[m_packetLen] = 'Z'; + ++m_packetLen; + m_lastCommandID = msg->getCommand()->getID(); + deleteInstance(m_lastCommand); + m_lastCommand = newInstance(NetCommandRef)(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + ++m_lastFrame; // need this cause we're actually advancing to the next frame by adding this command. + ++m_numCommands; + // frameinfodebug +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added frame %d, player %d, command count = %d, command id = %d, repeat", m_lastFrame, m_lastPlayerID, 0, m_lastCommandID)); + return TRUE; + } + if (isRoomForFrameMessage(msg)) { + NetFrameCommandMsg *cmdMsg = (NetFrameCommandMsg *)(msg->getCommand()); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addFrameCommand - adding frame command for frame %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getCommandCount(), cmdMsg->getID())); + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("relay = %d, ", m_lastRelay)); + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + needNewCommandID = TRUE; + } + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("player = %d", m_lastPlayerID)); + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command id = %d", m_lastCommandID)); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + UnsignedShort cmdCount = cmdMsg->getCommandCount(); + memcpy(m_packet + m_packetLen, &cmdCount, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + + // frameinfodebug +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added frame %d, player %d, command count = %d, command id = %d", cmdMsg->getExecutionFrame(), cmdMsg->getPlayerID(), cmdMsg->getCommandCount(), cmdMsg->getID())); + + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + ++m_numCommands; + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is enough room in this packet for this frame message. + */ +Bool NetPacket::isRoomForFrameMessage(NetCommandRef *msg) { + Int len = 0; + Bool needNewCommandID = FALSE; + NetFrameCommandMsg *cmdMsg = (NetFrameCommandMsg *)(msg->getCommand()); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + len += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastRelay != msg->getRelay()) { + len += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + len += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedShort); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::isFrameRepeat(NetCommandRef *msg) { + if (m_lastCommand == NULL) { + return FALSE; + } + if (m_lastCommand->getCommand()->getNetCommandType() != NETCOMMANDTYPE_FRAMEINFO) { + return FALSE; + } + NetFrameCommandMsg *framemsg = (NetFrameCommandMsg *)(msg->getCommand()); + NetFrameCommandMsg *lastmsg = (NetFrameCommandMsg *)(m_lastCommand->getCommand()); + if (framemsg->getCommandCount() != 0) { + return FALSE; + } + if (framemsg->getExecutionFrame() != (lastmsg->getExecutionFrame() + 1)) { + return FALSE; + } + if (msg->getRelay() != m_lastCommand->getRelay()) { + return FALSE; + } + if (framemsg->getID() != (lastmsg->getID() + 1)) { + return FALSE; + } + return TRUE; +} + +/** + * Add an ack "both" command. + */ +Bool NetPacket::addAckBothCommand(NetCommandRef *msg) { + NetAckBothCommandMsg *ackmsg = (NetAckBothCommandMsg *)(msg->getCommand()); + return addAckCommand(msg, ackmsg->getCommandID(), ackmsg->getOriginalPlayerID()); +} + +/** + * Add an ack stage 1 command. + */ +Bool NetPacket::addAckStage1Command(NetCommandRef *msg) { + NetAckStage1CommandMsg *ackmsg = (NetAckStage1CommandMsg *)(msg->getCommand()); + return addAckCommand(msg, ackmsg->getCommandID(), ackmsg->getOriginalPlayerID()); +} + +/** + * Add an ack stage 2 command. + */ +Bool NetPacket::addAckStage2Command(NetCommandRef *msg) { + NetAckStage2CommandMsg *ackmsg = (NetAckStage2CommandMsg *)(msg->getCommand()); + return addAckCommand(msg, ackmsg->getCommandID(), ackmsg->getOriginalPlayerID()); +} + +/** + * Add this ack command to the packet. Returns true if successful. + */ +Bool NetPacket::addAckCommand(NetCommandRef *msg, UnsignedShort commandID, UnsignedByte originalPlayerID) { + if (isAckRepeat(msg)) { + if (m_packetLen >= MAX_PACKET_SIZE) { + return FALSE; + } + m_packet[m_packetLen] = 'Z'; + ++m_packetLen; + ++m_numCommands; + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + return TRUE; + } + if (isRoomForAckMessage(msg)) { + NetCommandMsg *cmdMsg = msg->getCommand(); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addAckCommand - adding ack for command %d for player %d", cmdMsg->getCommandID(), msg->getCommand()->getPlayerID())); + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + + // Put in the command id of the command we are acking. + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + memcpy(m_packet + m_packetLen, &commandID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + memcpy(m_packet + m_packetLen, &originalPlayerID, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("outgoing - added ACK, original player %d, command id %d", origPlayerID, cmdID)); + ++m_numCommands; + return TRUE; + } + return FALSE; +} + +/** + * Returns true if there is enough room in the packet for this ack message. + */ +Bool NetPacket::isRoomForAckMessage(NetCommandRef *msg) { + Int len = 0; + NetCommandMsg *cmdMsg = msg->getCommand(); + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + ++len; + len += sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + ++len; + len += sizeof(UnsignedByte); + } + + ++len; // for 'D' + len += sizeof(UnsignedShort); + len += sizeof(UnsignedByte); + if ((len + m_packetLen) > MAX_PACKET_SIZE) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::isAckRepeat(NetCommandRef *msg) { + if (m_lastCommand == NULL) { + return FALSE; + } + if (m_lastCommand->getCommand()->getNetCommandType() != msg->getCommand()->getNetCommandType()) { + return FALSE; + } + if (msg->getCommand()->getNetCommandType() == NETCOMMANDTYPE_ACKBOTH) { + return isAckBothRepeat(msg); + } + if (msg->getCommand()->getNetCommandType() == NETCOMMANDTYPE_ACKSTAGE1) { + return isAckStage1Repeat(msg); + } + if (msg->getCommand()->getNetCommandType() == NETCOMMANDTYPE_ACKSTAGE2) { + return isAckStage2Repeat(msg); + } + return FALSE; +} + +Bool NetPacket::isAckBothRepeat(NetCommandRef *msg) { + NetAckBothCommandMsg *ack = (NetAckBothCommandMsg *)(msg->getCommand()); + NetAckBothCommandMsg *lastAck = (NetAckBothCommandMsg *)(m_lastCommand->getCommand()); + if (lastAck->getCommandID() != (ack->getCommandID() - 1)) { + return FALSE; + } + if (lastAck->getOriginalPlayerID() != ack->getOriginalPlayerID()) { + return FALSE; + } + if (msg->getRelay() != m_lastCommand->getRelay()) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::isAckStage1Repeat(NetCommandRef *msg) { + NetAckStage2CommandMsg *ack = (NetAckStage2CommandMsg *)(msg->getCommand()); + NetAckStage2CommandMsg *lastAck = (NetAckStage2CommandMsg *)(m_lastCommand->getCommand()); + if (lastAck->getCommandID() != (ack->getCommandID() - 1)) { + return FALSE; + } + if (lastAck->getOriginalPlayerID() != ack->getOriginalPlayerID()) { + return FALSE; + } + if (msg->getRelay() != m_lastCommand->getRelay()) { + return FALSE; + } + return TRUE; +} + +Bool NetPacket::isAckStage2Repeat(NetCommandRef *msg) { + NetAckStage2CommandMsg *ack = (NetAckStage2CommandMsg *)(msg->getCommand()); + NetAckStage2CommandMsg *lastAck = (NetAckStage2CommandMsg *)(m_lastCommand->getCommand()); + if (lastAck->getCommandID() != (ack->getCommandID() - 1)) { + return FALSE; + } + if (lastAck->getOriginalPlayerID() != ack->getOriginalPlayerID()) { + return FALSE; + } + if (msg->getRelay() != m_lastCommand->getRelay()) { + return FALSE; + } + return TRUE; +} + +/** + * Adds this game command to the packet. Returns true if successful. + */ +Bool NetPacket::addGameCommand(NetCommandRef *msg) { + Bool retval = FALSE; + NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)(msg->getCommand()); + // get the game message from the NetCommandMsg + GameMessage *gmsg = cmdMsg->constructGameMessage(); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addGameCommand for command ID %d", cmdMsg->getID())); + + if (isRoomForGameMessage(msg, gmsg)) { + // Now we know there is enough room, put the new game message into the packet. + + Bool needNewCommandID = FALSE; // this is to allow us to force the starting command ID to be respecified with this command. + + // If necessary, put the NetCommandType into the packet. + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + m_packet[m_packetLen] = 'T'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getNetCommandType(); + m_packetLen += sizeof(UnsignedByte); + + m_lastCommandType = cmdMsg->getNetCommandType(); + } + + // If necessary, put the execution frame into the packet. + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + m_packet[m_packetLen] = 'F'; + ++m_packetLen; + UnsignedInt newframe = cmdMsg->getExecutionFrame(); + memcpy(m_packet+m_packetLen, &newframe, sizeof(UnsignedInt)); + m_packetLen += sizeof(UnsignedInt); + + m_lastFrame = newframe; + } + + // If necessary, put the relay into the packet. + if (m_lastRelay != msg->getRelay()) { + m_packet[m_packetLen] = 'R'; + ++m_packetLen; + UnsignedByte newRelay = msg->getRelay(); + memcpy(m_packet+m_packetLen, &newRelay, sizeof(UnsignedByte)); + m_packetLen += sizeof(UnsignedByte); + + m_lastRelay = newRelay; + } + + // If necessary, put the playerID into the packet. + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + m_packet[m_packetLen] = 'P'; + ++m_packetLen; + m_packet[m_packetLen] = cmdMsg->getPlayerID(); + m_packetLen += sizeof(UnsignedByte); + //since if we have a new player we need to respecify the starting command ID, lets force the command id to be different. + needNewCommandID = TRUE; + + m_lastPlayerID = cmdMsg->getPlayerID(); + } + + // If necessary, specify the command ID of this command. + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + m_packet[m_packetLen] = 'C'; + ++m_packetLen; + UnsignedShort newID = cmdMsg->getID(); + memcpy(m_packet + m_packetLen, &newID, sizeof(UnsignedShort)); + m_packetLen += sizeof(UnsignedShort); + } + m_lastCommandID = cmdMsg->getID(); + + m_packet[m_packetLen] = 'D'; + ++m_packetLen; + + // Now copy the GameMessage type into the packet. + GameMessage::Type newType = gmsg->getType(); + memcpy(m_packet + m_packetLen, &newType, sizeof(GameMessage::Type)); + m_packetLen += sizeof(GameMessage::Type); + + + GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); + UnsignedByte numTypes = parser->getNumTypes(); + memcpy(m_packet + m_packetLen, &numTypes, sizeof(numTypes)); + m_packetLen += sizeof(numTypes); + + GameMessageParserArgumentType *argType = parser->getFirstArgumentType(); + while (argType != NULL) { + UnsignedByte type = (UnsignedByte)(argType->getType()); + memcpy(m_packet + m_packetLen, &type, sizeof(type)); + m_packetLen += sizeof(type); + + UnsignedByte argTypeCount = argType->getArgCount(); + memcpy(m_packet + m_packetLen, &argTypeCount, sizeof(argTypeCount)); + m_packetLen += sizeof(argTypeCount); + + argType = argType->getNext(); + } + + Int numArgs = gmsg->getArgumentCount(); + for (Int i = 0; i < numArgs; ++i) { + GameMessageArgumentDataType type = gmsg->getArgumentDataType(i); + GameMessageArgumentType arg = *(gmsg->getArgument(i)); + writeGameMessageArgumentToPacket(type, arg); + } + + deleteInstance(parser); + parser = NULL; + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::addGameMessage - added game message, frame %d, player %d, command ID %d", m_lastFrame, m_lastPlayerID, m_lastCommandID)); + + ++m_numCommands; + + if (m_lastCommand != NULL) { + deleteInstance(m_lastCommand); + m_lastCommand = NULL; + } + m_lastCommand = NEW_NETCOMMANDREF(msg->getCommand()); + m_lastCommand->setRelay(msg->getRelay()); + + retval = TRUE; + } + + if (gmsg) { + deleteInstance(gmsg); + gmsg = NULL; + } + + return retval; +} + +void NetPacket::writeGameMessageArgumentToPacket(GameMessageArgumentDataType type, GameMessageArgumentType arg) { + + switch(type) { + + case ARGUMENTDATATYPE_INTEGER: + memcpy(m_packet + m_packetLen, &(arg.integer), sizeof(arg.integer)); + m_packetLen += sizeof(arg.integer); + break; + case ARGUMENTDATATYPE_REAL: + memcpy(m_packet + m_packetLen, &(arg.real), sizeof(arg.real)); + m_packetLen += sizeof(arg.real); + break; + case ARGUMENTDATATYPE_BOOLEAN: + memcpy(m_packet + m_packetLen, &(arg.boolean), sizeof(arg.boolean)); + m_packetLen += sizeof(arg.boolean); + break; + case ARGUMENTDATATYPE_OBJECTID: + memcpy(m_packet + m_packetLen, &(arg.objectID), sizeof(arg.objectID)); + m_packetLen += sizeof(arg.objectID); + break; + case ARGUMENTDATATYPE_DRAWABLEID: + memcpy(m_packet + m_packetLen, &(arg.drawableID), sizeof(arg.drawableID)); + m_packetLen += sizeof(arg.drawableID); + break; + case ARGUMENTDATATYPE_TEAMID: + memcpy(m_packet + m_packetLen, &(arg.teamID), sizeof(arg.teamID)); + m_packetLen += sizeof(arg.teamID); + break; + case ARGUMENTDATATYPE_LOCATION: + memcpy(m_packet + m_packetLen, &(arg.location), sizeof(arg.location)); + m_packetLen += sizeof(arg.location); + break; + case ARGUMENTDATATYPE_PIXEL: + memcpy(m_packet + m_packetLen, &(arg.pixel), sizeof(arg.pixel)); + m_packetLen += sizeof(arg.pixel); + break; + case ARGUMENTDATATYPE_PIXELREGION: + memcpy(m_packet + m_packetLen, &(arg.pixelRegion), sizeof(arg.pixelRegion)); + m_packetLen += sizeof(arg.pixelRegion); + break; + case ARGUMENTDATATYPE_TIMESTAMP: + memcpy(m_packet + m_packetLen, &(arg.timestamp), sizeof(arg.timestamp)); + m_packetLen += sizeof(arg.timestamp); + break; + case ARGUMENTDATATYPE_WIDECHAR: + memcpy(m_packet + m_packetLen, &(arg.wChar), sizeof(arg.wChar)); + m_packetLen += sizeof(arg.wChar); + break; + + } // switch(type) +} + +/** + * Returns true if there is enough room in this packet for this message. + */ +Bool NetPacket::isRoomForGameMessage(NetCommandRef *msg, GameMessage *gmsg) { + // Calculate how much space the NetCommandMsg will take in this packet. + Int msglen = 0; + + NetGameCommandMsg *cmdMsg = (NetGameCommandMsg *)(msg->getCommand()); + + Bool needNewCommandID = FALSE; + + if (m_lastFrame != cmdMsg->getExecutionFrame()) { + msglen += sizeof(UnsignedInt) + sizeof(UnsignedByte); + } + if (m_lastPlayerID != cmdMsg->getPlayerID()) { + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + needNewCommandID = TRUE; + } + if (m_lastRelay != msg->getRelay()) { + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (m_lastCommandType != cmdMsg->getNetCommandType()) { + msglen += sizeof(UnsignedByte) + sizeof(UnsignedByte); + } + if (((m_lastCommandID + 1) != (UnsignedShort)(cmdMsg->getID())) || (needNewCommandID == TRUE)) { + msglen += sizeof(UnsignedShort) + sizeof(UnsignedByte); + } + + GameMessageParser *parser = newInstance(GameMessageParser)(gmsg); + + ++msglen; // for 'D' + msglen += sizeof(GameMessage::Type); + msglen += sizeof(UnsignedByte); +// Int numTypes = parser->getNumTypes(); + GameMessageParserArgumentType *arg = parser->getFirstArgumentType(); + while (arg != NULL) { + msglen += 2 * sizeof(UnsignedByte); // for the type and number of args of that type declaration. + GameMessageArgumentDataType type = arg->getType(); + + switch (type) { + + case ARGUMENTDATATYPE_INTEGER: + msglen += arg->getArgCount() * sizeof(Int); + break; + case ARGUMENTDATATYPE_REAL: + msglen += arg->getArgCount() * sizeof(Real); + break; + case ARGUMENTDATATYPE_BOOLEAN: + msglen += arg->getArgCount() * sizeof(Bool); + break; + case ARGUMENTDATATYPE_OBJECTID: + msglen += arg->getArgCount() * sizeof(ObjectID); + break; + case ARGUMENTDATATYPE_DRAWABLEID: + msglen += arg->getArgCount() * sizeof(DrawableID); + break; + case ARGUMENTDATATYPE_TEAMID: + msglen += arg->getArgCount() * sizeof(UnsignedInt); + break; + case ARGUMENTDATATYPE_LOCATION: + msglen += arg->getArgCount() * sizeof(Coord3D); + break; + case ARGUMENTDATATYPE_PIXEL: + msglen += arg->getArgCount() * sizeof(ICoord2D); + break; + case ARGUMENTDATATYPE_PIXELREGION: + msglen += arg->getArgCount() * sizeof(IRegion2D); + break; + case ARGUMENTDATATYPE_TIMESTAMP: + msglen += arg->getArgCount() * sizeof(UnsignedInt); + break; + case ARGUMENTDATATYPE_WIDECHAR: + msglen += arg->getArgCount() * sizeof(WideChar); + break; + + } // switch (type) + + arg = arg->getNext(); + + } + + deleteInstance(parser); + parser = NULL; + + // Is there enough room in the packet for this message? + if (msglen > (MAX_PACKET_SIZE - m_packetLen)) { + return FALSE; // there isn't enough room in this packet for this new message. + } + return TRUE; +} + +/** + * Returns the list of commands that are in this packet. + */ +NetCommandList * NetPacket::getCommandList() { + NetCommandList *retval = newInstance(NetCommandList); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList, packet length = %d", m_packetLen)); + retval->init(); + + // These need to be the same as the default values for m_lastPlayerID, m_lastFrame, etc. + UnsignedByte playerID = 0; + UnsignedInt frame = 0; + UnsignedShort commandID = 1; // The first command is going to be + UnsignedByte commandType = 0; + UnsignedByte relay = 0; + NetCommandRef *lastCommand = NULL; + NetCommandRef *ref = NULL; + NetCommandMsg *msg = NULL; + + Int i = 0; + while (i < m_packetLen) { + + switch(m_packet[i]) { + + case 'T': + ++i; + memcpy(&commandType, m_packet + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + break; + case 'F': + ++i; + memcpy(&frame, m_packet + i, sizeof(UnsignedInt)); + i += sizeof(UnsignedInt); + break; + case 'P': + ++i; + memcpy(&playerID, m_packet + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + break; + case 'R': + ++i; + memcpy(&relay, m_packet + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + break; + case 'C': + ++i; + memcpy(&commandID, m_packet + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + break; + case 'D': + ++i; + + msg = NULL; + ref = NULL; + + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList() - command of type %d(%s)", commandType, GetAsciiNetCommandType((NetCommandType)commandType).str())); + + switch((NetCommandType)commandType) + { + case NETCOMMANDTYPE_GAMECOMMAND: + msg = readGameMessage(m_packet, i); + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read game command from player %d for frame %d", playerID, frame)); + break; + case NETCOMMANDTYPE_ACKBOTH: + msg = readAckBothMessage(m_packet, i); + break; + case NETCOMMANDTYPE_ACKSTAGE1: + msg = readAckStage1Message(m_packet, i); + break; + case NETCOMMANDTYPE_ACKSTAGE2: + msg = readAckStage2Message(m_packet, i); + break; + case NETCOMMANDTYPE_FRAMEINFO: + msg = readFrameMessage(m_packet, i); + // frameinfodebug + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read frame %d from player %d, command count = %d, relay = 0x%X", frame, playerID, ((NetFrameCommandMsg *)msg)->getCommandCount(), relay)); + break; + case NETCOMMANDTYPE_PLAYERLEAVE: + msg = readPlayerLeaveMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read player leave message from player %d for execution on frame %d", playerID, frame)); + break; + case NETCOMMANDTYPE_RUNAHEADMETRICS: + msg = readRunAheadMetricsMessage(m_packet, i); + break; + case NETCOMMANDTYPE_RUNAHEAD: + msg = readRunAheadMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read run ahead message from player %d for execution on frame %d", playerID, frame)); + break; + case NETCOMMANDTYPE_DESTROYPLAYER: + msg = readDestroyPlayerMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read CRC info message from player %d for execution on frame %d", playerID, frame)); + break; + case NETCOMMANDTYPE_KEEPALIVE: + msg = readKeepAliveMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read keep alive message from player %d", playerID)); + break; + case NETCOMMANDTYPE_DISCONNECTKEEPALIVE: + msg = readDisconnectKeepAliveMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read keep alive message from player %d", playerID)); + break; + case NETCOMMANDTYPE_DISCONNECTPLAYER: + msg = readDisconnectPlayerMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect player message from player %d", playerID)); + break; + case NETCOMMANDTYPE_PACKETROUTERQUERY: + msg = readPacketRouterQueryMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read packet router query message from player %d", playerID)); + break; + case NETCOMMANDTYPE_PACKETROUTERACK: + msg = readPacketRouterAckMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read packet router ack message from player %d", playerID)); + break; + case NETCOMMANDTYPE_DISCONNECTCHAT: + msg = readDisconnectChatMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect chat message from player %d", playerID)); + break; + case NETCOMMANDTYPE_DISCONNECTVOTE: + msg = readDisconnectVoteMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect vote message from player %d", playerID)); + break; + case NETCOMMANDTYPE_CHAT: + msg = readChatMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read chat message from player %d", playerID)); + break; + case NETCOMMANDTYPE_PROGRESS: + msg = readProgressMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read Progress message from player %d", playerID)); + break; + case NETCOMMANDTYPE_LOADCOMPLETE: + msg = readLoadCompleteMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read LoadComplete message from player %d", playerID)); + break; + case NETCOMMANDTYPE_TIMEOUTSTART: + msg = readTimeOutGameStartMessage(m_packet, i); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read TimeOutGameStart message from player %d", playerID)); + break; + case NETCOMMANDTYPE_WRAPPER: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read Wrapper message from player %d", playerID)); + msg = readWrapperMessage(m_packet, i); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Done reading Wrapper message from player %d - wrapped command was %d", playerID, + ((NetWrapperCommandMsg *)msg)->getWrappedCommandID())); + break; + case NETCOMMANDTYPE_FILE: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read file message from player %d", playerID)); + msg = readFileMessage(m_packet, i); + break; + case NETCOMMANDTYPE_FILEANNOUNCE: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read file announce message from player %d", playerID)); + msg = readFileAnnounceMessage(m_packet, i); + break; + case NETCOMMANDTYPE_FILEPROGRESS: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read file progress message from player %d", playerID)); + msg = readFileProgressMessage(m_packet, i); + break; + case NETCOMMANDTYPE_DISCONNECTFRAME: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect frame message from player %d", playerID)); + msg = readDisconnectFrameMessage(m_packet, i); + break; + case NETCOMMANDTYPE_DISCONNECTSCREENOFF: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read disconnect screen off message from player %d", playerID)); + msg = readDisconnectScreenOffMessage(m_packet, i); + break; + case NETCOMMANDTYPE_FRAMERESENDREQUEST: + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("read frame resend request message from player %d", playerID)); + msg = readFrameResendRequestMessage(m_packet, i); + break; + } + + if (msg == NULL) { + DEBUG_CRASH(("Didn't read a message from the packet. Things are about to go wrong.")); + continue; + } + + // set the info + msg->setExecutionFrame(frame); + msg->setPlayerID(playerID); + msg->setNetCommandType((NetCommandType)commandType); + msg->setID(commandID); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("frame = %d, player = %d, command type = %d, id = %d", frame, playerID, commandType, commandID)); + + // increment to the next command ID. + if (DoesCommandRequireACommandID((NetCommandType)commandType)) { + ++commandID; + } + + // add the message to the list. + ref = retval->addMessage(msg); + if (ref != NULL) { + ref->setRelay(relay); + } else { + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList - failed to set relay for message %d", msg->getID())); + } + + if (lastCommand != NULL) { + deleteInstance(lastCommand); + lastCommand = NULL; + } + lastCommand = newInstance(NetCommandRef)(msg); + + msg->detach(); // Need to detach from new NetCommandMsg created by the "readXMessage" above. + + // since the message is part of the list now, we don't have to keep track of it. So we'll just set it to NULL. + msg = NULL; + break; // switch(m_packet[i]) case 'D': + + case 'Z': // switch(m_packet[i]) + + ++i; + // Repeat the last command, doing some funky cool byte-saving stuff + if (lastCommand == NULL) { + DEBUG_CRASH(("Got a repeat command with no command to repeat.")); + } + msg = NULL; + ref = NULL; + + switch(commandType) { + + case NETCOMMANDTYPE_ACKSTAGE1: { + msg = newInstance(NetAckStage1CommandMsg)(); + NetAckStage1CommandMsg* laststageone = (NetAckStage1CommandMsg*)(lastCommand->getCommand()); + ((NetAckStage1CommandMsg*)msg)->setCommandID(laststageone->getCommandID() + 1); + ((NetAckStage1CommandMsg*)msg)->setOriginalPlayerID(laststageone->getOriginalPlayerID()); + break; + } + case NETCOMMANDTYPE_ACKSTAGE2: { + msg = newInstance(NetAckStage2CommandMsg)(); + NetAckStage2CommandMsg* laststagetwo = (NetAckStage2CommandMsg*)(lastCommand->getCommand()); + ((NetAckStage2CommandMsg*)msg)->setCommandID(laststagetwo->getCommandID() + 1); + ((NetAckStage2CommandMsg*)msg)->setOriginalPlayerID(laststagetwo->getOriginalPlayerID()); + break; + } + case NETCOMMANDTYPE_ACKBOTH: { + msg = newInstance(NetAckBothCommandMsg)(); + NetAckBothCommandMsg* lastboth = (NetAckBothCommandMsg*)(lastCommand->getCommand()); + ((NetAckBothCommandMsg*)msg)->setCommandID(lastboth->getCommandID() + 1); + ((NetAckBothCommandMsg*)msg)->setOriginalPlayerID(lastboth->getOriginalPlayerID()); + break; + } + case NETCOMMANDTYPE_FRAMEINFO: { + msg = newInstance(NetFrameCommandMsg)(); + ++frame; // this is set below. + ((NetFrameCommandMsg*)msg)->setCommandCount(0); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Read a repeated frame command, frame = %d, player = %d, commandID = %d", frame, playerID, commandID)); + break; + } + default: + DEBUG_CRASH(("Trying to repeat a command that shouldn't be repeated.")); + continue; + + } + + msg->setExecutionFrame(frame); + msg->setPlayerID(playerID); + msg->setNetCommandType((NetCommandType)commandType); + msg->setID(commandID); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("frame = %d, player = %d, command type = %d, id = %d", frame, playerID, commandType, commandID)); + + // increment to the next command ID. + if (DoesCommandRequireACommandID((NetCommandType)commandType)) { + ++commandID; + } + + // add the message to the list. + ref = retval->addMessage(msg); + if (ref != NULL) { + ref->setRelay(relay); + } + + deleteInstance(lastCommand); + lastCommand = NULL; +// lastCommand = newInstance(NetCommandRef)(msg); + lastCommand = NEW_NETCOMMANDREF(msg); + + msg->detach(); // Need to detach from new NetCommandMsg created by the "readXMessage" above. + + // since the message is part of the list now, we don't have to keep track of it. So we'll just set it to NULL. + msg = NULL; + break; + + default: + // we don't recognize this command, but we have to increment i so we don't fall into an infinite loop. + DEBUG_CRASH(("Unrecognized packet entry, ignoring.")); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::getCommandList - Unrecognized packet entry at index %d", i)); + dumpPacketToLog(); + ++i; + break; + + } + + } + + if (lastCommand != NULL) { + deleteInstance(lastCommand); + lastCommand = NULL; + } + return retval; +} + +/** + * Reads the data portion of a game message from the given position in the packet. + */ +NetCommandMsg * NetPacket::readGameMessage(UnsignedByte *data, Int &i) +{ + NetGameCommandMsg *msg = newInstance(NetGameCommandMsg); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readGameMessage")); + + // Get the GameMessage command type. + GameMessage::Type newType; + memcpy(&newType, data + i, sizeof(GameMessage::Type)); + i += sizeof(GameMessage::Type); + msg->setGameMessageType(newType); + + // Get the number of argument types + UnsignedByte numArgTypes = 0; + memcpy(&numArgTypes, data + i, sizeof(numArgTypes)); + i += sizeof(numArgTypes); + + // Get the types and the number of arguments of those types. + Int totalArgCount = 0; + GameMessageParser *parser = newInstance(GameMessageParser)(); + Int j = 0; + for (; j < numArgTypes; ++j) { + UnsignedByte type = (UnsignedByte)ARGUMENTDATATYPE_UNKNOWN; + memcpy(&type, data + i, sizeof(type)); + i += sizeof(type); + + UnsignedByte argCount = 0; + memcpy(&argCount, data + i, sizeof(argCount)); + i += sizeof(argCount); + + parser->addArgType((GameMessageArgumentDataType)type, argCount); + totalArgCount += argCount; + } + + GameMessageParserArgumentType *parserArgType = parser->getFirstArgumentType(); + GameMessageArgumentDataType lasttype = ARGUMENTDATATYPE_UNKNOWN; + Int argsLeftForType = 0; + if (parserArgType != NULL) { + lasttype = parserArgType->getType(); + argsLeftForType = parserArgType->getArgCount(); + } + for (j = 0; j < totalArgCount; ++j) { + readGameMessageArgumentFromPacket(lasttype, msg, data, i); + + --argsLeftForType; + if (argsLeftForType == 0) { + DEBUG_ASSERTCRASH(parserArgType != NULL, ("parserArgType was NULL when it shouldn't have been.")); + if (parserArgType == NULL) { + return NULL; + } + + parserArgType = parserArgType->getNext(); + // parserArgType is allowed to be NULL here + if (parserArgType != NULL) { + argsLeftForType = parserArgType->getArgCount(); + lasttype = parserArgType->getType(); + } + } + } + + deleteInstance(parser); + parser = NULL; + + return (NetCommandMsg *)msg; +} + +void NetPacket::readGameMessageArgumentFromPacket(GameMessageArgumentDataType type, NetGameCommandMsg *msg, UnsignedByte *data, Int &i) { + + GameMessageArgumentType arg; + + switch (type) { + + case ARGUMENTDATATYPE_INTEGER: + Int theint; + memcpy(&theint, data + i, sizeof(theint)); + i += sizeof(theint); + arg.integer = theint; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_REAL: + Real thereal; + memcpy(&thereal, data + i, sizeof(thereal)); + i += sizeof(thereal); + arg.real = thereal; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_BOOLEAN: + Bool thebool; + memcpy(&thebool, data + i, sizeof(thebool)); + i += sizeof(thebool); + arg.boolean = thebool; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_OBJECTID: + ObjectID theobjectid; + memcpy(&theobjectid, data + i, sizeof(theobjectid)); + i += sizeof(theobjectid); + arg.objectID = theobjectid; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_DRAWABLEID: + DrawableID thedrawableid; + memcpy(&thedrawableid, data + i, sizeof(thedrawableid)); + i += sizeof(thedrawableid); + arg.drawableID = thedrawableid; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_TEAMID: + UnsignedInt theunsignedint; + memcpy(&theunsignedint, data + i, sizeof(theunsignedint)); + i += sizeof(theunsignedint); + arg.teamID = theunsignedint; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_LOCATION: + Coord3D coord; + memcpy(&coord, data + i, sizeof(coord)); + i += sizeof(coord); + arg.location = coord; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_PIXEL: + ICoord2D pixel; + memcpy(&pixel, data + i, sizeof(pixel)); + i += sizeof(pixel); + arg.pixel = pixel; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_PIXELREGION: + IRegion2D reg; + memcpy(®, data + i, sizeof(reg)); + i += sizeof(reg); + arg.pixelRegion = reg; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_TIMESTAMP: + UnsignedInt stamp; + memcpy(&stamp, data + i, sizeof(stamp)); + i += sizeof(stamp); + arg.timestamp = stamp; + msg->addArgument(type, arg); + break; + + case ARGUMENTDATATYPE_WIDECHAR: + WideChar c; + memcpy(&c, data + i, sizeof(c)); + i += sizeof(c); + arg.wChar = c; + msg->addArgument(type, arg); + break; + + } + +} + +/** + * Reads the data portion of the ack message at this position in the packet. + */ +NetCommandMsg * NetPacket::readAckBothMessage(UnsignedByte *data, Int &i) { + NetAckBothCommandMsg *msg = newInstance(NetAckBothCommandMsg); + + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readAckMessage, ")); + UnsignedShort cmdID = 0; + + memcpy(&cmdID, data + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + msg->setCommandID(cmdID); + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("commandID = %d, ", cmdID)); + + UnsignedByte origPlayerID = 0; + memcpy(&origPlayerID, data + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + msg->setOriginalPlayerID(origPlayerID); + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("original player id = %d", origPlayerID)); + + return msg; +} + +/** + * Reads the data portion of the ack message at this position in the packet. + */ +NetCommandMsg * NetPacket::readAckStage1Message(UnsignedByte *data, Int &i) { + NetAckStage1CommandMsg *msg = newInstance(NetAckStage1CommandMsg); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readAckMessage, ")); + UnsignedShort cmdID = 0; + + memcpy(&cmdID, data + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + msg->setCommandID(cmdID); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("commandID = %d, ", cmdID)); + + UnsignedByte origPlayerID = 0; + memcpy(&origPlayerID, data + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + msg->setOriginalPlayerID(origPlayerID); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("original player id = %d", origPlayerID)); + + return msg; +} + +/** + * Reads the data portion of the ack message at this position in the packet. + */ +NetCommandMsg * NetPacket::readAckStage2Message(UnsignedByte *data, Int &i) { + NetAckStage2CommandMsg *msg = newInstance(NetAckStage2CommandMsg); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readAckMessage, ")); + UnsignedShort cmdID = 0; + + memcpy(&cmdID, data + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + msg->setCommandID(cmdID); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("commandID = %d, ", cmdID)); + + UnsignedByte origPlayerID = 0; + memcpy(&origPlayerID, data + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + msg->setOriginalPlayerID(origPlayerID); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("original player id = %d", origPlayerID)); + + return msg; +} + +/** + * Reads the data portion of the frame message at this position in the packet. + */ +NetCommandMsg * NetPacket::readFrameMessage(UnsignedByte *data, Int &i) { + NetFrameCommandMsg *msg = newInstance(NetFrameCommandMsg); + +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readFrameMessage, ")); + UnsignedShort cmdCount = 0; + + memcpy(&cmdCount, data + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + msg->setCommandCount(cmdCount); +// DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("command count = %d, ", cmdCount)); + + return msg; +} + +/** + * Reads the player leave message at this position in the packet. + */ +NetCommandMsg * NetPacket::readPlayerLeaveMessage(UnsignedByte *data, Int &i) { + NetPlayerLeaveCommandMsg *msg = newInstance(NetPlayerLeaveCommandMsg); + + UnsignedByte leavingPlayerID = 0; + + memcpy(&leavingPlayerID, data + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + msg->setLeavingPlayerID(leavingPlayerID); + + return msg; +} + +/** + * Reads the run ahead metrics message at this position in the packet. + */ +NetCommandMsg * NetPacket::readRunAheadMetricsMessage(UnsignedByte *data, Int &i) { + NetRunAheadMetricsCommandMsg *msg = newInstance(NetRunAheadMetricsCommandMsg); + + Real averageLatency = (Real)0.2; + UnsignedShort averageFps = 30; + + memcpy(&averageLatency, data + i, sizeof(Real)); + i += sizeof(Real); + msg->setAverageLatency(averageLatency); + + memcpy(&averageFps, data + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + msg->setAverageFps((Int)averageFps); + return msg; +} + +/** + * Reads the run ahead message at this position in the packet. + */ +NetCommandMsg * NetPacket::readRunAheadMessage(UnsignedByte *data, Int &i) { + NetRunAheadCommandMsg *msg = newInstance(NetRunAheadCommandMsg); + + UnsignedShort newRunAhead = 20; + memcpy(&newRunAhead, data + i, sizeof(UnsignedShort)); + i += sizeof(UnsignedShort); + msg->setRunAhead(newRunAhead); + + UnsignedByte newFrameRate = 30; + memcpy(&newFrameRate, data + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + msg->setFrameRate(newFrameRate); + + return msg; +} + +/** + * Reads the CRC info message at this position in the packet. + */ +NetCommandMsg * NetPacket::readDestroyPlayerMessage(UnsignedByte *data, Int &i) { + NetDestroyPlayerCommandMsg *msg = newInstance(NetDestroyPlayerCommandMsg); + + UnsignedInt newVal = 0; + memcpy(&newVal, data + i, sizeof(UnsignedInt)); + i += sizeof(UnsignedInt); + msg->setPlayerIndex(newVal); + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("Saw CRC of 0x%8.8X", newCRC)); + + return msg; +} + +/** + * Reads the keep alive data, of which there is none. + */ +NetCommandMsg * NetPacket::readKeepAliveMessage(UnsignedByte *data, Int &i) { + NetKeepAliveCommandMsg *msg = newInstance(NetKeepAliveCommandMsg); + + return msg; +} + +/** + * Reads the disconnect keep alive data, of which there is none. + */ +NetCommandMsg * NetPacket::readDisconnectKeepAliveMessage(UnsignedByte *data, Int &i) { + NetDisconnectKeepAliveCommandMsg *msg = newInstance(NetDisconnectKeepAliveCommandMsg); + + return msg; +} + +/** + * Reads the disconnect player data. Which is the slot number of the player being disconnected. + */ +NetCommandMsg * NetPacket::readDisconnectPlayerMessage(UnsignedByte *data, Int &i) { + NetDisconnectPlayerCommandMsg *msg = newInstance(NetDisconnectPlayerCommandMsg); + + UnsignedByte slot = 0; + memcpy(&slot, data + i, sizeof(slot)); + i += sizeof(slot); + msg->setDisconnectSlot(slot); + + UnsignedInt disconnectFrame = 0; + memcpy(&disconnectFrame, data + i, sizeof(disconnectFrame)); + i += sizeof(disconnectFrame); + msg->setDisconnectFrame(disconnectFrame); + + return msg; +} + +/** + * Reads the packet router query data, of which there is none. + */ +NetCommandMsg * NetPacket::readPacketRouterQueryMessage(UnsignedByte *data, Int &i) { + NetPacketRouterQueryCommandMsg *msg = newInstance(NetPacketRouterQueryCommandMsg); + + return msg; +} + +/** + * Reads the packet router ack data, of which there is none. + */ +NetCommandMsg * NetPacket::readPacketRouterAckMessage(UnsignedByte *data, Int &i) { + NetPacketRouterAckCommandMsg *msg = newInstance(NetPacketRouterAckCommandMsg); + + return msg; +} + +/** + * Reads the disconnect chat data, which is just the string. + */ +NetCommandMsg * NetPacket::readDisconnectChatMessage(UnsignedByte *data, Int &i) { + NetDisconnectChatCommandMsg *msg = newInstance(NetDisconnectChatCommandMsg); + + WideChar text[256]; + UnsignedByte length; + memcpy(&length, data + i, sizeof(UnsignedByte)); + ++i; + memcpy(text, data + i, length * sizeof(WideChar)); + i += length * sizeof(WideChar); + text[length] = 0; + + UnicodeString unitext; + unitext.set(text); + + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readDisconnectChatMessage - read message, message is %ls", unitext.str())); + + msg->setText(unitext); + return msg; +} + +/** + * Reads the chat data, which is just the string. + */ +NetCommandMsg * NetPacket::readChatMessage(UnsignedByte *data, Int &i) { + NetChatCommandMsg *msg = newInstance(NetChatCommandMsg); + + WideChar text[256]; + UnsignedByte length; + Int playerMask; + memcpy(&length, data + i, sizeof(UnsignedByte)); + ++i; + memcpy(text, data + i, length * sizeof(WideChar)); + i += length * sizeof(WideChar); + text[length] = 0; + memcpy(&playerMask, data + i, sizeof(Int)); + i += sizeof(Int); + + + UnicodeString unitext; + unitext.set(text); + + //DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readChatMessage - read message, message is %ls", unitext.str())); + + msg->setText(unitext); + msg->setPlayerMask(playerMask); + return msg; +} + +/** + * Reads the disconnect vote data. Which is the slot number of the player being disconnected. + */ +NetCommandMsg * NetPacket::readDisconnectVoteMessage(UnsignedByte *data, Int &i) { + NetDisconnectVoteCommandMsg *msg = newInstance(NetDisconnectVoteCommandMsg); + + UnsignedByte slot = 0; + memcpy(&slot, data + i, sizeof(slot)); + i += sizeof(slot); + msg->setSlot(slot); + + UnsignedInt voteFrame = 0; + memcpy(&voteFrame, data + i, sizeof(voteFrame)); + i += sizeof(voteFrame); + msg->setVoteFrame(voteFrame); + + return msg; +} + +/** + * Reads the Progress data. Which is the slot number of the player being disconnected. + */ +NetCommandMsg * NetPacket::readProgressMessage(UnsignedByte *data, Int &i) { + NetProgressCommandMsg *msg = newInstance(NetProgressCommandMsg); + + UnsignedByte percentage = 0; + memcpy(&percentage, data + i, sizeof(UnsignedByte)); + i += sizeof(UnsignedByte); + msg->setPercentage(percentage); + + return msg; +} + +NetCommandMsg * NetPacket::readLoadCompleteMessage(UnsignedByte *data, Int &i) { + NetCommandMsg *msg = newInstance(NetCommandMsg); + return msg; +} + +NetCommandMsg * NetPacket::readTimeOutGameStartMessage(UnsignedByte *data, Int &i) { + NetCommandMsg *msg = newInstance(NetCommandMsg); + return msg; +} + +NetCommandMsg * NetPacket::readWrapperMessage(UnsignedByte *data, Int &i) { + NetWrapperCommandMsg *msg = newInstance(NetWrapperCommandMsg); + + // get the wrapped command ID + UnsignedShort wrappedCommandID = 0; + memcpy(&wrappedCommandID, data + i, sizeof(wrappedCommandID)); + msg->setWrappedCommandID(wrappedCommandID); + i += sizeof(wrappedCommandID); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - wrapped command ID == %d", wrappedCommandID)); + + // get the chunk number. + UnsignedInt chunkNumber = 0; + memcpy(&chunkNumber, data + i, sizeof(chunkNumber)); + msg->setChunkNumber(chunkNumber); + i += sizeof(chunkNumber); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - chunk number = %d", chunkNumber)); + + // get the number of chunks + UnsignedInt numChunks = 0; + memcpy(&numChunks, data + i, sizeof(numChunks)); + msg->setNumChunks(numChunks); + i += sizeof(numChunks); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - number of chunks = %d", numChunks)); + + // get the total data length + UnsignedInt totalDataLength = 0; + memcpy(&totalDataLength, data + i, sizeof(totalDataLength)); + msg->setTotalDataLength(totalDataLength); + i += sizeof(totalDataLength); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - total data length = %d", totalDataLength)); + + // get the data length for this chunk + UnsignedInt dataLength = 0; + memcpy(&dataLength, data + i, sizeof(dataLength)); + i += sizeof(dataLength); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - data length = %d", dataLength)); + + UnsignedInt dataOffset = 0; + memcpy(&dataOffset, data + i, sizeof(dataOffset)); + msg->setDataOffset(dataOffset); + i += sizeof(dataOffset); + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readWrapperMessage - data offset = %d", dataOffset)); + + msg->setData(data + i, dataLength); + i += dataLength; + + return msg; +} + +NetCommandMsg * NetPacket::readFileMessage(UnsignedByte *data, Int &i) { + NetFileCommandMsg *msg = newInstance(NetFileCommandMsg); + char filename[_MAX_PATH]; + char *c = filename; + + while (data[i] != 0) { + *c = data[i]; + ++c; + ++i; + } + *c = 0; + ++i; + msg->setPortableFilename(AsciiString(filename)); // it's transferred as a portable filename + + UnsignedInt dataLength = 0; + memcpy(&dataLength, data + i, sizeof(dataLength)); + i += sizeof(dataLength); + + UnsignedByte *buf = NEW UnsignedByte[dataLength]; + memcpy(buf, data + i, dataLength); + i += dataLength; + + msg->setFileData(buf, dataLength); + + return msg; +} + +NetCommandMsg * NetPacket::readFileAnnounceMessage(UnsignedByte *data, Int &i) { + NetFileAnnounceCommandMsg *msg = newInstance(NetFileAnnounceCommandMsg); + char filename[_MAX_PATH]; + char *c = filename; + + while (data[i] != 0) { + *c = data[i]; + ++c; + ++i; + } + *c = 0; + ++i; + msg->setPortableFilename(AsciiString(filename)); // it's transferred as a portable filename + + UnsignedShort fileID = 0; + memcpy(&fileID, data + i, sizeof(fileID)); + i += sizeof(fileID); + msg->setFileID(fileID); + + UnsignedByte playerMask = 0; + memcpy(&playerMask, data + i, sizeof(playerMask)); + i += sizeof(playerMask); + msg->setPlayerMask(playerMask); + + return msg; +} + +NetCommandMsg * NetPacket::readFileProgressMessage(UnsignedByte *data, Int &i) { + NetFileProgressCommandMsg *msg = newInstance(NetFileProgressCommandMsg); + + UnsignedShort fileID = 0; + memcpy(&fileID, data + i, sizeof(fileID)); + i += sizeof(fileID); + msg->setFileID(fileID); + + Int progress = 0; + memcpy(&progress, data + i, sizeof(progress)); + i += sizeof(progress); + msg->setProgress(progress); + + return msg; +} + +NetCommandMsg * NetPacket::readDisconnectFrameMessage(UnsignedByte *data, Int &i) { + NetDisconnectFrameCommandMsg *msg = newInstance(NetDisconnectFrameCommandMsg); + + UnsignedInt disconnectFrame = 0; + memcpy(&disconnectFrame, data + i, sizeof(disconnectFrame)); + i += sizeof(disconnectFrame); + msg->setDisconnectFrame(disconnectFrame); + + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::readDisconnectFrameMessage - read disconnect frame for frame %d", disconnectFrame)); + + return msg; +} + +NetCommandMsg * NetPacket::readDisconnectScreenOffMessage(UnsignedByte *data, Int &i) { + NetDisconnectScreenOffCommandMsg *msg = newInstance(NetDisconnectScreenOffCommandMsg); + + UnsignedInt newFrame = 0; + memcpy(&newFrame, data + i, sizeof(newFrame)); + i += sizeof(newFrame); + msg->setNewFrame(newFrame); + + return msg; +} + +NetCommandMsg * NetPacket::readFrameResendRequestMessage(UnsignedByte *data, Int &i) { + NetFrameResendRequestCommandMsg *msg = newInstance(NetFrameResendRequestCommandMsg); + + UnsignedInt frameToResend = 0; + memcpy(&frameToResend, data + i, sizeof(frameToResend)); + i += sizeof(frameToResend); + msg->setFrameToResend(frameToResend); + + return msg; +} + +/** + * Returns the number of commands in this packet. Only valid if the packet is locally constructed. + */ +Int NetPacket::getNumCommands() { + return m_numCommands; +} + +/** + * Returns the address that this packet is to be sent to. Only valid if the packet is locally constructed. + */ +UnsignedInt NetPacket::getAddr() { + return m_addr; +} + +/** + * Returns the port that this packet is to be sent to. Only valid if the packet is locally constructed. + */ +UnsignedShort NetPacket::getPort() { + return m_port; +} + +/** + * Returns the data of this packet. + */ +UnsignedByte * NetPacket::getData() { + return m_packet; +} + +/** + * Returns the length of the packet. + */ +Int NetPacket::getLength() { + return m_packetLen; +} + +/** + * Dumps the packet to the debug log file + */ +void NetPacket::dumpPacketToLog() { + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("NetPacket::dumpPacketToLog() - packet is %d bytes", m_packetLen)); + Int numLines = m_packetLen / 8; + if ((m_packetLen % 8) != 0) { + ++numLines; + } + for (Int dumpindex = 0; dumpindex < numLines; ++dumpindex) { + DEBUG_LOG_LEVEL_RAW(DEBUG_LEVEL_NET, ("\t%d\t", dumpindex*8)); + for (Int dumpindex2 = 0; (dumpindex2 < 8) && ((dumpindex*8 + dumpindex2) < m_packetLen); ++dumpindex2) { + DEBUG_LOG_LEVEL_RAW(DEBUG_LEVEL_NET, ("%02x '%c' ", m_packet[dumpindex*8 + dumpindex2], m_packet[dumpindex*8 + dumpindex2])); + } + DEBUG_LOG_LEVEL_RAW(DEBUG_LEVEL_NET, ("\n")); + } + DEBUG_LOG_LEVEL(DEBUG_LEVEL_NET, ("End of packet dump")); +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp index be0ff31762b..ecd66148d2e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp @@ -559,7 +559,7 @@ Bool Network::processCommand(GameMessage *msg) // Send command counts for all the frames we can. for (Int i = m_lastFrameCompleted + 1; i < executionFrame; ++i) { m_conMgr->processFrameTick(i); - //DEBUG_LOG(("Network::processCommand - calling processFrameTick for frame %d", i)); + //DEBUG_LOG(("Network::processCommand - calling processFrameTick for frame %d", i)); m_lastFrameCompleted = i; } } @@ -679,7 +679,7 @@ void Network::processRunAheadCommand(NetRunAheadCommandMsg *msg) { NetworkLog(ELogVerbosity::LOG_DEBUG, "Frame grouping is %d, formula was (1000 * %d) / %d", (int)frameGrouping, (int)m_runAhead, (int)m_frameRate); frameGrouping = frameGrouping / 2; // since we only want the latency for one way to be a factor. -// DEBUG_LOG(("Network::processRunAheadCommand - trying to set frame grouping to %d. run ahead = %d, m_frameRate = %d", frameGrouping, m_runAhead, m_frameRate)); +// DEBUG_LOG(("Network::processRunAheadCommand - trying to set frame grouping to %d. run ahead = %d, m_frameRate = %d", frameGrouping, m_runAhead, m_frameRate)); if (frameGrouping < 1) { frameGrouping = 1; // Having a value less than 1 doesn't make sense. } @@ -763,7 +763,7 @@ void Network::update( void ) if (AllCommandsReady(TheGameLogic->getFrame())) { // If all the commands are ready for the next frame... m_conMgr->handleAllCommandsReady(); -// DEBUG_LOG(("Network::update - frame %d is ready", TheGameLogic->getFrame())); +// DEBUG_LOG(("Network::update - frame %d is ready", TheGameLogic->getFrame())); if (timeForNewFrame()) { // This needs to come after any other pre-frame execution checks as this changes the timing variables. RelayCommandsToCommandList(TheGameLogic->getFrame()); // Put the commands for the next frame on TheCommandList. m_frameDataReady = TRUE; // Tell the GameEngine to run the commands for the new frame. @@ -773,7 +773,7 @@ void Network::update( void ) void Network::liteupdate() { -#if defined(RTS_DEBUG) +#if defined(RTS_DEBUG) if (m_networkOn == FALSE) { return; } @@ -819,33 +819,37 @@ Bool Network::timeForNewFrame() { Real cushion = m_conMgr->getMinimumCushion(); Real runAheadPercentage = m_runAhead * (TheGlobalData->m_networkRunAheadSlack / (Real)100.0); // If we are at least 50% into our slack, we need to slow down. if (cushion < runAheadPercentage) { - __int64 oldFrameDelay = frameDelay; - frameDelay += oldFrameDelay / 10; // temporarily decrease the frame rate by 20%. -// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f. Adjusting frameDelay from %I64d to %I64d", cushion, runAheadPercentage, oldFrameDelay, frameDelay)); + __int64 oldFrameDelay = frameDelay; +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) + frameDelay += oldFrameDelay / 5; // temporarily decrease the frame rate by 20%. +#else + frameDelay += oldFrameDelay / 10; // temporarily decrease the frame rate by 20%. +#endif +// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f. Adjusting frameDelay from %I64d to %I64d", cushion, runAheadPercentage, oldFrameDelay, frameDelay)); m_didSelfSlug = TRUE; // } else { -// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f", cushion, runAheadPercentage)); +// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f", cushion, runAheadPercentage)); } } // Check to see if we can run another frame. if (curTime >= m_nextFrameTime) { -// DEBUG_LOG(("Allowing a new frame, frameDelay = %I64d, curTime - m_nextFrameTime = %I64d", frameDelay, curTime - m_nextFrameTime)); +// DEBUG_LOG(("Allowing a new frame, frameDelay = %I64d, curTime - m_nextFrameTime = %I64d", frameDelay, curTime - m_nextFrameTime)); // if (m_nextFrameTime + frameDelay < curTime) { if ((m_nextFrameTime + (2 * frameDelay)) < curTime) { // If we get too far behind on our framerate we need to reset the nextFrameTime thing. m_nextFrameTime = curTime; -// DEBUG_LOG(("Initializing m_nextFrameTime to %I64d", m_nextFrameTime)); +// DEBUG_LOG(("Initializing m_nextFrameTime to %I64d", m_nextFrameTime)); } else { // Set the soonest possible starting time for the next frame. m_nextFrameTime += frameDelay; -// DEBUG_LOG(("m_nextFrameTime = %I64d", m_nextFrameTime)); +// DEBUG_LOG(("m_nextFrameTime = %I64d", m_nextFrameTime)); } return TRUE; } -// DEBUG_LOG(("Slowing down frame rate. frame rate = %d, frame delay = %I64d, curTime - m_nextFrameTime = %I64d", m_frameRate, frameDelay, curTime - m_nextFrameTime)); +// DEBUG_LOG(("Slowing down frame rate. frame rate = %d, frame delay = %I64d, curTime - m_nextFrameTime = %I64d", m_frameRate, frameDelay, curTime - m_nextFrameTime)); return FALSE; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp index ffb3af1df32..2f27c83d979 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp @@ -215,7 +215,7 @@ Bool CommandRequiresDirectSend(NetCommandMsg *msg) { return TRUE; } #if defined(GENERALS_ONLINE) - return FALSE; + return TRUE; #else return FALSE; #endif From 9ff2a80b668bf7e36a3c4a20927ed7d9d42e6dac Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Sat, 2 Aug 2025 22:08:30 -0400 Subject: [PATCH 11/83] - Client now sets "client_id" as required by backend - More RA tweaks --- .../Include/GameNetwork/GeneralsOnline/NextGenMP_defines.h | 6 ++++++ .../GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp | 3 +++ .../Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp | 6 +++--- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenMP_defines.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenMP_defines.h index 73fbd0879d2..ebbe6b6885a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenMP_defines.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenMP_defines.h @@ -50,6 +50,12 @@ //#define GENERALS_ONLINE_HIGH_FPS_SERVER 1 +#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) +#define GENERALS_ONLINE_CLIENT_ID "gen_online_60hz" +#else +#define GENERALS_ONLINE_CLIENT_ID "gen_online_30hz" +#endif + #if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index b0b143426aa..77232efc09d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -153,6 +153,7 @@ void NGMP_OnlineServices_AuthInterface::BeginLogin() nlohmann::json j; j["token"] = strToken.c_str(); + j["client_id"] = GENERALS_ONLINE_CLIENT_ID; PrepareChallenge(j); std::string strPostData = j.dump(); @@ -207,6 +208,7 @@ void NGMP_OnlineServices_AuthInterface::BeginLogin() nlohmann::json j; j["token"] = strToken.c_str(); + j["client_id"] = GENERALS_ONLINE_CLIENT_ID; PrepareChallenge(j); std::string strPostData = j.dump(); @@ -300,6 +302,7 @@ void NGMP_OnlineServices_AuthInterface::Tick() nlohmann::json j; j["code"] = m_strCode.c_str(); + j["client_id"] = GENERALS_ONLINE_CLIENT_ID; PrepareChallenge(j); std::string strPostData = j.dump(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp index 2f27c83d979..dc1e7df8604 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp @@ -29,13 +29,13 @@ #if defined(GENERALS_ONLINE) #if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) -Int MAX_FRAMES_AHEAD = 128; -Int MIN_RUNAHEAD = 5; +Int MAX_FRAMES_AHEAD = 128 * 2; +Int MIN_RUNAHEAD = 10; Int FRAME_DATA_LENGTH = (MAX_FRAMES_AHEAD + 1) * 2; Int FRAMES_TO_KEEP = (MAX_FRAMES_AHEAD / 2) + 1; #else Int MAX_FRAMES_AHEAD = 128; -Int MIN_RUNAHEAD = 5; +Int MIN_RUNAHEAD = 5; Int FRAME_DATA_LENGTH = (MAX_FRAMES_AHEAD + 1) * 2; Int FRAMES_TO_KEEP = (MAX_FRAMES_AHEAD / 2) + 1; #endif From 6ad992fd0a800b5b3fec62ccbbe8e95c32b4cc76 Mon Sep 17 00:00:00 2001 From: Stubbjax Date: Sun, 3 Aug 2025 18:35:23 +1000 Subject: [PATCH 12/83] bugfix(controls): Fix double-press group selection logic not reselecting deselected groups (#1355) --- .../Code/GameEngine/Include/GameClient/InGameUI.h | 1 + .../GameEngine/Source/GameClient/InGameUI.cpp | 15 +++++++++++++++ .../GameClient/MessageStream/SelectionXlat.cpp | 13 +++++++++++-- .../Code/GameEngine/Include/GameClient/InGameUI.h | 1 + .../GameEngine/Source/GameClient/InGameUI.cpp | 15 +++++++++++++++ .../GameClient/MessageStream/SelectionXlat.cpp | 13 +++++++++++-- 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h index 4f8e37cb94f..a5051706b1c 100644 --- a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h @@ -445,6 +445,7 @@ friend class Drawable; // for selection/deselection transactions virtual Drawable *getFirstSelectedDrawable( void ); ///< get the first selected drawable (if any) virtual DrawableID getSoloNexusSelectedDrawableID( void ) { return m_soloNexusSelectedDrawableID; } ///< Return the one drawable of the nexus if only 1 angry mob is selected virtual Bool isDrawableSelected( DrawableID idToCheck ) const; ///< Return true if the selected ID is in the drawable list + virtual Bool areAllObjectsSelected(const std::vector& objectsToCheck) const; ///< Return true if all of the selected objects are in the drawable list virtual Bool isAnySelectedKindOf( KindOfType kindOf ) const; ///< is any selected object a kind of virtual Bool isAllSelectedKindOf( KindOfType kindOf ) const; ///< are all selected objects a kind of diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index 77de792aec6..ae7332dbc1a 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -3335,6 +3335,21 @@ Bool InGameUI::isDrawableSelected( DrawableID idToCheck ) const } // end isDrawableSelected +//------------------------------------------------------------------------------------------------- +/** Return true if all of the given objects are selected */ +//------------------------------------------------------------------------------------------------- +Bool InGameUI::areAllObjectsSelected(const std::vector& objectsToCheck) const +{ + for (std::vector::const_iterator it = objectsToCheck.begin(); it != objectsToCheck.end(); ++it) + { + if (!(*it)->getDrawable()->isSelected()) + return FALSE; + } + + return TRUE; + +} // end areAllObjectsSelected + // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ Bool InGameUI::isAnySelectedKindOf( KindOfType kindOf ) const diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp index 9998b745227..2a0b7486449 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp @@ -962,6 +962,7 @@ GameMessageDisposition SelectionTranslator::translateGameMessage(const GameMessa if (! TheGlobalData->m_useAlternateMouse || TheInGameUI->getPendingPlaceSourceObjectID() != INVALID_ID) { deselectAll(); + m_lastGroupSelGroup = -1; } } } @@ -1024,10 +1025,15 @@ GameMessageDisposition SelectionTranslator::translateGameMessage(const GameMessa m_lastGroupSelTime = now; } + Bool performSelection = TRUE; + // check for double-press to jump view if ( now - m_lastGroupSelTime < 20 && group == m_lastGroupSelGroup ) { DEBUG_LOG(("META: DOUBLETAP select team %d",group)); + // TheSuperHackers @bugfix Stubbjax 26/05/2025 Perform selection on double-press + // if the group or part of it is somehow deselected between presses. + performSelection = FALSE; Player *player = ThePlayerList->getLocalPlayer(); if (player) { @@ -1039,12 +1045,15 @@ GameMessageDisposition SelectionTranslator::translateGameMessage(const GameMessa if (numObjs > 0) { // if theres someone in the group, center the camera on them. - TheTacticalView->lookAt( objlist[numObjs-1]->getDrawable()->getPosition() ); + Drawable* drawable = objlist[numObjs - 1]->getDrawable(); + TheTacticalView->lookAt( drawable->getPosition() ); + performSelection = !TheInGameUI->areAllObjectsSelected( objlist ); } } } } - else + + if ( performSelection ) { TheInGameUI->deselectAllDrawables( false ); //No need to post message because we're just creating a new group! diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index 237cc09cb4c..74379ee9407 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -460,6 +460,7 @@ friend class Drawable; // for selection/deselection transactions virtual Drawable *getFirstSelectedDrawable( void ); ///< get the first selected drawable (if any) virtual DrawableID getSoloNexusSelectedDrawableID( void ) { return m_soloNexusSelectedDrawableID; } ///< Return the one drawable of the nexus if only 1 angry mob is selected virtual Bool isDrawableSelected( DrawableID idToCheck ) const; ///< Return true if the selected ID is in the drawable list + virtual Bool areAllObjectsSelected(const std::vector& objectsToCheck) const; ///< Return true if all of the selected objects are in the drawable list virtual Bool isAnySelectedKindOf( KindOfType kindOf ) const; ///< is any selected object a kind of virtual Bool isAllSelectedKindOf( KindOfType kindOf ) const; ///< are all selected objects a kind of diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 0801d31b05c..81e719ed45b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -3425,6 +3425,21 @@ Bool InGameUI::isDrawableSelected( DrawableID idToCheck ) const } // end isDrawableSelected +//------------------------------------------------------------------------------------------------- +/** Return true if all of the given objects are selected */ +//------------------------------------------------------------------------------------------------- +Bool InGameUI::areAllObjectsSelected(const std::vector& objectsToCheck) const +{ + for (std::vector::const_iterator it = objectsToCheck.begin(); it != objectsToCheck.end(); ++it) + { + if (!(*it)->getDrawable()->isSelected()) + return FALSE; + } + + return TRUE; + +} // end areAllObjectsSelected + // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ Bool InGameUI::isAnySelectedKindOf( KindOfType kindOf ) const diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp index e61b7684f18..e79a16a3f20 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/SelectionXlat.cpp @@ -954,6 +954,7 @@ GameMessageDisposition SelectionTranslator::translateGameMessage(const GameMessa if( !TheInGameUI->getPreventLeftClickDeselectionInAlternateMouseModeForOneClick() ) { deselectAll(); + m_lastGroupSelGroup = -1; } else { @@ -1099,10 +1100,15 @@ GameMessageDisposition SelectionTranslator::translateGameMessage(const GameMessa m_lastGroupSelTime = now; } + Bool performSelection = TRUE; + // check for double-press to jump view if ( now - m_lastGroupSelTime < 20 && group == m_lastGroupSelGroup ) { DEBUG_LOG(("META: DOUBLETAP select team %d",group)); + // TheSuperHackers @bugfix Stubbjax 26/05/2025 Perform selection on double-press + // if the group or part of it is somehow deselected between presses. + performSelection = FALSE; Player *player = ThePlayerList->getLocalPlayer(); if (player) { @@ -1114,12 +1120,15 @@ GameMessageDisposition SelectionTranslator::translateGameMessage(const GameMessa if (numObjs > 0) { // if theres someone in the group, center the camera on them. - TheTacticalView->lookAt( objlist[numObjs-1]->getDrawable()->getPosition() ); + Drawable* drawable = objlist[numObjs - 1]->getDrawable(); + TheTacticalView->lookAt( drawable->getPosition() ); + performSelection = !TheInGameUI->areAllObjectsSelected( objlist ); } } } } - else + + if ( performSelection ) { TheInGameUI->deselectAllDrawables( false ); //No need to post message because we're just creating a new group! From 3a58ec44bc5ff41cff9095d6c0286ae99c737ed2 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Sun, 3 Aug 2025 09:41:29 +0100 Subject: [PATCH 13/83] bugfix(object): Fix crash when an object is created with veterancy and an upgrade that applies a terrain decal (#1359) Most notably happens when spawning the USA Pilot with the Chemical Suits upgrade --- .../Code/GameEngine/Source/GameLogic/Object/Object.cpp | 7 ++++--- .../Code/GameEngine/Source/GameLogic/Object/Object.cpp | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 6ec11879111..82768829233 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -430,7 +430,11 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu m_soleHealingBenefactorID = INVALID_ID; ///< who is the only other object that can give me this non-stacking heal benefit? m_soleHealingBenefactorExpirationFrame = 0; ///< on what frame can I accept healing (thus to switch) from a new benefactor + // TheSuperHackers @bugfix Mauller/xezon 02/08/2025 sendObjectCreated needs calling before CreateModule's are initialized to prevent drawable related crashes + // This predominantly occurs with the veterancy create module when the chemical suits upgrade is unlocked as it tries to set the terrain decal. + // emit message announcing object's creation + TheGameLogic->sendObjectCreated( this ); } // end Object @@ -455,9 +459,6 @@ void Object::initObject() // If I have a valid team assigned, I can run through my Upgrade modules with his flags updateUpgradeModules(); - // emit message announcing object's creation - TheGameLogic->sendObjectCreated( this ); - //If the player has battle plans (America Strategy Center), then apply those bonuses //to this object if applicable. Internally it validates certain kinds of objects. const Player* controller = getControllingPlayer(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 370851e84ad..13fbed27052 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -499,7 +499,11 @@ Object::Object( const ThingTemplate *tt, const ObjectStatusMaskType &objectStatu m_soleHealingBenefactorID = INVALID_ID; ///< who is the only other object that can give me this non-stacking heal benefit? m_soleHealingBenefactorExpirationFrame = 0; ///< on what frame can I accept healing (thus to switch) from a new benefactor + // TheSuperHackers @bugfix Mauller/xezon 02/08/2025 sendObjectCreated needs calling before CreateModule's are initialized to prevent drawable related crashes + // This predominantly occurs with the veterancy create module when the chemical suits upgrade is unlocked as it tries to set the terrain decal. + // emit message announcing object's creation + TheGameLogic->sendObjectCreated( this ); } // end Object @@ -524,9 +528,6 @@ void Object::initObject() for (int i = 0; i < WEAPONSLOT_COUNT; ++i) m_lastWeaponCondition[i] = WSF_INVALID; - // emit message announcing object's creation - TheGameLogic->sendObjectCreated( this ); - // If I have a valid team assigned, I can run through my Upgrade modules with his flags updateUpgradeModules(); From 9c276512f491cccb9c07eb13ee76c14ff091b4b0 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:09:46 +0200 Subject: [PATCH 14/83] bugfix(cpudetect): Fix issues caused by broken memory benchmark value (#1409) --- .../Source/WWVegas/WWLib/CMakeLists.txt | 1 + .../Libraries/Source/WWVegas/WWLib/WWCommon.h | 25 +++++++++++ Core/Libraries/Source/WWVegas/WWLib/always.h | 2 + .../Source/WWVegas/WWLib/cpudetect.h | 45 ++++++------------- .../GameEngine/Include/Common/GameCommon.h | 1 + .../Code/GameEngine/Include/Common/GameLOD.h | 2 +- .../Code/GameEngine/Source/Common/GameLOD.cpp | 2 +- .../W3DDevice/GameClient/W3DShaderManager.h | 2 +- .../W3DDevice/GameClient/W3DShaderManager.cpp | 13 +++--- .../GameEngine/Include/Common/GameCommon.h | 1 + .../Code/GameEngine/Include/Common/GameLOD.h | 2 +- .../Code/GameEngine/Source/Common/GameLOD.cpp | 2 +- .../W3DDevice/GameClient/W3DShaderManager.h | 2 +- .../W3DDevice/GameClient/W3DShaderManager.cpp | 9 ++-- 14 files changed, 60 insertions(+), 49 deletions(-) create mode 100644 Core/Libraries/Source/WWVegas/WWLib/WWCommon.h diff --git a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt index 88a0c0b3ab0..4198e37e324 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt +++ b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt @@ -131,6 +131,7 @@ set(WWLIB_SRC widestring.cpp widestring.h win.h + WWCommon.h wwfile.cpp WWFILE.H wwstring.cpp diff --git a/Core/Libraries/Source/WWVegas/WWLib/WWCommon.h b/Core/Libraries/Source/WWVegas/WWLib/WWCommon.h new file mode 100644 index 00000000000..a4ed490128f --- /dev/null +++ b/Core/Libraries/Source/WWVegas/WWLib/WWCommon.h @@ -0,0 +1,25 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +#if defined(_MSC_VER) && _MSC_VER < 1300 +typedef unsigned MemValueType; +#else +typedef unsigned long long MemValueType; +#endif diff --git a/Core/Libraries/Source/WWVegas/WWLib/always.h b/Core/Libraries/Source/WWVegas/WWLib/always.h index 2c102bed476..56bb4a530e1 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/always.h +++ b/Core/Libraries/Source/WWVegas/WWLib/always.h @@ -40,6 +40,8 @@ #ifndef ALWAYS_H #define ALWAYS_H +#include "WWCommon.h" + #include #include diff --git a/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h b/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h index 9d88979c0fb..23318f65077 100644 --- a/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h +++ b/Core/Libraries/Source/WWVegas/WWLib/cpudetect.h @@ -194,22 +194,12 @@ class CPUDetectClass static unsigned Get_L1_Instruction_Trace_Cache_Set_Associative() { return L1InstructionTraceCacheSetAssociative; } // System memory - -#if defined(_MSC_VER) && _MSC_VER < 1300 - static unsigned Get_Total_Physical_Memory() { return TotalPhysicalMemory; } - static unsigned Get_Available_Physical_Memory() { return AvailablePhysicalMemory; } - static unsigned Get_Total_Page_File_Size() { return TotalPageMemory; } - static unsigned Get_Available_Page_File_Size() { return AvailablePageMemory; } - static unsigned Get_Total_Virtual_Memory() { return TotalVirtualMemory; } - static unsigned Get_Available_Virtual_Memory() { return AvailableVirtualMemory; } -#else - static unsigned long long Get_Total_Physical_Memory() { return TotalPhysicalMemory; } - static unsigned long long Get_Available_Physical_Memory() { return AvailablePhysicalMemory; } - static unsigned long long Get_Total_Page_File_Size() { return TotalPageMemory; } - static unsigned long long Get_Available_Page_File_Size() { return AvailablePageMemory; } - static unsigned long long Get_Total_Virtual_Memory() { return TotalVirtualMemory; } - static unsigned long long Get_Available_Virtual_Memory() { return AvailableVirtualMemory; } -#endif + static MemValueType Get_Total_Physical_Memory() { return TotalPhysicalMemory; } + static MemValueType Get_Available_Physical_Memory() { return AvailablePhysicalMemory; } + static MemValueType Get_Total_Page_File_Size() { return TotalPageMemory; } + static MemValueType Get_Available_Page_File_Size() { return AvailablePageMemory; } + static MemValueType Get_Total_Virtual_Memory() { return TotalVirtualMemory; } + static MemValueType Get_Available_Virtual_Memory() { return AvailableVirtualMemory; } static unsigned Get_Processor_Type() { return ProcessorType; } @@ -288,21 +278,14 @@ class CPUDetectClass // L1 instruction trace cache information static unsigned L1InstructionTraceCacheSize; static unsigned L1InstructionTraceCacheSetAssociative; -#if defined(_MSC_VER) && _MSC_VER < 1300 - static unsigned TotalPhysicalMemory; - static unsigned AvailablePhysicalMemory; - static unsigned TotalPageMemory; - static unsigned AvailablePageMemory; - static unsigned TotalVirtualMemory; - static unsigned AvailableVirtualMemory; -#else - static unsigned long long TotalPhysicalMemory; - static unsigned long long AvailablePhysicalMemory; - static unsigned long long TotalPageMemory; - static unsigned long long AvailablePageMemory; - static unsigned long long TotalVirtualMemory; - static unsigned long long AvailableVirtualMemory; -#endif + + static MemValueType TotalPhysicalMemory; + static MemValueType AvailablePhysicalMemory; + static MemValueType TotalPageMemory; + static MemValueType AvailablePageMemory; + static MemValueType TotalVirtualMemory; + static MemValueType AvailableVirtualMemory; + static unsigned OSVersionNumberMajor; static unsigned OSVersionNumberMinor; static unsigned OSVersionBuildNumber; diff --git a/Generals/Code/GameEngine/Include/Common/GameCommon.h b/Generals/Code/GameEngine/Include/Common/GameCommon.h index ce36e1c50a4..63e1a1f6491 100644 --- a/Generals/Code/GameEngine/Include/Common/GameCommon.h +++ b/Generals/Code/GameEngine/Include/Common/GameCommon.h @@ -52,6 +52,7 @@ // ---------------------------------------------------------------------------------------------- #include "Lib/BaseType.h" +#include "WWCommon.h" #include "Common/GameDefines.h" // ---------------------------------------------------------------------------------------------- diff --git a/Generals/Code/GameEngine/Include/Common/GameLOD.h b/Generals/Code/GameEngine/Include/Common/GameLOD.h index d726bf80868..46593f73ff5 100644 --- a/Generals/Code/GameEngine/Include/Common/GameLOD.h +++ b/Generals/Code/GameEngine/Include/Common/GameLOD.h @@ -213,7 +213,7 @@ class GameLODManager StaticGameLODLevel m_idealDetailLevel; ChipsetType m_videoChipType; CpuType m_cpuType; - Int m_numRAM; + MemValueType m_numRAM; Int m_cpuFreq; Real m_intBenchIndex; Real m_floatBenchIndex; diff --git a/Generals/Code/GameEngine/Source/Common/GameLOD.cpp b/Generals/Code/GameEngine/Source/Common/GameLOD.cpp index 530015ab727..9791161242a 100644 --- a/Generals/Code/GameEngine/Source/Common/GameLOD.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameLOD.cpp @@ -45,7 +45,7 @@ #define PROFILE_ERROR_LIMIT 0.94f //fraction of profiled result needed to get a match. Allows some room for error/fluctuation. //Hack to get access to a static method on the W3DDevice side. -MW -extern Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); +extern Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); GameLODManager *TheGameLODManager=NULL; diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h index 1d34c90ac66..db2b2c8dfbe 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h @@ -88,7 +88,7 @@ class W3DShaderManager /// Loads a .vso file and creates a vertex shader for it static HRESULT LoadAndCreateD3DShader(const char* strFilePath, const DWORD* pDeclaration, DWORD Usage, Bool ShaderType, DWORD* pHandle); - static Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); + static Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); static StaticGameLODLevel getGPUPerformanceIndex(void); static Real GetCPUBenchTime(void); diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp index e54c1553f6b..9df1c97c87a 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp @@ -71,7 +71,6 @@ #include "Common/GameLOD.h" #include "d3dx8tex.h" #include "dx8caps.h" -#include "Common/GameLOD.h" /** Interface definition for custom shaders we define in our app. These shaders can perform more complex @@ -2801,12 +2800,12 @@ HRESULT W3DShaderManager::LoadAndCreateD3DShader(const char* strFilePath, const #define MIN_ACCEPTED_TEXTURE_MEMORY (1024*1024*30) //30 MB /**Hack to give gameengine access to this function*/ -Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) +Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) { return W3DShaderManager::testMinimumRequirements(videoChipType,cpuType,cpuFreq,numRAM,intBenchIndex,floatBenchIndex,memBenchIndex); } -Bool W3DShaderManager::testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) +Bool W3DShaderManager::testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) { if (videoChipType) *videoChipType = getChipset(); @@ -2838,11 +2837,11 @@ Bool W3DShaderManager::testMinimumRequirements(ChipsetType *videoChipType, CpuTy if (intBenchIndex && floatBenchIndex && memBenchIndex) { - // TheSuperHackers @tweak Aliendroid1 19/06/2025 Legacy benchmarking code was removed. - // Since modern hardware always meets the minimum requirements, we preset the benchmark "results" to a high value. - *intBenchIndex = 10.0f; + // TheSuperHackers @tweak Aliendroid1 19/06/2025 Legacy benchmarking code was removed. + // Since modern hardware always meets the minimum requirements, we preset the benchmark "results" to a high value. + *intBenchIndex = 10.0f; *floatBenchIndex = 10.0f; - *memBenchIndex = 10.0f; + *memBenchIndex = 10.0f; } return TRUE; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameCommon.h b/GeneralsMD/Code/GameEngine/Include/Common/GameCommon.h index 1d40f77448d..a571f5af72b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameCommon.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameCommon.h @@ -58,6 +58,7 @@ // ---------------------------------------------------------------------------------------------- #include "Lib/BaseType.h" +#include "WWCommon.h" #include "Common/GameDefines.h" // ---------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameLOD.h b/GeneralsMD/Code/GameEngine/Include/Common/GameLOD.h index 23462eade48..bf9c88271cf 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameLOD.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameLOD.h @@ -214,7 +214,7 @@ class GameLODManager StaticGameLODLevel m_idealDetailLevel; ChipsetType m_videoChipType; CpuType m_cpuType; - Int m_numRAM; + MemValueType m_numRAM; Int m_cpuFreq; Real m_intBenchIndex; Real m_floatBenchIndex; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameLOD.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameLOD.cpp index 9627087bf08..8a3c79c7e4c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameLOD.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameLOD.cpp @@ -45,7 +45,7 @@ #define PROFILE_ERROR_LIMIT 0.94f //fraction of profiled result needed to get a match. Allows some room for error/fluctuation. //Hack to get access to a static method on the W3DDevice side. -MW -extern Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); +extern Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); GameLODManager *TheGameLODManager=NULL; diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h index 9817c428a67..7233b0a6859 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DShaderManager.h @@ -97,7 +97,7 @@ class W3DShaderManager /// Loads a .vso file and creates a vertex shader for it static HRESULT LoadAndCreateD3DShader(const char* strFilePath, const DWORD* pDeclaration, DWORD Usage, Bool ShaderType, DWORD* pHandle); - static Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); + static Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex); static StaticGameLODLevel getGPUPerformanceIndex(void); static Real GetCPUBenchTime(void); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp index 00d73fc29b3..4ee8e94ac66 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShaderManager.cpp @@ -72,7 +72,6 @@ #include "Common/GameLOD.h" #include "d3dx8tex.h" #include "dx8caps.h" -#include "Common/GameLOD.h" // Turn this on to turn off pixel shaders. jba[4/3/2003] @@ -3071,12 +3070,12 @@ HRESULT W3DShaderManager::LoadAndCreateD3DShader(const char* strFilePath, const #define MIN_ACCEPTED_TEXTURE_MEMORY (1024*1024*30) //30 MB /**Hack to give gameengine access to this function*/ -Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) +Bool testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) { return W3DShaderManager::testMinimumRequirements(videoChipType,cpuType,cpuFreq,numRAM,intBenchIndex,floatBenchIndex,memBenchIndex); } -Bool W3DShaderManager::testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, Int *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) +Bool W3DShaderManager::testMinimumRequirements(ChipsetType *videoChipType, CpuType *cpuType, Int *cpuFreq, MemValueType *numRAM, Real *intBenchIndex, Real *floatBenchIndex, Real *memBenchIndex) { if (videoChipType) *videoChipType = getChipset(); @@ -3108,8 +3107,8 @@ Bool W3DShaderManager::testMinimumRequirements(ChipsetType *videoChipType, CpuTy if (intBenchIndex && floatBenchIndex && memBenchIndex) { - // TheSuperHackers @tweak Aliendroid1 19/06/2025 Legacy benchmarking code was removed. - // Since modern hardware always meets the minimum requirements, we preset the benchmark "results" to a high value. + // TheSuperHackers @tweak Aliendroid1 19/06/2025 Legacy benchmarking code was removed. + // Since modern hardware always meets the minimum requirements, we preset the benchmark "results" to a high value. *intBenchIndex = 10.0f; *floatBenchIndex = 10.0f; *memBenchIndex = 10.0f; From f70794d1538acfb863878653296e3af9a1aaf206 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:35:37 +0200 Subject: [PATCH 15/83] fix(contain): Fix compile errors with RETAIL_COMPATIBLE_CRC 0 (#1411) --- .../Source/GameLogic/Object/Contain/OpenContain.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp b/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp index 3da8dc3d92c..41c2d63d1f5 100644 --- a/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp +++ b/Generals/Code/GameEngine/Source/GameLogic/Object/Contain/OpenContain.cpp @@ -1279,6 +1279,8 @@ void OpenContain::orderAllPassengersToExit( CommandSourceType commandSource ) //------------------------------------------------------------------------------------------------- void OpenContain::processDamageToContained() { + const OpenContainModuleData* data = getOpenContainModuleData(); + #if RETAIL_COMPATIBLE_CRC const ContainedItemsList* items = getContainedItemsList(); @@ -1292,7 +1294,7 @@ void OpenContain::processDamageToContained() Object *object = *it++; //Calculate the damage to be inflicted on each unit. - Real damage = object->getBodyModule()->getMaxHealth() * getOpenContainModuleData()->m_damagePercentageToUnits; + Real damage = object->getBodyModule()->getMaxHealth() * data->m_damagePercentageToUnits; DamageInfo damageInfo; damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE; @@ -1301,7 +1303,7 @@ void OpenContain::processDamageToContained() damageInfo.in.m_amount = damage; object->attemptDamage( &damageInfo ); - if( !object->isEffectivelyDead() && getOpenContainModuleData()->m_damagePercentageToUnits == 1.0f ) + if( !object->isEffectivelyDead() && data->m_damagePercentageToUnits == 1.0f ) object->kill(); // in case we are carrying flame proof troops we have been asked to kill // TheSuperHackers @info Calls to Object::attemptDamage and Object::kill will not remove @@ -1349,16 +1351,16 @@ void OpenContain::processDamageToContained() DEBUG_ASSERTCRASH( object, ("Contain list must not contain NULL element") ); // Calculate the damage to be inflicted on each unit. - Real damage = object->getBodyModule()->getMaxHealth() * percentDamage; + Real damage = object->getBodyModule()->getMaxHealth() * data->m_damagePercentageToUnits; DamageInfo damageInfo; damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE; - damageInfo.in.m_deathType = data->m_isBurnedDeathToUnits ? DEATH_BURNED : DEATH_NORMAL; + damageInfo.in.m_deathType = DEATH_BURNED; damageInfo.in.m_sourceID = getObject()->getID(); damageInfo.in.m_amount = damage; object->attemptDamage( &damageInfo ); - if( !object->isEffectivelyDead() && percentDamage == 1.0f ) + if( !object->isEffectivelyDead() && data->m_damagePercentageToUnits == 1.0f ) object->kill(); // in case we are carrying flame proof troops we have been asked to kill if ( object->isEffectivelyDead() ) From 45a02a4338f21f2c2043072fb9a2e3109397f7a1 Mon Sep 17 00:00:00 2001 From: Mauller <26652186+Mauller@users.noreply.github.com> Date: Sun, 3 Aug 2025 16:42:51 +0100 Subject: [PATCH 16/83] tweak(string): Implement functions in AsciiString, UnicodeString to return the byte count of the contained string (#1407) --- Core/GameEngine/Include/Common/AsciiString.h | 14 +++++++++++++- Core/GameEngine/Include/Common/UnicodeString.h | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Include/Common/AsciiString.h b/Core/GameEngine/Include/Common/AsciiString.h index 213f0481824..d1d004aa543 100644 --- a/Core/GameEngine/Include/Common/AsciiString.h +++ b/Core/GameEngine/Include/Common/AsciiString.h @@ -154,9 +154,14 @@ class AsciiString ~AsciiString(); /** - Return the length, in characters (not bytes!), of the string. + Return the length, in characters, of the string up to the first zero or null terminator. */ int getLength() const; + + /** + Return the number of bytes used by the string up to the first zero or null terminator. + */ + int getByteCount() const; /** Return true iff the length of the string is zero. Equivalent to (getLength() == 0) but slightly more efficient. @@ -391,6 +396,13 @@ inline int AsciiString::getLength() const return m_data ? strlen(peek()) : 0; } +// ----------------------------------------------------- +inline int AsciiString::getByteCount() const +{ + validate(); + return m_data ? getLength() : 0; +} + // ----------------------------------------------------- inline Bool AsciiString::isEmpty() const { diff --git a/Core/GameEngine/Include/Common/UnicodeString.h b/Core/GameEngine/Include/Common/UnicodeString.h index b138ab6929f..a2e008582e1 100644 --- a/Core/GameEngine/Include/Common/UnicodeString.h +++ b/Core/GameEngine/Include/Common/UnicodeString.h @@ -154,9 +154,14 @@ class UnicodeString ~UnicodeString(); /** - Return the length, in characters (not bytes!), of the string. + Return the length, in characters, of the string up to the first zero or null terminator. */ int getLength() const; + + /** + Return the number of bytes used by the string up to the first zero or null terminator. + */ + int getByteCount() const; /** Return true iff the length of the string is zero. Equivalent to (getLength() == 0) but slightly more efficient. @@ -341,6 +346,13 @@ inline int UnicodeString::getLength() const return m_data ? wcslen(peek()) : 0; } +// ----------------------------------------------------- +inline int UnicodeString::getByteCount() const +{ + validate(); + return m_data ? getLength() * sizeof(WideChar) : 0; +} + // ----------------------------------------------------- inline Bool UnicodeString::isEmpty() const { From abd1b90fbf4eb34ef8f4f99121fbc1d8bc812621 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 3 Aug 2025 17:45:21 +0200 Subject: [PATCH 17/83] chore(script): Add script to help move files to Core (#1398) --- scripts/cpp/unify_move_files.py | 135 ++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 scripts/cpp/unify_move_files.py diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py new file mode 100644 index 00000000000..3c6a05c81c0 --- /dev/null +++ b/scripts/cpp/unify_move_files.py @@ -0,0 +1,135 @@ +# Created with python 3.11.4 + +# This script helps with moving cpp files from Generals or GeneralsMD to Core + +import os +import shutil +from enum import Enum + + +class Game(Enum): + GENERALS = 0 + ZEROHOUR = 1 + CORE = 2 + + +class CmakeModifyType(Enum): + ADD_COMMENT = 0 + REMOVE_COMMENT = 1 + + +current_dir = os.path.dirname(os.path.abspath(__file__)) +root_dir = os.path.join(current_dir, "..", "..") +root_dir = os.path.normpath(root_dir) +core_dir = os.path.join(root_dir, "Core") +generals_dir = os.path.join(root_dir, "Generals", "Code") +generalsmd_dir = os.path.join(root_dir, "GeneralsMD", "Code") + + +def get_game_path(game: Game): + if game == Game.GENERALS: + return generals_dir + elif game == Game.ZEROHOUR: + return generalsmd_dir + elif game == Game.CORE: + return core_dir + assert(0) + + +def get_opposite_game(game: Game): + if game == Game.GENERALS: + return Game.ZEROHOUR + elif game == Game.ZEROHOUR: + return Game.GENERALS + assert(0) + + +def move_file(fromGame: Game, fromFile: str, toGame: Game, toFile: str): + fromPath = os.path.join(get_game_path(fromGame), os.path.normpath(fromFile)) + toPath = os.path.join(get_game_path(toGame), os.path.normpath(toFile)) + os.makedirs(os.path.dirname(toPath), exist_ok=True) + shutil.move(fromPath, toPath) + + +def delete_file(game: Game, path: str): + os.remove(os.path.join(get_game_path(game), os.path.normpath(path))) + + +def modify_cmakelists(cmakeFile: str, searchString: str, type: CmakeModifyType): + lines: list[str] + with open(cmakeFile, 'r', encoding="ascii") as file: + lines = file.readlines() + + with open(cmakeFile, 'w', encoding="ascii") as file: + for index, line in enumerate(lines): + if searchString in line: + if type == CmakeModifyType.ADD_COMMENT: + lines[index] = "#" + line + else: + lines[index] = line.replace("#", "", 1) + + file.writelines(lines) + + +def unify_file(fromGame: Game, fromFile: str, toGame: Game, toFile: str): + assert(toGame == Game.CORE) + + fromOppositeGame = get_opposite_game(fromGame) + fromOppositeGamePath = get_game_path(fromOppositeGame) + fromGamePath = get_game_path(fromGame) + toGamePath = get_game_path(toGame) + + fromFirstFolderIndex = fromFile.find("/") + toFirstFolderIndex = toFile.find("/") + assert(fromFirstFolderIndex > 0) + assert(toFirstFolderIndex > 0) + + fromFirstFolderName = fromFile[:fromFirstFolderIndex] + toFirstFolderName = toFile[:toFirstFolderIndex] + fromFileInCmake = fromFile[fromFirstFolderIndex+1:] + toFileInCmake = toFile[toFirstFolderIndex+1:] + + fromOppositeCmakeFile = os.path.join(fromOppositeGamePath, fromFirstFolderName, "CMakeLists.txt") + fromCmakeFile = os.path.join(fromGamePath, fromFirstFolderName, "CMakeLists.txt") + toCmakeFile = os.path.join(toGamePath, toFirstFolderName, "CMakeLists.txt") + + modify_cmakelists(fromOppositeCmakeFile, fromFileInCmake, CmakeModifyType.ADD_COMMENT) + modify_cmakelists(fromCmakeFile, fromFileInCmake, CmakeModifyType.ADD_COMMENT) + modify_cmakelists(toCmakeFile, toFileInCmake, CmakeModifyType.REMOVE_COMMENT) + + delete_file(fromOppositeGame, fromFile) + move_file(fromGame, fromFile, toGame, toFile) + + +def unify_move_file(fromGame: Game, fromFile: str, toGame: Game, toFile: str): + assert(toGame == Game.CORE) + + fromGamePath = get_game_path(fromGame) + toGamePath = get_game_path(toGame) + + fromFirstFolderIndex = fromFile.find("/") + toFirstFolderIndex = toFile.find("/") + assert(fromFirstFolderIndex > 0) + assert(toFirstFolderIndex > 0) + + fromFirstFolderName = fromFile[:fromFirstFolderIndex] + toFirstFolderName = toFile[:toFirstFolderIndex] + fromFileInCmake = fromFile[fromFirstFolderIndex+1:] + toFileInCmake = toFile[toFirstFolderIndex+1:] + + fromCmakeFile = os.path.join(fromGamePath, fromFirstFolderName, "CMakeLists.txt") + toCmakeFile = os.path.join(toGamePath, toFirstFolderName, "CMakeLists.txt") + + modify_cmakelists(fromCmakeFile, fromFileInCmake, CmakeModifyType.ADD_COMMENT) + modify_cmakelists(toCmakeFile, toFileInCmake, CmakeModifyType.REMOVE_COMMENT) + + move_file(fromGame, fromFile, toGame, toFile) + + +def main(): + + return + + +if __name__ == "__main__": + main() From 5efeb0e61de6f56e97e5794abf98993bdabd3a69 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:27:10 +0200 Subject: [PATCH 18/83] unify(crc): Move CRC files to Core (#1387) --- Core/GameEngine/CMakeLists.txt | 8 +- .../GameEngine/Include/Common/CRCDebug.h | 0 .../GameEngine/Include/Common/crc.h | 0 .../GameEngine/Source/Common/CRCDebug.cpp | 0 .../GameEngine/Source/Common/crc.cpp | 0 Generals/Code/GameEngine/CMakeLists.txt | 8 +- .../Code/GameEngine/Include/Common/CRCDebug.h | 133 -------- Generals/Code/GameEngine/Include/Common/crc.h | 143 -------- .../GameEngine/Source/Common/CRCDebug.cpp | 319 ------------------ .../Code/GameEngine/Source/Common/crc.cpp | 83 ----- GeneralsMD/Code/GameEngine/CMakeLists.txt | 8 +- scripts/cpp/unify_move_files.py | 5 + 12 files changed, 17 insertions(+), 690 deletions(-) rename {GeneralsMD/Code => Core}/GameEngine/Include/Common/CRCDebug.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Include/Common/crc.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/CRCDebug.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/crc.cpp (100%) delete mode 100644 Generals/Code/GameEngine/Include/Common/CRCDebug.h delete mode 100644 Generals/Code/GameEngine/Include/Common/crc.h delete mode 100644 Generals/Code/GameEngine/Source/Common/CRCDebug.cpp delete mode 100644 Generals/Code/GameEngine/Source/Common/crc.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 8a7110946d6..51d40af1d4f 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -21,8 +21,8 @@ set(GAMEENGINE_SRC # Include/Common/CDManager.h # Include/Common/ClientUpdateModule.h # Include/Common/CommandLine.h -# Include/Common/crc.h -# Include/Common/CRCDebug.h + Include/Common/crc.h + Include/Common/CRCDebug.h # Include/Common/CriticalSection.h # Include/Common/CustomMatchPreferences.h # Include/Common/DamageFX.h @@ -564,8 +564,8 @@ set(GAMEENGINE_SRC # Source/Common/Bezier/BezierSegment.cpp # Source/Common/BitFlags.cpp # Source/Common/CommandLine.cpp -# Source/Common/crc.cpp -# Source/Common/CRCDebug.cpp + Source/Common/crc.cpp + Source/Common/CRCDebug.cpp # Source/Common/DamageFX.cpp # Source/Common/Dict.cpp # Source/Common/DiscreteCircle.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/CRCDebug.h b/Core/GameEngine/Include/Common/CRCDebug.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/Common/CRCDebug.h rename to Core/GameEngine/Include/Common/CRCDebug.h diff --git a/GeneralsMD/Code/GameEngine/Include/Common/crc.h b/Core/GameEngine/Include/Common/crc.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/Common/crc.h rename to Core/GameEngine/Include/Common/crc.h diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CRCDebug.cpp b/Core/GameEngine/Source/Common/CRCDebug.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/CRCDebug.cpp rename to Core/GameEngine/Source/Common/CRCDebug.cpp diff --git a/GeneralsMD/Code/GameEngine/Source/Common/crc.cpp b/Core/GameEngine/Source/Common/crc.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/crc.cpp rename to Core/GameEngine/Source/Common/crc.cpp diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index c0d605965bd..d3f4e7379be 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -19,8 +19,8 @@ set(GAMEENGINE_SRC Include/Common/CDManager.h Include/Common/ClientUpdateModule.h Include/Common/CommandLine.h - Include/Common/crc.h - Include/Common/CRCDebug.h +# Include/Common/crc.h +# Include/Common/CRCDebug.h Include/Common/CriticalSection.h Include/Common/CustomMatchPreferences.h Include/Common/DamageFX.h @@ -523,8 +523,8 @@ set(GAMEENGINE_SRC Source/Common/Bezier/BezierSegment.cpp Source/Common/BitFlags.cpp Source/Common/CommandLine.cpp - Source/Common/crc.cpp - Source/Common/CRCDebug.cpp +# Source/Common/crc.cpp +# Source/Common/CRCDebug.cpp Source/Common/DamageFX.cpp Source/Common/Dict.cpp Source/Common/DiscreteCircle.cpp diff --git a/Generals/Code/GameEngine/Include/Common/CRCDebug.h b/Generals/Code/GameEngine/Include/Common/CRCDebug.h deleted file mode 100644 index 6a0d7e2f267..00000000000 --- a/Generals/Code/GameEngine/Include/Common/CRCDebug.h +++ /dev/null @@ -1,133 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// CRCDebug.h /////////////////////////////////////////////////////////////// -// Macros/functions/etc to help logging values for tracking down sync errors -// Author: Matthew D. Campbell, June 2002 - -// no #pragma once - we want to be able to conditionally define NO_DEBUG_CRC in indiv .cpp files - -#ifndef __CRCDEBUG_H__ -#define __CRCDEBUG_H__ - -#include "Common/Debug.h" - -#ifndef NO_DEBUG_CRC - #ifdef DEBUG_LOGGING - #define DEBUG_CRC - #endif -#endif - -#ifdef DEBUG_CRC - -#include "Common/AsciiString.h" -#include "GameLogic/GameLogic.h" -#include "Lib/BaseType.h" -#include "WWMath/vector3.h" -#include "WWMath/matrix3d.h" - - #define AS_INT(x) (*(Int *)(&x)) - #define DUMPVEL DUMPCOORD3DNAMED(&m_vel, "m_vel") - #define DUMPACCEL DUMPCOORD3DNAMED(&m_accel, "m_accel") - #define DUMPVECTOR3(x) DUMPVECTOR3NAMED(x, #x) - #define DUMPVECTOR3NAMED(x, y) dumpVector3(x, y, __FILE__, __LINE__) - #define DUMPCOORD3D(x) DUMPCOORD3DNAMED(x, #x) - #define DUMPCOORD3DNAMED(x, y) dumpCoord3D(x, y, __FILE__, __LINE__) - #define DUMPMATRIX3D(x) DUMPMATRIX3DNAMED(x, #x) - #define DUMPMATRIX3DNAMED(x, y) dumpMatrix3D(x, y, __FILE__, __LINE__) - #define DUMPREAL(x) DUMPREALNAMED(x, #x) - #define DUMPREALNAMED(x, y) dumpReal(x, y, __FILE__, __LINE__) - - extern Int TheCRCFirstFrameToLog; - extern UnsignedInt TheCRCLastFrameToLog; - - void dumpVector3(const Vector3 *v, AsciiString name, AsciiString fname, Int line); - void dumpCoord3D(const Coord3D *c, AsciiString name, AsciiString fname, Int line); - void dumpMatrix3D(const Matrix3D *m, AsciiString name, AsciiString fname, Int line); - void dumpReal(Real r, AsciiString name, AsciiString fname, Int line); - - void outputCRCDebugLines( void ); - void CRCDebugStartNewGame( void ); - void outputCRCDumpLines( void ); - - void addCRCDebugLine(const char *fmt, ...); - void addCRCDebugLineNoCounter(const char *fmt, ...); - void addCRCDumpLine(const char *fmt, ...); - void addCRCGenLine(const char *fmt, ...); - #define CRCDEBUG_LOG(x) addCRCDebugLine x - #define CRCDUMP_LOG(x) addCRCDumpLine x - #define CRCGEN_LOG(x) addCRCGenLine x - - class CRCVerification - { - public: - CRCVerification(); - ~CRCVerification(); - protected: - UnsignedInt m_startCRC; - }; - #define VERIFY_CRC CRCVerification crcVerification; - - extern Int lastCRCDebugFrame; - extern Int lastCRCDebugIndex; - - extern Bool g_verifyClientCRC; - extern Bool g_clientDeepCRC; - - extern Bool g_crcModuleDataFromClient; - extern Bool g_crcModuleDataFromLogic; - - extern Bool g_keepCRCSaves; - extern Bool g_saveDebugCRCPerFrame; - extern AsciiString g_saveDebugCRCPerFrameDir; - - extern Bool g_logObjectCRCs; - -#else // DEBUG_CRC - - #define DUMPVEL {} - #define DUMPACCEL {} - #define DUMPVECTOR3(x) {} - #define DUMPVECTOR3NAMED(x, y) {} - #define DUMPCOORD3D(x) {} - #define DUMPCOORD3DNAMED(x, y) {} - #define DUMPMATRIX3D(x) {} - #define DUMPMATRIX3DNAMED(x, y) {} - - #define DUMPREAL(x) {} - #define DUMPREALNAMED(x, y) {} - - #define CRCDEBUG_LOG(x) {} - #define CRCDUMP_LOG(x) {} - #define CRCGEN_LOG(x) {} - - #define VERIFY_CRC {} - -#endif - -extern Int NET_CRC_INTERVAL; -extern Int REPLAY_CRC_INTERVAL; -extern Bool TheDebugIgnoreSyncErrors; - -#endif // __CRCDEBUG_H__ \ No newline at end of file diff --git a/Generals/Code/GameEngine/Include/Common/crc.h b/Generals/Code/GameEngine/Include/Common/crc.h deleted file mode 100644 index 5b05c92f2ad..00000000000 --- a/Generals/Code/GameEngine/Include/Common/crc.h +++ /dev/null @@ -1,143 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// CRC.h /////////////////////////////////////////////////////////////// -// A class encapsulating CRC calculation -// Author: Matthew D. Campbell, October 2001 - -#pragma once - -#ifndef _CRC_H_ -#define _CRC_H_ - -#include "Lib/BaseType.h" - -#ifdef RTS_DEBUG - -//#include "winsock2.h" // for htonl - -class CRC -{ -public: - CRC() { crc = 0; } - - void computeCRC( const void *buf, Int len ); ///< Compute the CRC for a buffer, added into current CRC - void clear( void ) { crc = 0; } ///< Clears the CRC to 0 -// UnsignedInt get( void ) { return htonl(crc); } ///< Get the combined CRC - UnsignedInt get( void ); - -#if (defined(_MSC_VER) && _MSC_VER < 1300) && RETAIL_COMPATIBLE_CRC - void set( UnsignedInt v ) - { - crc = v; - } -#endif - -private: - void addCRC( UnsignedByte val ); ///< CRC a 4-byte block - - UnsignedInt crc; -}; - -#else - -// optimized inline only version -class CRC -{ -public: - CRC(void) { crc=0; } - - /// Compute the CRC for a buffer, added into current CRC - __forceinline void computeCRC( const void *buf, Int len ) - { - if (!buf||len<1) - return; - -#if !(defined(_MSC_VER) && _MSC_VER < 1300) - // C++ version left in for reference purposes - for (UnsignedByte *uintPtr=(UnsignedByte *)buf;len>0;len--,uintPtr++) - { - int hibit; - if (crc & 0x80000000) - { - hibit = 1; - } - else - { - hibit = 0; - } - - crc <<= 1; - crc += *uintPtr; - crc += hibit; - } -#else - // ASM version, verified by comparing resulting data with C++ version data - unsigned *crcPtr=&crc; - _asm - { - mov esi,[buf] - mov ecx,[len] - dec ecx - mov edi,[crcPtr] - mov ebx,dword ptr [edi] - xor eax,eax - lp: - mov al,byte ptr [esi] - shl ebx,1 - inc esi - adc ebx,eax - dec ecx - jns lp - mov dword ptr [edi],ebx - }; -#endif - } - - /// Clears the CRC to 0 - void clear( void ) - { - crc = 0; - } - - ///< Get the combined CRC - UnsignedInt get( void ) const - { - return crc; - } - -#if (defined(_MSC_VER) && _MSC_VER < 1300) && RETAIL_COMPATIBLE_CRC - void set( UnsignedInt v ) - { - crc = v; - } -#endif - -private: - UnsignedInt crc; -}; - -#endif - -#endif // _CRC_H_ diff --git a/Generals/Code/GameEngine/Source/Common/CRCDebug.cpp b/Generals/Code/GameEngine/Source/Common/CRCDebug.cpp deleted file mode 100644 index e1401c394ce..00000000000 --- a/Generals/Code/GameEngine/Source/Common/CRCDebug.cpp +++ /dev/null @@ -1,319 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - - -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - -#include "Common/CRCDebug.h" -#include "Common/Debug.h" -#include "Common/PerfTimer.h" -#include "Common/LocalFileSystem.h" -#include "GameClient/InGameUI.h" -#include "GameNetwork/IPEnumeration.h" -#include - - -#ifdef DEBUG_CRC - -static const Int MaxStrings = 64000; -static const Int MaxStringLen = 1024; - -static char DebugStrings[MaxStrings][MaxStringLen]; -static Int nextDebugString = 0; -static Int numDebugStrings = 0; -//static char DumpStrings[MaxStrings][MaxStringLen]; -//static Int nextDumpString = 0; -//static Int numDumpStrings = 0; - -#define IS_FRAME_OK_TO_LOG TheGameLogic->isInGame() && !TheGameLogic->isInShellGame() && !TheDebugIgnoreSyncErrors && \ - TheCRCFirstFrameToLog >= 0 && TheCRCFirstFrameToLog <= TheGameLogic->getFrame() \ - && TheGameLogic->getFrame() <= TheCRCLastFrameToLog - -CRCVerification::CRCVerification() -{ -#ifdef DEBUG_LOGGING -/**/ - if (g_verifyClientCRC && (IS_FRAME_OK_TO_LOG)) - { - m_startCRC = TheGameLogic->getCRC(CRC_RECALC, (g_clientDeepCRC)?"clientPre.crc":""); - } - else - { - m_startCRC = 0; - } -/**/ -#endif -} - -CRCVerification::~CRCVerification() -{ -#ifdef DEBUG_LOGGING -/**/ - UnsignedInt endCRC = 0; - if (g_verifyClientCRC && (IS_FRAME_OK_TO_LOG)) - { - endCRC = TheGameLogic->getCRC(CRC_RECALC, (g_clientDeepCRC)?"clientPost.crc":""); - } - DEBUG_ASSERTCRASH(!TheGameLogic->isInGame() || m_startCRC == endCRC, ("GameLogic changed outside of GameLogic::update() on frame %d!", TheGameLogic->getFrame())); - if (TheGameLogic->isInMultiplayerGame() && m_startCRC != endCRC) - { - if (TheInGameUI) - { - TheInGameUI->message(UnicodeString(L"GameLogic changed outside of GameLogic::update() - call Matt (x36804)!")); - } - CRCDEBUG_LOG(("GameLogic changed outside of GameLogic::update()!!!")); - } -/**/ -#endif -} - -void outputCRCDebugLines( void ) -{ - IPEnumeration ips; - AsciiString fname; - fname.format("crcDebug%s.txt", ips.getMachineName().str()); - FILE *fp = fopen(fname.str(), "wt"); - int start = 0; - int end = nextDebugString; - if (numDebugStrings >= MaxStrings) - start = nextDebugString - MaxStrings; - - for (Int i=start; iisInShellGame()) - return; - if (g_saveDebugCRCPerFrame) - { - // Create folder for frame data, if it doesn't exist yet. - CreateDirectory(g_saveDebugCRCPerFrameDir.str(), NULL); - - // Delete existing files - FilenameList files; - AsciiString dir = g_saveDebugCRCPerFrameDir; - dir.concat("/"); - TheLocalFileSystem->getFileListInDirectory(dir.str(), "", "DebugFrame_*.txt", files, FALSE); - FilenameList::iterator it; - for (it = files.begin(); it != files.end(); ++it) - { - DeleteFile(it->str()); - } - } - nextDebugString = 0; - numDebugStrings = 0; - lastCRCDebugFrame = 0; - lastCRCDebugIndex = 0; -} - -static void outputCRCDebugLinesPerFrame() -{ - if (!g_saveDebugCRCPerFrame || numDebugStrings == 0) - return; - AsciiString fname; - fname.format("%s/DebugFrame_%06d.txt", g_saveDebugCRCPerFrameDir.str(), lastCRCDebugFrame); - FILE *fp = fopen(fname.str(), "wt"); - int start = 0; - int end = nextDebugString; - if (numDebugStrings >= MaxStrings) - start = nextDebugString - MaxStrings; - nextDebugString = 0; - numDebugStrings = 0; - if (!fp) - return; - - for (Int i=start; i= MaxStrings) - start = nextDumpString - MaxStrings; - - for (Int i=start; igetFrame()) - { - outputCRCDebugLinesPerFrame(); - lastCRCDebugFrame = TheGameLogic->getFrame(); - lastCRCDebugIndex = 0; - } - - if (count) - sprintf(DebugStrings[nextDebugString], "%d:%05d ", TheGameLogic->getFrame(), lastCRCDebugIndex++); - else - DebugStrings[nextDebugString][0] = 0; - Int len = strlen(DebugStrings[nextDebugString]); - - vsnprintf(DebugStrings[nextDebugString]+len, MaxStringLen-len, fmt, args); - - char *tmp = DebugStrings[nextDebugString]; - while (tmp && *tmp) - { - if (*tmp == '\r' || *tmp == '\n') - { - *tmp = ' '; - } - ++tmp; - } - - //DEBUG_LOG(("%s", DebugStrings[nextDebugString])); - - ++nextDebugString; - ++numDebugStrings; - if (nextDebugString == MaxStrings) - nextDebugString = 0; -} - -void addCRCDebugLine(const char *fmt, ...) -{ - va_list args; - va_start(args, fmt); - addCRCDebugLineInternal(true, fmt, args); - va_end(args); -} - -void addCRCDebugLineNoCounter(const char *fmt, ...) -{ - // TheSuperHackers @feature helmutbuhler 04/09/2025 - // This version doesn't increase the lastCRCDebugIndex counter - // and can be used for logging lines that don't necessarily match up on all peers. - // (Otherwise the numbers would no longer match up and the diff would be very difficult to read) - va_list args; - va_start(args, fmt); - addCRCDebugLineInternal(false, fmt, args); - va_end(args); -} - -void addCRCGenLine(const char *fmt, ...) -{ - if (!(IS_FRAME_OK_TO_LOG)) - return; - - static char buf[MaxStringLen]; - va_list va; - va_start( va, fmt ); - vsnprintf(buf, MaxStringLen, fmt, va ); - va_end( va ); - addCRCDebugLine("%s", buf); - - //DEBUG_LOG(("%s", buf)); -} - -void addCRCDumpLine(const char *fmt, ...) -{ - /* - va_list va; - va_start( va, fmt ); - vsnprintf(DumpStrings[nextDumpString], MaxStringLen, fmt, va ); - va_end( va ); - - ++nextDumpString; - ++numDumpStrings; - if (nextDumpString == MaxStrings) - nextDumpString = 0; - */ -} - -void dumpVector3(const Vector3 *v, AsciiString name, AsciiString fname, Int line) -{ - if (!(IS_FRAME_OK_TO_LOG)) return; - fname.toLower(); - fname = getFname(fname); - addCRCDebugLine("dumpVector3() %s:%d %s %8.8X %8.8X %8.8X", - fname.str(), line, name.str(), - AS_INT(v->X), AS_INT(v->Y), AS_INT(v->Z)); -} - -void dumpCoord3D(const Coord3D *c, AsciiString name, AsciiString fname, Int line) -{ - if (!(IS_FRAME_OK_TO_LOG)) return; - fname.toLower(); - fname = getFname(fname); - addCRCDebugLine("dumpCoord3D() %s:%d %s %8.8X %8.8X %8.8X", - fname.str(), line, name.str(), - AS_INT(c->x), AS_INT(c->y), AS_INT(c->z)); -} - -void dumpMatrix3D(const Matrix3D *m, AsciiString name, AsciiString fname, Int line) -{ - if (!(IS_FRAME_OK_TO_LOG)) return; - fname.toLower(); - fname = getFname(fname); - const Real *matrix = (const Real *)m; - addCRCDebugLine("dumpMatrix3D() %s:%d %s", - fname.str(), line, name.str()); - for (Int i=0; i<3; ++i) - addCRCDebugLine(" 0x%08X 0x%08X 0x%08X 0x%08X", - AS_INT(matrix[(i<<2)+0]), AS_INT(matrix[(i<<2)+1]), AS_INT(matrix[(i<<2)+2]), AS_INT(matrix[(i<<2)+3])); -} - -void dumpReal(Real r, AsciiString name, AsciiString fname, Int line) -{ - if (!(IS_FRAME_OK_TO_LOG)) return; - fname.toLower(); - fname = getFname(fname); - addCRCDebugLine("dumpReal() %s:%d %s %8.8X (%f)", - fname.str(), line, name.str(), AS_INT(r), r); -} - -#endif // DEBUG_CRC diff --git a/Generals/Code/GameEngine/Source/Common/crc.cpp b/Generals/Code/GameEngine/Source/Common/crc.cpp deleted file mode 100644 index f3cbc369e2a..00000000000 --- a/Generals/Code/GameEngine/Source/Common/crc.cpp +++ /dev/null @@ -1,83 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - - -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - -#include "Common/crc.h" -#include "Common/Debug.h" - -#ifdef RTS_DEBUG - -void CRC::addCRC( UnsignedByte val ) -{ - int hibit; - - //cout << "\t\t" << hex << val; -// val = htonl(val); - //cout << " / " << hex << val < Date: Sun, 3 Aug 2025 21:36:48 +0100 Subject: [PATCH 19/83] feat(file): Implement functions in LocalFile for writing formatted strings to a file (#1414) --- Core/GameEngine/Include/Common/LocalFile.h | 4 ++- Core/GameEngine/Include/Common/RAMFile.h | 2 ++ Core/GameEngine/Include/Common/file.h | 8 +++++ .../Source/Common/System/LocalFile.cpp | 33 ++++++++++++++++++- .../Source/Common/System/RAMFile.cpp | 18 ++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Include/Common/LocalFile.h b/Core/GameEngine/Include/Common/LocalFile.h index 807ae612150..ad1dcf8ae96 100644 --- a/Core/GameEngine/Include/Common/LocalFile.h +++ b/Core/GameEngine/Include/Common/LocalFile.h @@ -55,7 +55,7 @@ #include "Common/file.h" #if USE_BUFFERED_IO -#include +#include "Utility/stdio_adapter.h" #endif //---------------------------------------------------------------------------- @@ -100,6 +100,8 @@ class LocalFile : public File virtual void close( void ); ///< Close the file virtual Int read( void *buffer, Int bytes ); ///< Read the specified number of bytes in to buffer: See File::read virtual Int write( const void *buffer, Int bytes ); ///< Write the specified number of bytes from the buffer: See File::write + virtual Int writeFormat( const Char* format, ... ); ///< Write an unterminated formatted string to the file + virtual Int writeFormat( const WideChar* format, ... ); ///< Write an unterminated formatted string to the file virtual Int seek( Int new_pos, seekMode mode = CURRENT ); ///< Set file position: See File::seek virtual Bool flush(); ///< flush data to disk virtual void nextLine(Char *buf = NULL, Int bufSize = 0); ///< moves file position to after the next new-line diff --git a/Core/GameEngine/Include/Common/RAMFile.h b/Core/GameEngine/Include/Common/RAMFile.h index 3ad6ca65e3a..7978b4e07d5 100644 --- a/Core/GameEngine/Include/Common/RAMFile.h +++ b/Core/GameEngine/Include/Common/RAMFile.h @@ -91,6 +91,8 @@ class RAMFile : public File virtual void close( void ); ///< Close the file virtual Int read( void *buffer, Int bytes ); ///< Read the specified number of bytes in to buffer: See File::read virtual Int write( const void *buffer, Int bytes ); ///< Write the specified number of bytes from the buffer: See File::write + virtual Int writeFormat( const Char* format, ... ); ///< Write the formatted string to the file + virtual Int writeFormat( const WideChar* format, ... ); ///< Write the formatted string to the file virtual Int seek( Int new_pos, seekMode mode = CURRENT ); ///< Set file position: See File::seek virtual Bool flush(); ///< flush data to disk virtual void nextLine(Char *buf = NULL, Int bufSize = 0); ///< moves current position to after the next new-line diff --git a/Core/GameEngine/Include/Common/file.h b/Core/GameEngine/Include/Common/file.h index 38714a7105d..7ca601c39d7 100644 --- a/Core/GameEngine/Include/Common/file.h +++ b/Core/GameEngine/Include/Common/file.h @@ -153,6 +153,14 @@ class File : public MemoryPoolObject * memory pointed at by buffer to the file. Returns the number of bytes written. * Returns -1 if an error occurred. */ + virtual Int writeFormat( const Char* format, ... ) = 0 ; /**< Write an unterminated formatted string to the file + * Returns the number of bytes written. + * Returns -1 if an error occurred. + */ + virtual Int writeFormat( const WideChar* format, ... ) = 0 ; /**< Write an unterminated formatted wide character string to the file + * Returns the number of bytes written. + * Returns -1 if an error occurred. + */ virtual Int seek( Int bytes, seekMode mode = CURRENT ) = 0; /**< Sets the file position of the next read/write operation. Returns the new file * position as the number of bytes from the start of the file. * Returns -1 if an error occurred. diff --git a/Core/GameEngine/Source/Common/System/LocalFile.cpp b/Core/GameEngine/Source/Common/System/LocalFile.cpp index 9edc2a44b6e..c1265f72adc 100644 --- a/Core/GameEngine/Source/Common/System/LocalFile.cpp +++ b/Core/GameEngine/Source/Common/System/LocalFile.cpp @@ -47,7 +47,6 @@ #include "PreRTS.h" -#include #include #include #include @@ -381,6 +380,38 @@ Int LocalFile::write( const void *buffer, Int bytes ) return ret; } +//================================================================= +// LocalFile::writeFormat - Ascii +//================================================================= + +Int LocalFile::writeFormat( const Char* format, ... ) +{ + char buffer[1024]; + + va_list args; + va_start(args, format); + Int length = vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + + return write( buffer, length ); +} + +//================================================================= +// LocalFile::writeFormat - Wide character +//================================================================= + +Int LocalFile::writeFormat( const WideChar* format, ... ) +{ + WideChar buffer[1024]; + + va_list args; + va_start(args, format); + Int length = vswprintf(buffer, sizeof(buffer) / sizeof(WideChar), format, args); + va_end(args); + + return write( buffer, length * sizeof(WideChar) ); +} + //================================================================= // LocalFile::seek //================================================================= diff --git a/Core/GameEngine/Source/Common/System/RAMFile.cpp b/Core/GameEngine/Source/Common/System/RAMFile.cpp index 0aa729fcf36..7c5646241dc 100644 --- a/Core/GameEngine/Source/Common/System/RAMFile.cpp +++ b/Core/GameEngine/Source/Common/System/RAMFile.cpp @@ -294,6 +294,24 @@ Int RAMFile::write( const void *buffer, Int bytes ) return -1; } +//================================================================= +// RAMFile::writeFormat - Ascii +//================================================================= + +Int RAMFile::writeFormat( const Char* format, ... ) +{ + return -1; +} + +//================================================================= +// RAMFile::writeFormat - Wide character +//================================================================= + +Int RAMFile::writeFormat( const WideChar* format, ... ) +{ + return -1; +} + //================================================================= // RAMFile::seek //================================================================= From aaac77db426d5d6b5112e98d8d32eb5b7f6cdefc Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 3 Aug 2025 22:41:47 +0200 Subject: [PATCH 20/83] unify(common): Move RandomValue files to Core (#1388) --- Core/GameEngine/CMakeLists.txt | 8 +- .../GameEngine/Include/Common/RandomValue.h | 0 .../Include/GameClient/ClientRandomValue.h | 0 .../Include/GameLogic/LogicRandomValue.h | 0 .../GameEngine/Source/Common/RandomValue.cpp | 0 Generals/Code/GameEngine/CMakeLists.txt | 8 +- .../GameEngine/Include/Common/RandomValue.h | 44 -- .../Include/GameClient/ClientRandomValue.h | 95 ---- .../Include/GameLogic/LogicRandomValue.h | 95 ---- .../GameEngine/Source/Common/RandomValue.cpp | 438 ------------------ GeneralsMD/Code/GameEngine/CMakeLists.txt | 8 +- scripts/cpp/unify_move_files.py | 5 + 12 files changed, 17 insertions(+), 684 deletions(-) rename {GeneralsMD/Code => Core}/GameEngine/Include/Common/RandomValue.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Include/GameClient/ClientRandomValue.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Include/GameLogic/LogicRandomValue.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/RandomValue.cpp (100%) delete mode 100644 Generals/Code/GameEngine/Include/Common/RandomValue.h delete mode 100644 Generals/Code/GameEngine/Include/GameClient/ClientRandomValue.h delete mode 100644 Generals/Code/GameEngine/Include/GameLogic/LogicRandomValue.h delete mode 100644 Generals/Code/GameEngine/Source/Common/RandomValue.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 51d40af1d4f..ed9b5774d63 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -92,7 +92,7 @@ set(GAMEENGINE_SRC # Include/Common/QuotedPrintable.h # Include/Common/Radar.h Include/Common/RAMFile.h -# Include/Common/RandomValue.h + Include/Common/RandomValue.h # Include/Common/Recorder.h # Include/Common/Registry.h Include/Common/ReplaySimulation.h @@ -143,7 +143,7 @@ set(GAMEENGINE_SRC # Include/GameClient/CDCheck.h # Include/GameClient/ChallengeGenerals.h # Include/GameClient/ClientInstance.h -# Include/GameClient/ClientRandomValue.h + Include/GameClient/ClientRandomValue.h # Include/GameClient/Color.h # Include/GameClient/CommandXlat.h # Include/GameClient/ControlBar.h @@ -252,7 +252,7 @@ set(GAMEENGINE_SRC # Include/GameLogic/GhostObject.h # Include/GameLogic/Locomotor.h # Include/GameLogic/LocomotorSet.h -# Include/GameLogic/LogicRandomValue.h + Include/GameLogic/LogicRandomValue.h # Include/GameLogic/Module/ActiveBody.h # Include/GameLogic/Module/ActiveShroudUpgrade.h # Include/GameLogic/Module/AIUpdate.h @@ -608,7 +608,7 @@ set(GAMEENGINE_SRC # Source/Common/NameKeyGenerator.cpp # Source/Common/PartitionSolver.cpp # Source/Common/PerfTimer.cpp -# Source/Common/RandomValue.cpp + Source/Common/RandomValue.cpp # Source/Common/Recorder.cpp Source/Common/ReplaySimulation.cpp # Source/Common/RTS/AcademyStats.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/RandomValue.h b/Core/GameEngine/Include/Common/RandomValue.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/Common/RandomValue.h rename to Core/GameEngine/Include/Common/RandomValue.h diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ClientRandomValue.h b/Core/GameEngine/Include/GameClient/ClientRandomValue.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/GameClient/ClientRandomValue.h rename to Core/GameEngine/Include/GameClient/ClientRandomValue.h diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/LogicRandomValue.h b/Core/GameEngine/Include/GameLogic/LogicRandomValue.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/GameLogic/LogicRandomValue.h rename to Core/GameEngine/Include/GameLogic/LogicRandomValue.h diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RandomValue.cpp b/Core/GameEngine/Source/Common/RandomValue.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/RandomValue.cpp rename to Core/GameEngine/Source/Common/RandomValue.cpp diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index d3f4e7379be..e694e0fe973 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -89,7 +89,7 @@ set(GAMEENGINE_SRC Include/Common/QuotedPrintable.h Include/Common/Radar.h # Include/Common/RAMFile.h - Include/Common/RandomValue.h +# Include/Common/RandomValue.h Include/Common/Recorder.h # Include/Common/ReplaySimulation.h Include/Common/Registry.h @@ -135,7 +135,7 @@ set(GAMEENGINE_SRC Include/GameClient/CampaignManager.h Include/GameClient/CDCheck.h Include/GameClient/ClientInstance.h - Include/GameClient/ClientRandomValue.h +# Include/GameClient/ClientRandomValue.h Include/GameClient/Color.h Include/GameClient/CommandXlat.h Include/GameClient/ControlBar.h @@ -240,7 +240,7 @@ set(GAMEENGINE_SRC Include/GameLogic/GhostObject.h Include/GameLogic/Locomotor.h Include/GameLogic/LocomotorSet.h - Include/GameLogic/LogicRandomValue.h +# Include/GameLogic/LogicRandomValue.h Include/GameLogic/Module/ActiveBody.h Include/GameLogic/Module/ActiveShroudUpgrade.h Include/GameLogic/Module/AIUpdate.h @@ -567,7 +567,7 @@ set(GAMEENGINE_SRC Source/Common/NameKeyGenerator.cpp Source/Common/PartitionSolver.cpp Source/Common/PerfTimer.cpp - Source/Common/RandomValue.cpp +# Source/Common/RandomValue.cpp Source/Common/Recorder.cpp Source/Common/RTS/ActionManager.cpp Source/Common/RTS/Energy.cpp diff --git a/Generals/Code/GameEngine/Include/Common/RandomValue.h b/Generals/Code/GameEngine/Include/Common/RandomValue.h deleted file mode 100644 index 422b9f00434..00000000000 --- a/Generals/Code/GameEngine/Include/Common/RandomValue.h +++ /dev/null @@ -1,44 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// RandomValue.h -// Random number generation system -// Author: Michael S. Booth, January 1998 - -#pragma once - -#ifndef _RANDOM_VALUE_H_ -#define _RANDOM_VALUE_H_ - -#include "Lib/BaseType.h" - -extern void InitRandom( void ); -extern void InitRandom( UnsignedInt seed ); -extern void InitGameLogicRandom( UnsignedInt seed ); ///< Set the GameLogic seed to a known value at game start -extern UnsignedInt GetGameLogicRandomSeed( void ); ///< Get the seed (used for replays) -extern UnsignedInt GetGameLogicRandomSeedCRC( void );///< Get the seed (used for CRCs) - -//-------------------------------------------------------------------------------------------------------------- - -#endif // _RANDOM_VALUE_H_ diff --git a/Generals/Code/GameEngine/Include/GameClient/ClientRandomValue.h b/Generals/Code/GameEngine/Include/GameClient/ClientRandomValue.h deleted file mode 100644 index d821f74f928..00000000000 --- a/Generals/Code/GameEngine/Include/GameClient/ClientRandomValue.h +++ /dev/null @@ -1,95 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// ClientRandomValue.h -// Random number generation system -// Author: Michael S. Booth, January 1998 -// Split out into separate Logic/Client/Audio headers by MDC Sept 2002 - -#pragma once - -#ifndef _CLIENT_RANDOM_VALUE_H_ -#define _CLIENT_RANDOM_VALUE_H_ - -#include "Lib/BaseType.h" - -// do NOT use these functions directly, rather use the macros below -extern Int GetGameClientRandomValue( int lo, int hi, const char *file, int line ); -extern Real GetGameClientRandomValueReal( Real lo, Real hi, const char *file, int line ); - -// use these macros to access the random value functions -#define GameClientRandomValue( lo, hi ) GetGameClientRandomValue( lo, hi, __FILE__, __LINE__ ) -#define GameClientRandomValueReal( lo, hi ) GetGameClientRandomValueReal( lo, hi, __FILE__, __LINE__ ) - -//-------------------------------------------------------------------------------------------------------------- -class CColorAlphaDialog; -class DebugWindowDialog; - -/** - * A GameClientRandomVariable represents a distribution of random values - * from which discrete values can be retrieved. - */ -class GameClientRandomVariable -{ -public: - // NOTE: This class cannot have a constructor or destructor due to its use within unions - - /** - * CONSTANT represents a single, constant, value. - * UNIFORM represents a uniform distribution of random values. - * GAUSSIAN represents a normally distributed set of random values. - * TRIANGULAR represents a distribution of random values in the shape - * of a triangle, with the peak probability midway between low and high. - * LOW_BIAS represents a distribution of random values with - * maximum probability at low, and zero probability at high. - * HIGH_BIAS represents a distribution of random values with - * zero probability at low, and maximum probability at high. - */ - enum DistributionType - { - CONSTANT, UNIFORM, GAUSSIAN, TRIANGULAR, LOW_BIAS, HIGH_BIAS - }; - - static const char *DistributionTypeNames[]; - - /// define the range of random values, and the distribution of values - void setRange( Real low, Real high, DistributionType type = UNIFORM ); - - Real getValue( void ) const; ///< return a value from the random distribution - inline Real getMinimumValue( void ) const { return m_low; } - inline Real getMaximumValue( void ) const { return m_high; } - inline DistributionType getDistributionType( void ) const { return m_type; } -protected: - DistributionType m_type; ///< the kind of random distribution - Real m_low, m_high; ///< the range of random values - - // These two friends are for particle editing. - friend CColorAlphaDialog; - friend DebugWindowDialog; - -}; - -//-------------------------------------------------------------------------------------------------------------- - -#endif // _CLIENT_RANDOM_VALUE_H_ diff --git a/Generals/Code/GameEngine/Include/GameLogic/LogicRandomValue.h b/Generals/Code/GameEngine/Include/GameLogic/LogicRandomValue.h deleted file mode 100644 index 01022e8dbb3..00000000000 --- a/Generals/Code/GameEngine/Include/GameLogic/LogicRandomValue.h +++ /dev/null @@ -1,95 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// LogicRandomValue.h -// Random number generation system -// Author: Michael S. Booth, January 1998 -// Split out into separate Logic/Client/Audio headers by MDC Sept 2002 - -#pragma once - -#ifndef _LOGIC_RANDOM_VALUE_H_ -#define _LOGIC_RANDOM_VALUE_H_ - -#include "Lib/BaseType.h" - -// do NOT use these functions directly, rather use the macros below -extern Int GetGameLogicRandomValue( int lo, int hi, const char *file, int line ); -extern Real GetGameLogicRandomValueReal( Real lo, Real hi, const char *file, int line ); - -// use these macros to access the random value functions -#define GameLogicRandomValue( lo, hi ) GetGameLogicRandomValue( lo, hi, __FILE__, __LINE__ ) -#define GameLogicRandomValueReal( lo, hi ) GetGameLogicRandomValueReal( lo, hi, __FILE__, __LINE__ ) - -//-------------------------------------------------------------------------------------------------------------- -class CColorAlphaDialog; -class DebugWindowDialog; - -/** - * A GameLogicRandomVariable represents a distribution of random values - * from which discrete values can be retrieved. - */ -class GameLogicRandomVariable -{ -public: - // NOTE: This class cannot have a constructor or destructor due to its use within unions - - /** - * CONSTANT represents a single, constant, value. - * UNIFORM represents a uniform distribution of random values. - * GAUSSIAN represents a normally distributed set of random values. - * TRIANGULAR represents a distribution of random values in the shape - * of a triangle, with the peak probability midway between low and high. - * LOW_BIAS represents a distribution of random values with - * maximum probability at low, and zero probability at high. - * HIGH_BIAS represents a distribution of random values with - * zero probability at low, and maximum probability at high. - */ - enum DistributionType - { - CONSTANT, UNIFORM, GAUSSIAN, TRIANGULAR, LOW_BIAS, HIGH_BIAS - }; - - static const char *DistributionTypeNames[]; - - /// define the range of random values, and the distribution of values - void setRange( Real low, Real high, DistributionType type = UNIFORM ); - - Real getValue( void ) const; ///< return a value from the random distribution - inline Real getMinimumValue( void ) const { return m_low; } - inline Real getMaximumValue( void ) const { return m_high; } - inline DistributionType getDistributionType( void ) const { return m_type; } -protected: - DistributionType m_type; ///< the kind of random distribution - Real m_low, m_high; ///< the range of random values - - // These two friends are for particle editing. - friend CColorAlphaDialog; - friend DebugWindowDialog; - -}; - -//-------------------------------------------------------------------------------------------------------------- - -#endif // _LOGIC_RANDOM_VALUE_H_ diff --git a/Generals/Code/GameEngine/Source/Common/RandomValue.cpp b/Generals/Code/GameEngine/Source/Common/RandomValue.cpp deleted file mode 100644 index b57546df51d..00000000000 --- a/Generals/Code/GameEngine/Source/Common/RandomValue.cpp +++ /dev/null @@ -1,438 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// RandomValue.cpp -// Pseudo-random number generators -// Author: Michael S. Booth, January 1998 - -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - - -#include "Lib/BaseType.h" -#include "Common/RandomValue.h" -#include "Common/crc.h" -#include "Common/Debug.h" -#include "GameLogic/GameLogic.h" - -//#define DETERMINISTIC // to allow repetition for debugging - - -#undef DEBUG_RANDOM_CLIENT -#undef DEBUG_RANDOM_LOGIC -#undef DEBUG_RANDOM_AUDIO - -//#define DEBUG_RANDOM_CLIENT -//#define DEBUG_RANDOM_LOGIC -//#define DEBUG_RANDOM_AUDIO - -static const Real theMultFactor = 1.0f / (powf(2, 8 * sizeof(UnsignedInt)) - 1.0f); - -// Initial seed values. -static UnsignedInt theGameClientSeed[6] = -{ - 0xf22d0e56L, 0x883126e9L, 0xc624dd2fL, 0x702c49cL, 0x9e353f7dL, 0x6fdf3b64L -}; - -static UnsignedInt theGameAudioSeed[6] = -{ - 0xf22d0e56L, 0x883126e9L, 0xc624dd2fL, 0x702c49cL, 0x9e353f7dL, 0x6fdf3b64L -}; - -static UnsignedInt theGameLogicBaseSeed = 0; -static UnsignedInt theGameLogicSeed[6] = -{ - 0xf22d0e56L, 0x883126e9L, 0xc624dd2fL, 0x702c49cL, 0x9e353f7dL, 0x6fdf3b64L -}; - -// Add with carry. SUM is replaced with A + B + C, C is replaced with 1 if there was a carry, 0 if there wasn't. A carry occurred if the sum is less than one of the inputs. This is addition, so carry can never be more than one. -#define ADC(SUM, A, B, C) SUM = (A) + (B) + (C); C = ((SUM < (A)) || (SUM < (B))) - -static UnsignedInt randomValue(UnsignedInt *seed) -{ - UnsignedInt ax; - UnsignedInt c = 0; - - - ADC(ax, seed[5], seed[4], c); /* mov ax,seed+20 */ - /* add ax,seed+16 */ - seed[4] = ax; /* mov seed+8,ax */ - - ADC(ax, ax, seed[3], c); /* adc ax,seed+12 */ - seed[3] = ax; /* mov seed+12,ax */ - - ADC(ax, ax, seed[2], c); /* adc ax,seed+8 */ - seed[2] = ax; /* mov seed+8,ax */ - - ADC(ax, ax, seed[1], c); /* adc ax,seed+4 */ - seed[1] = ax; /* mov seed+4,ax */ - - ADC(ax, ax, seed[0], c); /* adc ax,seed+0 */ - seed[0] = ax; /* mov seed+0,ax */ - - /* Increment seed array, bubbling up the carries. */ - if (!++seed[5]) - { - if (!++seed[4]) - { - if (!++seed[3]) - { - if (!++seed[2]) - { - if (!++seed[1]) - { - ++seed[0]; - ++ax; - } - } - } - } - } - return(ax); -} - -static void seedRandom(UnsignedInt SEED, UnsignedInt *seed) -{ - UnsignedInt ax; - - ax = SEED; /* mov eax,SEED */ - ax += 0xf22d0e56; /* add eax,0f22d0e56h */ - seed[0] = ax; /* mov seed,eax */ - ax += 0x883126e9 - 0xf22d0e56; /* add eax,0883126e9h-0f22d0e56h */ - seed[1] = ax; /* mov seed+4,eax */ - ax += 0xc624dd2f - 0x883126e9; /* add eax,0c624dd2fh-0883126e9h */ - seed[2] = ax; /* mov seed+8,eax */ - ax += 0x0702c49c - 0xc624dd2f; /* add eax,00702c49ch-0c624dd2fh */ - seed[3] = ax; /* mov seed+12,eax */ - ax += 0x9e353f7d - 0x0702c49c; /* add eax,09e353f7dh-00702c49ch */ - seed[4] = ax; /* mov seed+16,eax */ - ax += 0x6fdf3b64 - 0x9e353f7d; /* add eax,06fdf3b64h-09e353f7dh */ - seed[5] = ax; /* mov seed+20,eax */ -} - -// -// It is necessary to separate the GameClient and GameLogic usage of random -// values to ensure that the GameLogic remains deterministic, regardless -// of the effects displayed on the GameClient. -// - -UnsignedInt GetGameLogicRandomSeed( void ) -{ - return theGameLogicBaseSeed; -} - -UnsignedInt GetGameLogicRandomSeedCRC( void ) -{ - CRC c; - c.computeCRC(theGameLogicSeed, 6*sizeof(UnsignedInt)); - return c.get(); -} - -void InitRandom( void ) -{ -#ifdef DETERMINISTIC - // needs to be the same every time - seedRandom(0, theGameClientSeed); - seedRandom(0, theGameAudioSeed); - seedRandom(0, theGameLogicSeed); - theGameLogicBaseSeed = 0; -#else - time_t seconds = time( NULL ); - - seedRandom(seconds, theGameAudioSeed); - seedRandom(seconds, theGameClientSeed); - seedRandom(seconds, theGameLogicSeed); - theGameLogicBaseSeed = seconds; -#endif -} - -void InitRandom( UnsignedInt seed ) -{ - seedRandom(seed, theGameAudioSeed); - seedRandom(seed, theGameClientSeed); - seedRandom(seed, theGameLogicSeed); - theGameLogicBaseSeed = seed; -#ifdef DEBUG_RANDOM_LOGIC -DEBUG_LOG(( "InitRandom %08lx",seed)); -#endif -} - -void InitGameLogicRandom( UnsignedInt seed ) -{ -#ifdef DETERMINISTIC - // needs to be the same every time - seedRandom(0, theGameLogicSeed); - theGameLogicBaseSeed = 0; -#else - seedRandom(seed, theGameLogicSeed); - theGameLogicBaseSeed = seed; -#endif -#ifdef DEBUG_RANDOM_LOGIC -DEBUG_LOG(( "InitRandom Logic %08lx",seed)); -#endif -} - -// -// Integer random value -// -Int GetGameLogicRandomValue( int lo, int hi, const char *file, int line ) -{ - //Int delta = hi - lo + 1; - //Int rval; - - //if (delta == 0) - //return hi; - - //rval = ((Int)(randomValue(theGameLogicSeed) % delta)) + lo; - - UnsignedInt delta = hi - lo + 1; - //UnsignedInt temp; - Int rval; - - if (delta == 0) - return hi; - - rval = ((Int)(randomValue(theGameLogicSeed) % delta)) + lo; - //temp = randomValue(theGameLogicSeed); - //temp = temp % delta; - - //rval = temp + lo; - -/**/ -#ifdef DEBUG_RANDOM_LOGIC -DEBUG_LOG(( "%d: GetGameLogicRandomValue = %d (%d - %d), %s line %d", - TheGameLogic->getFrame(), rval, lo, hi, file, line )); -#endif -/**/ - - return rval; -} - -// -// Integer random value -// -Int GetGameClientRandomValue( int lo, int hi, const char *file, int line ) -{ - UnsignedInt delta = hi - lo + 1; - Int rval; - - if (delta == 0) - return hi; - - rval = ((Int)(randomValue(theGameClientSeed) % delta)) + lo; - -/**/ -#ifdef DEBUG_RANDOM_CLIENT -DEBUG_LOG(( "%d: GetGameClientRandomValue = %d (%d - %d), %s line %d", - TheGameLogic ? TheGameLogic->getFrame() : -1, rval, lo, hi, file, line )); -#endif -/**/ - - return rval; -} - -// -// Integer random value -// -Int GetGameAudioRandomValue( int lo, int hi, const char *file, int line ) -{ - UnsignedInt delta = hi - lo + 1; - Int rval; - - if (delta == 0) - return hi; - - rval = ((Int)(randomValue(theGameAudioSeed) % delta)) + lo; - -/**/ -#ifdef DEBUG_RANDOM_AUDIO -DEBUG_LOG(( "%d: GetGameAudioRandomValue = %d (%d - %d), %s line %d", - TheGameLogic->getFrame(), rval, lo, hi, file, line )); -#endif -/**/ - - return rval; -} - -// -// Real valued random value -// -Real GetGameLogicRandomValueReal( Real lo, Real hi, const char *file, int line ) -{ - Real delta = hi - lo; - Real rval; - - if (delta <= 0.0f) - return hi; - - rval = ((Real)(randomValue(theGameLogicSeed)) * theMultFactor ) * delta + lo; - - DEBUG_ASSERTCRASH( rval >= lo && rval <= hi, ("Bad random val")); -/**/ -#ifdef DEBUG_RANDOM_LOGIC -DEBUG_LOG(( "%d: GetGameLogicRandomValueReal = %f, %s line %d", - TheGameLogic->getFrame(), rval, file, line )); -#endif -/**/ - - return rval; -} - -// -// Real valued random value -// -Real GetGameClientRandomValueReal( Real lo, Real hi, const char *file, int line ) -{ - Real delta = hi - lo; - Real rval; - - if (delta <= 0.0f) - return hi; - - rval = ((Real)(randomValue(theGameClientSeed)) * theMultFactor ) * delta + lo; - - DEBUG_ASSERTCRASH( rval >= lo && rval <= hi, ("Bad random val")); -/**/ -#ifdef DEBUG_RANDOM_CLIENT -DEBUG_LOG(( "%d: GetGameClientRandomValueReal = %f, %s line %d", - TheGameLogic->getFrame(), rval, file, line )); -#endif -/**/ - - return rval; -} - -// -// Real valued random value -// -Real GetGameAudioRandomValueReal( Real lo, Real hi, const char *file, int line ) -{ - Real delta = hi - lo; - Real rval; - - if (delta <= 0.0f) - return hi; - - rval = ((Real)(randomValue(theGameAudioSeed)) * theMultFactor ) * delta + lo; - - DEBUG_ASSERTCRASH( rval >= lo && rval <= hi, ("Bad random val")); -/**/ -#ifdef DEBUG_RANDOM_AUDIO -DEBUG_LOG(( "%d: GetGameAudioRandomValueReal = %f, %s line %d", - TheGameLogic->getFrame(), rval, file, line )); -#endif -/**/ - - return rval; -} - -//-------------------------------------------------------------------------------------------------------------- -// GameClientRandomVariable -// - -/*static*/ const char *GameClientRandomVariable::DistributionTypeNames[] = -{ - "CONSTANT", "UNIFORM", "GAUSSIAN", "TRIANGULAR", "LOW_BIAS", "HIGH_BIAS" -}; - -/** - define the range of random values, and the distribution of values -*/ -void GameClientRandomVariable::setRange( Real low, Real high, DistributionType type ) -{ - DEBUG_ASSERTCRASH(!(m_type == CONSTANT && m_low != m_high), ("CONSTANT GameClientRandomVariables should have low == high")); - m_low = low; - m_high = high; - m_type = type; -} - -/** - * Return a value from the random distribution - */ -Real GameClientRandomVariable::getValue( void ) const -{ - switch( m_type ) - { - case CONSTANT: - DEBUG_ASSERTLOG(m_low == m_high, ("m_low != m_high for a CONSTANT GameClientRandomVariable")); - if (m_low == m_high) { - return m_low; - } // else return as though a UNIFORM. - FALLTHROUGH; - - case UNIFORM: - return GameClientRandomValueReal( m_low, m_high ); - - default: - /// @todo fill in support for nonuniform GameClientRandomVariables. - DEBUG_CRASH(("unsupported DistributionType in GameClientRandomVariable::getValue")); - return 0.0f; - } -} - - -//-------------------------------------------------------------------------------------------------------------- -// GameLogicRandomVariable -// - -/*static*/ const char *GameLogicRandomVariable::DistributionTypeNames[] = -{ - "CONSTANT", "UNIFORM", "GAUSSIAN", "TRIANGULAR", "LOW_BIAS", "HIGH_BIAS" -}; - -/** - define the range of random values, and the distribution of values -*/ -void GameLogicRandomVariable::setRange( Real low, Real high, DistributionType type ) -{ - DEBUG_ASSERTCRASH(!(m_type == CONSTANT && m_low != m_high), ("CONSTANT GameLogicRandomVariables should have low == high")); - m_low = low; - m_high = high; - m_type = type; -} - -/** - * Return a value from the random distribution - */ -Real GameLogicRandomVariable::getValue( void ) const -{ - switch( m_type ) - { - case CONSTANT: - DEBUG_ASSERTLOG(m_low == m_high, ("m_low != m_high for a CONSTANT GameLogicRandomVariable")); - if (m_low == m_high) { - return m_low; - } // else return as though a UNIFORM. - FALLTHROUGH; - - case UNIFORM: - return GameLogicRandomValueReal( m_low, m_high ); - - default: - /// @todo fill in support for nonuniform GameLogicRandomVariables. - DEBUG_CRASH(("unsupported DistributionType in GameLogicRandomVariable::getValue")); - return 0.0f; - } -} - - diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 966f525c55a..f8d493bded2 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -92,7 +92,7 @@ set(GAMEENGINE_SRC Include/Common/QuotedPrintable.h Include/Common/Radar.h # Include/Common/RAMFile.h - Include/Common/RandomValue.h +# Include/Common/RandomValue.h Include/Common/Recorder.h # Include/Common/ReplaySimulation.h Include/Common/Registry.h @@ -142,7 +142,7 @@ set(GAMEENGINE_SRC Include/GameClient/CDCheck.h Include/GameClient/ChallengeGenerals.h Include/GameClient/ClientInstance.h - Include/GameClient/ClientRandomValue.h +# Include/GameClient/ClientRandomValue.h Include/GameClient/Color.h Include/GameClient/CommandXlat.h Include/GameClient/ControlBar.h @@ -251,7 +251,7 @@ set(GAMEENGINE_SRC Include/GameLogic/GhostObject.h Include/GameLogic/Locomotor.h Include/GameLogic/LocomotorSet.h - Include/GameLogic/LogicRandomValue.h +# Include/GameLogic/LogicRandomValue.h Include/GameLogic/Module/ActiveBody.h Include/GameLogic/Module/ActiveShroudUpgrade.h Include/GameLogic/Module/AIUpdate.h @@ -607,7 +607,7 @@ set(GAMEENGINE_SRC Source/Common/NameKeyGenerator.cpp Source/Common/PartitionSolver.cpp Source/Common/PerfTimer.cpp - Source/Common/RandomValue.cpp +# Source/Common/RandomValue.cpp Source/Common/Recorder.cpp # Source/Common/ReplaySimulation.cpp Source/Common/RTS/AcademyStats.cpp diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index 79ba3dcd6e4..c6c62bd9205 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -133,6 +133,11 @@ def main(): #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/crc.cpp", Game.CORE, "GameEngine/Source/Common/crc.cpp") #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/CRCDebug.cpp", Game.CORE, "GameEngine/Source/Common/CRCDebug.cpp") + #unify_file(Game.ZEROHOUR, "GameEngine/Include/Common/RandomValue.h", Game.CORE, "GameEngine/Include/Common/RandomValue.h") + #unify_file(Game.ZEROHOUR, "GameEngine/Include/GameClient/ClientRandomValue.h", Game.CORE, "GameEngine/Include/GameClient/ClientRandomValue.h") + #unify_file(Game.ZEROHOUR, "GameEngine/Include/GameLogic/LogicRandomValue.h", Game.CORE, "GameEngine/Include/GameLogic/LogicRandomValue.h") + #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/RandomValue.cpp", Game.CORE, "GameEngine/Source/Common/RandomValue.cpp") + return From 8c52b6aee9adbd002c14504f16bb1fd66bc14954 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:37:29 +0200 Subject: [PATCH 21/83] unify(debug): Merge GameEngine's Debug code (#1384) Generals inherits the following changes: 1. The ignore button is now default selected on assert dialogs (simply press ENTER to skip) 2. Debug now works in Generals World Builder 3. The fullscreen game will now hide on release crash 4. The German serious error message is removed --- .../Code/GameEngine/Include/Common/Debug.h | 14 +++++--- .../GameEngine/Source/Common/System/Debug.cpp | 32 +++++++++++-------- .../GameEngine/Source/Common/System/Debug.cpp | 10 ++---- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/Debug.h b/Generals/Code/GameEngine/Include/Common/Debug.h index e65118bbc41..b62a2757b43 100644 --- a/Generals/Code/GameEngine/Include/Common/Debug.h +++ b/Generals/Code/GameEngine/Include/Common/Debug.h @@ -50,15 +50,21 @@ class AsciiString; +#define NO_RELEASE_DEBUG_LOGGING + +#ifdef RELEASE_DEBUG_LOGGING ///< Creates a DebugLogFile.txt (No I or D) with all the debug log goodness. Good for startup problems. + #define ALLOW_DEBUG_UTILS 1 + #define DEBUG_LOGGING 1 + #define DISABLE_DEBUG_CRASHING 1 + #define DISABLE_DEBUG_STACKTRACE 1 + #define DISABLE_DEBUG_PROFILE 1 +#endif + // These are stolen from the WW3D Debug file. REALLY useful. :-) #define STRING_IT(a) #a #define TOKEN_IT(a) STRING_IT(,##a) #define MESSAGE(a) message (__FILE__ "(" TOKEN_IT(__LINE__) ") : " a) -// BGC, 3/26/03 - put this in so we can build internal worldbuilder for a patch that doesn't -// have any debugging of any kind. -//#define DISABLE_DEBUG_LOGGING - // by default, turn on ALLOW_DEBUG_UTILS if RTS_DEBUG is turned on. #if defined(RTS_DEBUG) && !defined(ALLOW_DEBUG_UTILS) && !defined(DISABLE_ALLOW_DEBUG_UTILS) #define ALLOW_DEBUG_UTILS 1 diff --git a/Generals/Code/GameEngine/Source/Common/System/Debug.cpp b/Generals/Code/GameEngine/Source/Common/System/Debug.cpp index 770940170a6..1c5b3fdd63c 100644 --- a/Generals/Code/GameEngine/Source/Common/System/Debug.cpp +++ b/Generals/Code/GameEngine/Source/Common/System/Debug.cpp @@ -54,21 +54,22 @@ // deterministic or the same on all peers in multiplayer games. //#define INCLUDE_DEBUG_LOG_IN_CRC_LOG -#include "Common/CommandLine.h" #define DEBUG_THREADSAFE #ifdef DEBUG_THREADSAFE #include "Common/CriticalSection.h" #endif +#include "Common/CommandLine.h" #include "Common/Debug.h" #include "Common/CRCDebug.h" -#include "Common/Registry.h" #include "Common/SystemInfo.h" #include "Common/UnicodeString.h" #include "GameClient/ClientInstance.h" #include "GameClient/GameText.h" #include "GameClient/Keyboard.h" #include "GameClient/Mouse.h" -#include "Common/StackDump.h" +#if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE) + #include "Common/StackDump.h" +#endif // Horrible reference, but we really, really need to know if we are windowed. extern bool DX8Wrapper_IsWindowed; @@ -271,8 +272,8 @@ static int doCrashBox(const char *buffer, Bool logResult) int result; if (!ignoringAsserts()) { - //result = MessageBoxWrapper(buffer, "Assertion Failure", MB_ABORTRETRYIGNORE|MB_TASKMODAL|MB_ICONWARNING|MB_DEFBUTTON3); - result = MessageBoxWrapper(buffer, "Assertion Failure", MB_ABORTRETRYIGNORE|MB_TASKMODAL|MB_ICONWARNING); + result = MessageBoxWrapper(buffer, "Assertion Failure", MB_ABORTRETRYIGNORE|MB_TASKMODAL|MB_ICONWARNING|MB_DEFBUTTON3); + //result = MessageBoxWrapper(buffer, "Assertion Failure", MB_ABORTRETRYIGNORE|MB_TASKMODAL|MB_ICONWARNING); } else { result = IDIGNORE; } @@ -358,7 +359,7 @@ void DebugInit(int flags) // ::MessageBox(NULL, "Debug already inited", "", MB_OK|MB_APPLMODAL); // just quietly allow multiple calls to this, so that static ctors can call it. - if (theDebugFlags == 0 && strcmp(gAppPrefix, "wb_") != 0) + if (theDebugFlags == 0) { theDebugFlags = flags; @@ -722,6 +723,11 @@ void ReleaseCrash(const char *reason) { /// do additional reporting on the crash, if possible + if (!DX8Wrapper_IsWindowed) { + if (ApplicationHWnd) { + ShowWindow(ApplicationHWnd, SW_HIDE); + } + } char prevbuf[ _MAX_PATH ]; char curbuf[ _MAX_PATH ]; @@ -767,15 +773,13 @@ void ReleaseCrash(const char *reason) #else // crash error messaged changed 3/6/03 BGC // ::MessageBox(NULL, "Sorry, a serious error occurred.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); +// ::MessageBox(NULL, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); + +// crash error message changed again 8/22/03 M Lorenzen... made this message box modal to the system so it will appear on top of any task-modal windows, splash-screen, etc. + ::MessageBox(NULL, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", + "Technical Difficulties...", + MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); - if (!GetRegistryLanguage().compareNoCase("german2") || !GetRegistryLanguage().compareNoCase("german") ) - { - ::MessageBox(NULL, "Es ist ein gravierender Fehler aufgetreten. Solche Fehler k\366nnen durch viele verschiedene Dinge wie Viren, \374berhitzte Hardware und Hardware, die den Mindestanforderungen des Spiels nicht entspricht, ausgel\366st werden. Tipps zur Vorgehensweise findest du in den Foren unter www.generals.ea.com, Informationen zum Technischen Kundendienst im Handbuch zum Spiel.", "Fehler...", MB_OK|MB_TASKMODAL|MB_ICONERROR); - } - else - { - ::MessageBox(NULL, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); - } #endif diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp index c8a2346c399..f45fbc5a70f 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp @@ -524,7 +524,7 @@ void DebugCrash(const char *format, ...) if (useLogging) { #ifdef DEBUG_LOGGING - if (ignoringAsserts()) + if (ignoringAsserts()) { doLogOutput("**** CRASH IN FULL SCREEN - Auto-ignored, CHECK THIS LOG!"); } @@ -728,13 +728,6 @@ void ReleaseCrash(const char *reason) ShowWindow(ApplicationHWnd, SW_HIDE); } } -//#if defined(RTS_DEBUG) -// /* static */ char buff[8192]; // not so static so we can be threadsafe -// snprintf(buff, 8192, "Sorry, a serious error occurred. (%s)", reason);/ -// ::MessageBox(NULL, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); -//#else -// ::MessageBox(NULL, "Sorry, a serious error occurred.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); -//#endif char prevbuf[ _MAX_PATH ]; char curbuf[ _MAX_PATH ]; @@ -772,6 +765,7 @@ void ReleaseCrash(const char *reason) ShowWindow(ApplicationHWnd, SW_HIDE); } } + #if defined(RTS_DEBUG) /* static */ char buff[8192]; // not so static so we can be threadsafe snprintf(buff, 8192, "Sorry, a serious error occurred. (%s)", reason); From d2573a7f1d65cb2c896134cac2cce28bc7559036 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Tue, 29 Jul 2025 22:53:52 +0200 Subject: [PATCH 22/83] unify(debug): Move GameEngine's Debug files to Core (#1384) --- Core/GameEngine/CMakeLists.txt | 4 +- .../GameEngine/Include/Common/Debug.h | 0 .../GameEngine/Source/Common/System/Debug.cpp | 0 .../Source/WWVegas/WWDebug/wwdebug.h | 3 +- Generals/Code/GameEngine/CMakeLists.txt | 4 +- .../Code/GameEngine/Include/Common/Debug.h | 264 ------ .../GameEngine/Source/Common/System/Debug.cpp | 853 ------------------ GeneralsMD/Code/GameEngine/CMakeLists.txt | 4 +- scripts/cpp/unify_move_files.py | 3 + 9 files changed, 10 insertions(+), 1125 deletions(-) rename {GeneralsMD/Code => Core}/GameEngine/Include/Common/Debug.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/System/Debug.cpp (100%) delete mode 100644 Generals/Code/GameEngine/Include/Common/Debug.h delete mode 100644 Generals/Code/GameEngine/Source/Common/System/Debug.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index ed9b5774d63..689e0004307 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -27,7 +27,7 @@ set(GAMEENGINE_SRC # Include/Common/CustomMatchPreferences.h # Include/Common/DamageFX.h # Include/Common/DataChunk.h -# Include/Common/Debug.h + Include/Common/Debug.h # Include/Common/Dict.h # Include/Common/Directory.h # Include/Common/DisabledTypes.h @@ -637,7 +637,7 @@ set(GAMEENGINE_SRC # Source/Common/System/CDManager.cpp # Source/Common/System/CriticalSection.cpp # Source/Common/System/DataChunk.cpp -# Source/Common/System/Debug.cpp + Source/Common/System/Debug.cpp # Source/Common/System/Directory.cpp # Source/Common/System/DisabledTypes.cpp # Source/Common/System/encrypt.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Debug.h b/Core/GameEngine/Include/Common/Debug.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/Common/Debug.h rename to Core/GameEngine/Include/Common/Debug.h diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/System/Debug.cpp rename to Core/GameEngine/Source/Common/System/Debug.cpp diff --git a/Core/Libraries/Source/WWVegas/WWDebug/wwdebug.h b/Core/Libraries/Source/WWVegas/WWDebug/wwdebug.h index f0701c05153..1d0e12ded26 100644 --- a/Core/Libraries/Source/WWVegas/WWDebug/wwdebug.h +++ b/Core/Libraries/Source/WWVegas/WWDebug/wwdebug.h @@ -110,8 +110,7 @@ void WWDebug_DBWin32_Message_Handler( const char * message); ** WWDEBUG_SAY(("dir = %f",dir)); */ -// TheSuperHackers @compile feliwir 12/04/2025 Both Debug headers are identical. Use ZH. -#include "../../../../../GeneralsMD/Code/GameEngine/Include/Common/Debug.h" +#include "../../../../../Core/GameEngine/Include/Common/Debug.h" #ifdef DEBUG_LOGGING #define WWDEBUG_SAY(x) DEBUG_LOG(x) diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index e694e0fe973..1bb7173ff51 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -25,7 +25,7 @@ set(GAMEENGINE_SRC Include/Common/CustomMatchPreferences.h Include/Common/DamageFX.h Include/Common/DataChunk.h - Include/Common/Debug.h +# Include/Common/Debug.h Include/Common/Dict.h Include/Common/Directory.h Include/Common/DisabledTypes.h @@ -594,7 +594,7 @@ set(GAMEENGINE_SRC Source/Common/System/CDManager.cpp Source/Common/System/CriticalSection.cpp Source/Common/System/DataChunk.cpp - Source/Common/System/Debug.cpp +# Source/Common/System/Debug.cpp Source/Common/System/Directory.cpp Source/Common/System/DisabledTypes.cpp Source/Common/System/encrypt.cpp diff --git a/Generals/Code/GameEngine/Include/Common/Debug.h b/Generals/Code/GameEngine/Include/Common/Debug.h deleted file mode 100644 index b62a2757b43..00000000000 --- a/Generals/Code/GameEngine/Include/Common/Debug.h +++ /dev/null @@ -1,264 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// FILE: Debug.h -//----------------------------------------------------------------------------- -// -// Westwood Studios Pacific. -// -// Confidential Information -// Copyright (C) 2001 - All Rights Reserved -// -//----------------------------------------------------------------------------- -// -// Project: RTS3 -// -// File name: Debug.h -// -// Created: Steven Johnson, August 2001 -// -// Desc: Debug Utilities -// -//----------------------------------------------------------------------------- -/////////////////////////////////////////////////////////////////////////////// - -#pragma once - -#ifndef __DEBUG_H_ -#define __DEBUG_H_ - -class AsciiString; - -#define NO_RELEASE_DEBUG_LOGGING - -#ifdef RELEASE_DEBUG_LOGGING ///< Creates a DebugLogFile.txt (No I or D) with all the debug log goodness. Good for startup problems. - #define ALLOW_DEBUG_UTILS 1 - #define DEBUG_LOGGING 1 - #define DISABLE_DEBUG_CRASHING 1 - #define DISABLE_DEBUG_STACKTRACE 1 - #define DISABLE_DEBUG_PROFILE 1 -#endif - -// These are stolen from the WW3D Debug file. REALLY useful. :-) -#define STRING_IT(a) #a -#define TOKEN_IT(a) STRING_IT(,##a) -#define MESSAGE(a) message (__FILE__ "(" TOKEN_IT(__LINE__) ") : " a) - -// by default, turn on ALLOW_DEBUG_UTILS if RTS_DEBUG is turned on. -#if defined(RTS_DEBUG) && !defined(ALLOW_DEBUG_UTILS) && !defined(DISABLE_ALLOW_DEBUG_UTILS) - #define ALLOW_DEBUG_UTILS 1 -#elif defined(DEBUG_LOGGING) || defined(DEBUG_CRASHING) || defined(DEBUG_STACKTRACE) || defined(DEBUG_PROFILE) - // TheSuperHackers @tweak also turn on when any of the above options is already set. - #define ALLOW_DEBUG_UTILS 1 -#endif - -// these are predicated on ALLOW_DEBUG_UTILS, not RTS_DEBUG, and allow you to selectively disable -// bits of the debug stuff for special builds. -#if defined(ALLOW_DEBUG_UTILS) && !defined(DEBUG_LOGGING) && !defined(DISABLE_DEBUG_LOGGING) - #define DEBUG_LOGGING 1 -#endif -#if defined(ALLOW_DEBUG_UTILS) && !defined(DEBUG_CRASHING) && !defined(DISABLE_DEBUG_CRASHING) - #define DEBUG_CRASHING 1 -#endif -#if defined(ALLOW_DEBUG_UTILS) && !defined(DEBUG_STACKTRACE) && !defined(DISABLE_DEBUG_STACKTRACE) - #define DEBUG_STACKTRACE 1 - #ifndef DEBUG_LOGGING - #define DEBUG_LOGGING 1 // TheSuperHackers @compile Stack trace requires logging. - #endif -#endif -#if defined(ALLOW_DEBUG_UTILS) && !defined(DEBUG_PROFILE) && !defined(DISABLE_DEBUG_PROFILE) - #define DEBUG_PROFILE 1 -#endif - -#ifdef __cplusplus - #define DEBUG_EXTERN_C extern "C" -#else - #define DEBUG_EXTERN_C extern -#endif - - -// SYSTEM INCLUDES //////////////////////////////////////////////////////////// - -// USER INCLUDES ////////////////////////////////////////////////////////////// - -// FORWARD REFERENCES ///////////////////////////////////////////////////////// - -// TYPE DEFINES /////////////////////////////////////////////////////////////// - -// INLINING /////////////////////////////////////////////////////////////////// - -// EXTERNALS ////////////////////////////////////////////////////////////////// - -/// @todo: the standard line-to-string trick isn't working correctly in vc6; figure out why -#define DEBUG_STRING_IT(b) #b -#define DEBUG_TOKEN_IT(a) DEBUG_STRING_IT(a) -#define DEBUG_FILENLINE __FILE__ ":" DEBUG_TOKEN_IT(__LINE__) - -#ifdef ALLOW_DEBUG_UTILS - - enum - { - DEBUG_FLAG_LOG_TO_FILE = 0x01, - DEBUG_FLAG_LOG_TO_CONSOLE = 0x02, - DEBUG_FLAG_PREPEND_TIME = 0x04, - DEBUG_FLAGS_DEFAULT = (DEBUG_FLAG_LOG_TO_FILE | DEBUG_FLAG_LOG_TO_CONSOLE), - }; - - DEBUG_EXTERN_C void DebugInit(int flags); - DEBUG_EXTERN_C void DebugShutdown(); - - DEBUG_EXTERN_C int DebugGetFlags(); - DEBUG_EXTERN_C void DebugSetFlags(int flags); - - #define DEBUG_INIT(f) do { DebugInit(f); } while (0) - #define DEBUG_SHUTDOWN() do { DebugShutdown(); } while (0) - -#else - - #define DEBUG_INIT(f) ((void)0) - #define DEBUG_SHUTDOWN() ((void)0) - -#endif - -#ifdef DEBUG_LOGGING - - DEBUG_EXTERN_C void DebugLog(const char *format, ...); - DEBUG_EXTERN_C void DebugLogRaw(const char *format, ...); - DEBUG_EXTERN_C const char* DebugGetLogFileName(); - DEBUG_EXTERN_C const char* DebugGetLogFileNamePrev(); - - // This defines a bitmask of log types that we care about, to allow some flexability - // in what gets logged. This should be extended to asserts, too, but the assert box - // is waiting to be rewritten. -MDC 3/19/2003 - extern unsigned int DebugLevelMask; - enum - { - DEBUG_LEVEL_NET = 0, // in-game network - DEBUG_LEVEL_MAX - }; - extern const char *TheDebugLevels[DEBUG_LEVEL_MAX]; - - #define DEBUG_LOG(m) do { { DebugLog m ; } } while (0) // Log message with trailing new line character (LF) - #define DEBUG_LOG_RAW(m) do { { DebugLogRaw m ; } } while (0) // Log message without trailing new line character (LF) - #define DEBUG_LOG_LEVEL(l, m) do { if (l & DebugLevelMask) { DebugLog m ; } } while (0) - #define DEBUG_LOG_LEVEL_RAW(l, m) do { if (l & DebugLevelMask) { DebugLogRaw m ; } } while (0) - #define DEBUG_ASSERTLOG(c, m) do { { if (!(c)) DebugLog m ; } } while (0) - -#else - - #define DEBUG_LOG(m) ((void)0) - #define DEBUG_LOG_RAW(m) ((void)0) - #define DEBUG_LOG_LEVEL(l, m) ((void)0) - #define DEBUG_LOG_LEVEL_RAW(l, m) ((void)0) - #define DEBUG_ASSERTLOG(c, m) ((void)0) - -#endif - -#ifdef DEBUG_CRASHING - - DEBUG_EXTERN_C void DebugCrash(const char *format, ...); - - /* - Yeah, it's a sleazy global, since we can't reasonably add - any args to DebugCrash due to the varargs nature of it. - We'll just let it slide in this case... - */ - DEBUG_EXTERN_C char* TheCurrentIgnoreCrashPtr; - - #define DEBUG_CRASH(m) \ - do { \ - { \ - static char ignoreCrash = 0; \ - if (!ignoreCrash) { \ - TheCurrentIgnoreCrashPtr = &ignoreCrash; \ - DebugCrash m ; \ - TheCurrentIgnoreCrashPtr = NULL; \ - } \ - } \ - } while (0) - - #define DEBUG_ASSERTCRASH(c, m) do { { if (!(c)) DEBUG_CRASH(m); } } while (0) - - //Note: RELEASE_CRASH(m) is now always defined. - //#define RELEASE_CRASH(m) DEBUG_CRASH((m)) - -#else - - #define DEBUG_CRASH(m) ((void)0) - #define DEBUG_ASSERTCRASH(c, m) ((void)0) - -// DEBUG_EXTERN_C void ReleaseCrash(const char* reason); - -// #define RELEASE_CRASH(m) do { ReleaseCrash(m); } while (0) - -#endif - -DEBUG_EXTERN_C void ReleaseCrash(const char* reason); -DEBUG_EXTERN_C void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m); - -#define RELEASE_CRASH(m) do { ReleaseCrash(m); } while (0) -#define RELEASE_CRASHLOCALIZED(p, m) do { ReleaseCrashLocalized(p, m); } while (0) - - -#ifdef DEBUG_PROFILE - -class SimpleProfiler -{ -private: - __int64 m_freq; - __int64 m_startThisSession; - __int64 m_totalThisSession; - __int64 m_totalAllSessions; - int m_numSessions; - -public: - - SimpleProfiler(); - void start(); - void stop(); - void stopAndLog(const char *msg, int howOftenToLog, int howOftenToResetAvg); - double getTime(); // of most recent session, in milliseconds - int getNumSessions(); - double getTotalTime(); // total over all sessions, in milliseconds - double getAverageTime(); // averaged over all sessions, in milliseconds - -}; - -#define BEGIN_PROFILE(uniqueid) \ - static SimpleProfiler prof_##uniqueid; \ - prof_##uniqueid.start(); - -#define END_PROFILE(uniqueid, msg, howoftentolog, howoftentoreset) \ - prof_##uniqueid.stopAndLog(msg, howoftentolog, howoftentoreset); - -#else - -#define BEGIN_PROFILE(uniqueid) -#define END_PROFILE(uniqueid, msg, howoftentolog, howoftentoreset) - -#endif - -// MACROS ////////////////////////////////////////////////////////////////// - -#endif // __DEBUG_H_ diff --git a/Generals/Code/GameEngine/Source/Common/System/Debug.cpp b/Generals/Code/GameEngine/Source/Common/System/Debug.cpp deleted file mode 100644 index 1c5b3fdd63c..00000000000 --- a/Generals/Code/GameEngine/Source/Common/System/Debug.cpp +++ /dev/null @@ -1,853 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -// FILE: Debug.cpp -//----------------------------------------------------------------------------- -// -// Westwood Studios Pacific. -// -// Confidential Information -// Copyright (C) 2001 - All Rights Reserved -// -//----------------------------------------------------------------------------- -// -// Project: RTS3 -// -// File name: Debug.cpp -// -// Created: Steven Johnson, August 2001 -// -// Desc: Debug logging and other debug utilities -// -// ---------------------------------------------------------------------------- - -// SYSTEM INCLUDES -#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine - - -// USER INCLUDES - -// TheSuperHackers @feature helmutbuhler 04/10/2025 -// Uncomment this to show normal logging stuff in the crc logging. -// This can be helpful for context, but can also clutter diffs because normal logs aren't necessarily -// deterministic or the same on all peers in multiplayer games. -//#define INCLUDE_DEBUG_LOG_IN_CRC_LOG - -#define DEBUG_THREADSAFE -#ifdef DEBUG_THREADSAFE -#include "Common/CriticalSection.h" -#endif -#include "Common/CommandLine.h" -#include "Common/Debug.h" -#include "Common/CRCDebug.h" -#include "Common/SystemInfo.h" -#include "Common/UnicodeString.h" -#include "GameClient/ClientInstance.h" -#include "GameClient/GameText.h" -#include "GameClient/Keyboard.h" -#include "GameClient/Mouse.h" -#if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE) - #include "Common/StackDump.h" -#endif - -// Horrible reference, but we really, really need to know if we are windowed. -extern bool DX8Wrapper_IsWindowed; -extern HWND ApplicationHWnd; - -extern const char *gAppPrefix; /// So WB can have a different log file name. - - -// ---------------------------------------------------------------------------- -// DEFINES -// ---------------------------------------------------------------------------- - -#ifdef DEBUG_LOGGING - -#if defined(RTS_DEBUG) - #define DEBUG_FILE_NAME "DebugLogFileD" - #define DEBUG_FILE_NAME_PREV "DebugLogFilePrevD" -#else - #define DEBUG_FILE_NAME "DebugLogFile" - #define DEBUG_FILE_NAME_PREV "DebugLogFilePrev" -#endif - -#endif - -// ---------------------------------------------------------------------------- -// PRIVATE TYPES -// ---------------------------------------------------------------------------- - -// ---------------------------------------------------------------------------- -// PRIVATE DATA -// ---------------------------------------------------------------------------- -// TheSuperHackers @info Must not use static RAII types when set in DebugInit, -// because DebugInit can be called during static module initialization before the main function is called. -#ifdef DEBUG_LOGGING -static FILE *theLogFile = NULL; -static char theLogFileName[ _MAX_PATH ]; -static char theLogFileNamePrev[ _MAX_PATH ]; -#endif -#define LARGE_BUFFER 8192 -static char theBuffer[ LARGE_BUFFER ]; // make it big to avoid weird overflow bugs in debug mode -static int theDebugFlags = 0; -static DWORD theMainThreadID = 0; -// ---------------------------------------------------------------------------- -// PUBLIC DATA -// ---------------------------------------------------------------------------- - -char* TheCurrentIgnoreCrashPtr = NULL; -#ifdef DEBUG_LOGGING -UnsignedInt DebugLevelMask = 0; -const char *TheDebugLevels[DEBUG_LEVEL_MAX] = { - "NET" -}; -#endif - -// ---------------------------------------------------------------------------- -// PRIVATE PROTOTYPES -// ---------------------------------------------------------------------------- -static const char *getCurrentTimeString(void); -static const char *getCurrentTickString(void); -static void prepBuffer(char *buffer); -#ifdef DEBUG_LOGGING -static void doLogOutput(const char *buffer); -static void doLogOutput(const char *buffer, const char *endline); -#endif -#ifdef DEBUG_CRASHING -static int doCrashBox(const char *buffer, Bool logResult); -#endif -static void whackFunnyCharacters(char *buf); -#ifdef DEBUG_STACKTRACE -static void doStackDump(); -#endif - -// ---------------------------------------------------------------------------- -// PRIVATE FUNCTIONS -// ---------------------------------------------------------------------------- - -// ---------------------------------------------------------------------------- -inline Bool ignoringAsserts() -{ - if (!DX8Wrapper_IsWindowed) - return true; - if (TheGlobalData && TheGlobalData->m_headless) - return true; -#ifdef DEBUG_CRASHING - if (TheGlobalData && TheGlobalData->m_debugIgnoreAsserts) - return true; -#endif - - return false; -} - -// ---------------------------------------------------------------------------- -inline HWND getThreadHWND() -{ - return (theMainThreadID == GetCurrentThreadId())?ApplicationHWnd:NULL; -} - -// ---------------------------------------------------------------------------- - -int MessageBoxWrapper( LPCSTR lpText, LPCSTR lpCaption, UINT uType ) -{ - HWND threadHWND = getThreadHWND(); - return ::MessageBox(threadHWND, lpText, lpCaption, uType); -} - -// ---------------------------------------------------------------------------- -// getCurrentTimeString -/** - Return the current time in string form -*/ -// ---------------------------------------------------------------------------- -static const char *getCurrentTimeString(void) -{ - time_t aclock; - time(&aclock); - struct tm *newtime = localtime(&aclock); - return asctime(newtime); -} - -// ---------------------------------------------------------------------------- -// getCurrentTickString -/** - Return the current TickCount in string form -*/ -// ---------------------------------------------------------------------------- -static const char *getCurrentTickString(void) -{ - static char TheTickString[32]; - snprintf(TheTickString, ARRAY_SIZE(TheTickString), "(T=%08lx)", ::GetTickCount()); - return TheTickString; -} - -// ---------------------------------------------------------------------------- -// prepBuffer -// zap the buffer and optionally prepend the tick time. -// ---------------------------------------------------------------------------- -/** - Empty the buffer passed in, then optionally prepend the current TickCount - value in string form, depending on the setting of theDebugFlags. -*/ -static void prepBuffer(char *buffer) -{ - buffer[0] = 0; -#ifdef ALLOW_DEBUG_UTILS - if (theDebugFlags & DEBUG_FLAG_PREPEND_TIME) - { - strcpy(buffer, getCurrentTickString()); - strcat(buffer, " "); - } -#endif -} - -// ---------------------------------------------------------------------------- -// doLogOutput -/** - send a string directly to the log file and/or console without further processing. -*/ -// ---------------------------------------------------------------------------- -#ifdef DEBUG_LOGGING -static void doLogOutput(const char *buffer) -{ - doLogOutput(buffer, "\n"); -} - -static void doLogOutput(const char *buffer, const char *endline) -{ - // log message to file - if (theDebugFlags & DEBUG_FLAG_LOG_TO_FILE) - { - if (theLogFile) - { - fprintf(theLogFile, "%s%s", buffer, endline); - fflush(theLogFile); - } - } - - // log message to dev studio output window - if (theDebugFlags & DEBUG_FLAG_LOG_TO_CONSOLE) - { - ::OutputDebugString(buffer); - ::OutputDebugString(endline); - } - -#ifdef INCLUDE_DEBUG_LOG_IN_CRC_LOG - addCRCDebugLineNoCounter("%s%s", buffer, endline); -#endif -} -#endif // DEBUG_LOGGING - -// ---------------------------------------------------------------------------- -// doCrashBox -/* - present a messagebox with the given message. Depending on user selection, - we exit the app, break into debugger, or continue execution. -*/ -// ---------------------------------------------------------------------------- -#ifdef DEBUG_CRASHING -static int doCrashBox(const char *buffer, Bool logResult) -{ - int result; - - if (!ignoringAsserts()) { - result = MessageBoxWrapper(buffer, "Assertion Failure", MB_ABORTRETRYIGNORE|MB_TASKMODAL|MB_ICONWARNING|MB_DEFBUTTON3); - //result = MessageBoxWrapper(buffer, "Assertion Failure", MB_ABORTRETRYIGNORE|MB_TASKMODAL|MB_ICONWARNING); - } else { - result = IDIGNORE; - } - - switch(result) - { - case IDABORT: -#ifdef DEBUG_LOGGING - if (logResult) - DebugLog("[Abort]"); -#endif - _exit(1); - break; - case IDRETRY: -#ifdef DEBUG_LOGGING - if (logResult) - DebugLog("[Retry]"); -#endif - ::DebugBreak(); - break; - case IDIGNORE: -#ifdef DEBUG_LOGGING - // do nothing, just keep going - if (logResult) - DebugLog("[Ignore]"); -#endif - break; - } - return result; -} -#endif - -#ifdef DEBUG_STACKTRACE -// ---------------------------------------------------------------------------- -/** - Dumps a stack trace (from the current PC) to logfile and/or console. -*/ -static void doStackDump() -{ - const int STACKTRACE_SIZE = 24; - const int STACKTRACE_SKIP = 2; - void* stacktrace[STACKTRACE_SIZE]; - - doLogOutput("\nStack Dump:"); - ::FillStackAddresses(stacktrace, STACKTRACE_SIZE, STACKTRACE_SKIP); - ::StackDumpFromAddresses(stacktrace, STACKTRACE_SIZE, doLogOutput); -} -#endif - -// ---------------------------------------------------------------------------- -// whackFunnyCharacters -/** - Eliminates any undesirable nonprinting characters, aside from newline, - replacing them with spaces. -*/ -// ---------------------------------------------------------------------------- -static void whackFunnyCharacters(char *buf) -{ - for (char *p = buf + strlen(buf) - 1; p >= buf; --p) - { - // ok, these are naughty magic numbers, but I'm guessing you know ASCII.... - if (*p >= 0 && *p < 32 && *p != 10 && *p != 13) - *p = 32; - } -} - -// ---------------------------------------------------------------------------- -// PUBLIC FUNCTIONS -// ---------------------------------------------------------------------------- - -// ---------------------------------------------------------------------------- -// DebugInit -// ---------------------------------------------------------------------------- -#ifdef ALLOW_DEBUG_UTILS -/** - Initialize the debug utilities. This should be called once, as near to the - start of the app as possible, before anything else (since other code will - probably want to make use of it). -*/ -void DebugInit(int flags) -{ -// if (theDebugFlags != 0) -// ::MessageBox(NULL, "Debug already inited", "", MB_OK|MB_APPLMODAL); - - // just quietly allow multiple calls to this, so that static ctors can call it. - if (theDebugFlags == 0) - { - theDebugFlags = flags; - - theMainThreadID = GetCurrentThreadId(); - - #ifdef DEBUG_LOGGING - - // TheSuperHackers @info Debug initialization can happen very early. - // Determine the client instance id before creating the log file with an instance specific name. - CommandLine::parseCommandLineForStartup(); - - if (!rts::ClientInstance::initialize()) - return; - - char dirbuf[ _MAX_PATH ]; - ::GetModuleFileName( NULL, dirbuf, sizeof( dirbuf ) ); - char *pEnd = dirbuf + strlen( dirbuf ); - while( pEnd != dirbuf ) - { - if( *pEnd == '\\' ) - { - *(pEnd + 1) = 0; - break; - } - pEnd--; - } - - strcpy(theLogFileNamePrev, dirbuf); - strcat(theLogFileNamePrev, gAppPrefix); - strcat(theLogFileNamePrev, DEBUG_FILE_NAME_PREV); - if (rts::ClientInstance::getInstanceId() > 1u) - { - size_t offset = strlen(theLogFileNamePrev); - snprintf(theLogFileNamePrev + offset, ARRAY_SIZE(theLogFileNamePrev) - offset, "_Instance%.2u", rts::ClientInstance::getInstanceId()); - } - strcat(theLogFileNamePrev, ".txt"); - - strcpy(theLogFileName, dirbuf); - strcat(theLogFileName, gAppPrefix); - strcat(theLogFileName, DEBUG_FILE_NAME); - if (rts::ClientInstance::getInstanceId() > 1u) - { - size_t offset = strlen(theLogFileName); - snprintf(theLogFileName + offset, ARRAY_SIZE(theLogFileName) - offset, "_Instance%.2u", rts::ClientInstance::getInstanceId()); - } - strcat(theLogFileName, ".txt"); - - remove(theLogFileNamePrev); - rename(theLogFileName, theLogFileNamePrev); - theLogFile = fopen(theLogFileName, "w"); - if (theLogFile != NULL) - { - DebugLog("Log %s opened: %s", theLogFileName, getCurrentTimeString()); - } - #endif - } - -} -#endif - -// ---------------------------------------------------------------------------- -// DebugLog -// ---------------------------------------------------------------------------- -#ifdef DEBUG_LOGGING -/** - Print a string to the log file and/or console. -*/ -void DebugLog(const char *format, ...) -{ -#ifdef DEBUG_THREADSAFE - ScopedCriticalSection scopedCriticalSection(TheDebugLogCriticalSection); -#endif - - if (theDebugFlags == 0) - MessageBoxWrapper("DebugLog - Debug not inited properly", "", MB_OK|MB_TASKMODAL); - - prepBuffer(theBuffer); - - va_list args; - va_start(args, format); - size_t offset = strlen(theBuffer); - vsnprintf(theBuffer + offset, ARRAY_SIZE(theBuffer) - offset, format, args); - va_end(args); - - if (strlen(theBuffer) >= sizeof(theBuffer)) - MessageBoxWrapper("String too long for debug buffer", "", MB_OK|MB_TASKMODAL); - - whackFunnyCharacters(theBuffer); - doLogOutput(theBuffer); -} - -/** - Print a string with no modifications to the log file and/or console. -*/ -void DebugLogRaw(const char *format, ...) -{ -#ifdef DEBUG_THREADSAFE - ScopedCriticalSection scopedCriticalSection(TheDebugLogCriticalSection); -#endif - - if (theDebugFlags == 0) - MessageBoxWrapper("DebugLogRaw - Debug not inited properly", "", MB_OK|MB_TASKMODAL); - - theBuffer[0] = 0; - - va_list args; - va_start(args, format); - vsnprintf(theBuffer, ARRAY_SIZE(theBuffer), format, args); - va_end(args); - - if (strlen(theBuffer) >= sizeof(theBuffer)) - MessageBoxWrapper("String too long for debug buffer", "", MB_OK|MB_TASKMODAL); - - doLogOutput(theBuffer, ""); -} - -const char* DebugGetLogFileName() -{ - return theLogFileName; -} - -const char* DebugGetLogFileNamePrev() -{ - return theLogFileNamePrev; -} - -#endif - -// ---------------------------------------------------------------------------- -// DebugCrash -// ---------------------------------------------------------------------------- -#ifdef DEBUG_CRASHING -/** - Print a character string to the log file and/or console, then halt execution - while presenting the user with an exit/debug/ignore dialog containing the same - text message. - - TheSuperHackers @tweak Now shows a message box without any logging when debug was not yet initialized. -*/ -void DebugCrash(const char *format, ...) -{ - // Note: You might want to make this thread safe, but we cannot. The reason is that - // there is an implicit requirement on other threads that the message loop be running. - - // make it not static so that it'll be thread-safe. - // make it big to avoid weird overflow bugs in debug mode - char theCrashBuffer[ LARGE_BUFFER ]; - - prepBuffer(theCrashBuffer); - strcat(theCrashBuffer, "ASSERTION FAILURE: "); - - va_list arg; - va_start(arg, format); - size_t offset = strlen(theCrashBuffer); - vsnprintf(theCrashBuffer + offset, ARRAY_SIZE(theCrashBuffer) - offset, format, arg); - va_end(arg); - - whackFunnyCharacters(theCrashBuffer); - - const bool useLogging = theDebugFlags != 0; - - if (useLogging) - { -#ifdef DEBUG_LOGGING - if (ignoringAsserts()) - { - doLogOutput("**** CRASH IN FULL SCREEN - Auto-ignored, CHECK THIS LOG!"); - } - doLogOutput(theCrashBuffer); -#endif -#ifdef DEBUG_STACKTRACE - if (!(TheGlobalData && TheGlobalData->m_debugIgnoreStackTrace)) - { - doStackDump(); - } -#endif - } - - strcat(theCrashBuffer, "\n\nAbort->exception; Retry->debugger; Ignore->continue"); - - const int result = doCrashBox(theCrashBuffer, useLogging); - - if (result == IDIGNORE && TheCurrentIgnoreCrashPtr != NULL) - { - int yn; - if (!ignoringAsserts()) - { - yn = MessageBoxWrapper("Ignore this crash from now on?", "", MB_YESNO|MB_TASKMODAL); - } - else - { - yn = IDYES; - } - if (yn == IDYES) - *TheCurrentIgnoreCrashPtr = 1; - if( TheKeyboard ) - TheKeyboard->resetKeys(); - if( TheMouse ) - TheMouse->reset(); - } - -} -#endif - -// ---------------------------------------------------------------------------- -// DebugShutdown -// ---------------------------------------------------------------------------- -#ifdef ALLOW_DEBUG_UTILS -/** - Shut down the debug utilities. This should be called once, as near to the - end of the app as possible, after everything else (since other code will - probably want to make use of it). -*/ -void DebugShutdown() -{ -#ifdef DEBUG_LOGGING - if (theLogFile) - { - DebugLog("Log closed: %s", getCurrentTimeString()); - fclose(theLogFile); - } - theLogFile = NULL; -#endif - theDebugFlags = 0; -} - -// ---------------------------------------------------------------------------- -// DebugGetFlags -// ---------------------------------------------------------------------------- -/** - Get the current values for the flags passed to DebugInit. Most code will never - need to use this; the most common usage would be to temporarily enable or disable - the DEBUG_FLAG_PREPEND_TIME bit for complex logfile messages. -*/ -int DebugGetFlags() -{ - return theDebugFlags; -} - -// ---------------------------------------------------------------------------- -// DebugSetFlags -// ---------------------------------------------------------------------------- -/** - Set the current values for the flags passed to DebugInit. Most code will never - need to use this; the most common usage would be to temporarily enable or disable - the DEBUG_FLAG_PREPEND_TIME bit for complex logfile messages. -*/ -void DebugSetFlags(int flags) -{ - theDebugFlags = flags; -} - -#endif // ALLOW_DEBUG_UTILS - -#ifdef DEBUG_PROFILE -// ---------------------------------------------------------------------------- -SimpleProfiler::SimpleProfiler() -{ - QueryPerformanceFrequency((LARGE_INTEGER*)&m_freq); - m_startThisSession = 0; - m_totalThisSession = 0; - m_totalAllSessions = 0; - m_numSessions = 0; -} - -// ---------------------------------------------------------------------------- -void SimpleProfiler::start() -{ - DEBUG_ASSERTCRASH(m_startThisSession == 0, ("already started")); - QueryPerformanceCounter((LARGE_INTEGER*)&m_startThisSession); -} - -// ---------------------------------------------------------------------------- -void SimpleProfiler::stop() -{ - if (m_startThisSession != 0) - { - __int64 stop; - QueryPerformanceCounter((LARGE_INTEGER*)&stop); - m_totalThisSession = stop - m_startThisSession; - m_totalAllSessions += stop - m_startThisSession; - m_startThisSession = 0; - ++m_numSessions; - } -} - -// ---------------------------------------------------------------------------- -void SimpleProfiler::stopAndLog(const char *msg, int howOftenToLog, int howOftenToResetAvg) -{ - stop(); - // howOftenToResetAvg==0 means "never reset" - if (howOftenToResetAvg > 0 && m_numSessions >= howOftenToResetAvg) - { - m_numSessions = 0; - m_totalAllSessions = 0; - DEBUG_LOG(("%s: reset averages",msg)); - } - DEBUG_ASSERTLOG(m_numSessions % howOftenToLog != 0, ("%s: %f msec, total %f msec, avg %f msec",msg,getTime(),getTotalTime(),getAverageTime())); -} - -// ---------------------------------------------------------------------------- -double SimpleProfiler::getTime() -{ - stop(); - return (double)m_totalThisSession / (double)m_freq * 1000.0; -} - -// ---------------------------------------------------------------------------- -int SimpleProfiler::getNumSessions() -{ - stop(); - return m_numSessions; -} - -// ---------------------------------------------------------------------------- -double SimpleProfiler::getTotalTime() -{ - stop(); - if (!m_numSessions) - return 0.0; - - return (double)m_totalAllSessions * 1000.0 / ((double)m_freq); -} - -// ---------------------------------------------------------------------------- -double SimpleProfiler::getAverageTime() -{ - stop(); - if (!m_numSessions) - return 0.0; - - return (double)m_totalAllSessions * 1000.0 / ((double)m_freq * (double)m_numSessions); -} - -#endif // DEBUG_PROFILE - -// ---------------------------------------------------------------------------- -// ReleaseCrash -// ---------------------------------------------------------------------------- -/** - Halt the application, EVEN IN FINAL RELEASE BUILDS. This should be called - only when a crash is guaranteed by continuing, and no meaningful continuation - of processing is possible, even by throwing an exception. -*/ - - #define RELEASECRASH_FILE_NAME "ReleaseCrashInfo.txt" - #define RELEASECRASH_FILE_NAME_PREV "ReleaseCrashInfoPrev.txt" - - static FILE *theReleaseCrashLogFile = NULL; - - static void releaseCrashLogOutput(const char *buffer) - { - if (theReleaseCrashLogFile) - { - fprintf(theReleaseCrashLogFile, "%s\n", buffer); - fflush(theReleaseCrashLogFile); - } - } - -void ReleaseCrash(const char *reason) -{ - /// do additional reporting on the crash, if possible - - if (!DX8Wrapper_IsWindowed) { - if (ApplicationHWnd) { - ShowWindow(ApplicationHWnd, SW_HIDE); - } - } - - char prevbuf[ _MAX_PATH ]; - char curbuf[ _MAX_PATH ]; - - if (TheGlobalData==NULL) { - return; // We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003] - } - - strcpy(prevbuf, TheGlobalData->getPath_UserData().str()); - strcat(prevbuf, RELEASECRASH_FILE_NAME_PREV); - strcpy(curbuf, TheGlobalData->getPath_UserData().str()); - strcat(curbuf, RELEASECRASH_FILE_NAME); - - remove(prevbuf); - rename(curbuf, prevbuf); - - theReleaseCrashLogFile = fopen(curbuf, "w"); - if (theReleaseCrashLogFile) - { - fprintf(theReleaseCrashLogFile, "Release Crash at %s; Reason %s\n", getCurrentTimeString(), reason); - fprintf(theReleaseCrashLogFile, "\nLast error:\n%s\n\nCurrent stack:\n", g_LastErrorDump.str()); - const int STACKTRACE_SIZE = 12; - const int STACKTRACE_SKIP = 6; - void* stacktrace[STACKTRACE_SIZE]; - ::FillStackAddresses(stacktrace, STACKTRACE_SIZE, STACKTRACE_SKIP); - ::StackDumpFromAddresses(stacktrace, STACKTRACE_SIZE, releaseCrashLogOutput); - - fflush(theReleaseCrashLogFile); - fclose(theReleaseCrashLogFile); - theReleaseCrashLogFile = NULL; - } - - if (!DX8Wrapper_IsWindowed) { - if (ApplicationHWnd) { - ShowWindow(ApplicationHWnd, SW_HIDE); - } - } - -#if defined(RTS_DEBUG) - /* static */ char buff[8192]; // not so static so we can be threadsafe - snprintf(buff, 8192, "Sorry, a serious error occurred. (%s)", reason); - ::MessageBox(NULL, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); -#else -// crash error messaged changed 3/6/03 BGC -// ::MessageBox(NULL, "Sorry, a serious error occurred.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); -// ::MessageBox(NULL, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); - -// crash error message changed again 8/22/03 M Lorenzen... made this message box modal to the system so it will appear on top of any task-modal windows, splash-screen, etc. - ::MessageBox(NULL, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", - "Technical Difficulties...", - MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); - - -#endif - - _exit(1); -} - -void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) -{ - if (!TheGameText) { - ReleaseCrash(m.str()); - // This won't ever return - return; - } - - UnicodeString prompt = TheGameText->fetch(p); - UnicodeString mesg = TheGameText->fetch(m); - - - /// do additional reporting on the crash, if possible - - if (!DX8Wrapper_IsWindowed) { - if (ApplicationHWnd) { - ShowWindow(ApplicationHWnd, SW_HIDE); - } - } - - if (TheSystemIsUnicode) - { - ::MessageBoxW(NULL, mesg.str(), prompt.str(), MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); - } - else - { - // However, if we're using the default version of the message box, we need to - // translate the string into an AsciiString - AsciiString promptA, mesgA; - promptA.translate(prompt); - mesgA.translate(mesg); - //Make sure main window is not TOP_MOST - ::SetWindowPos(ApplicationHWnd, HWND_NOTOPMOST, 0, 0, 0, 0,SWP_NOSIZE |SWP_NOMOVE); - ::MessageBoxA(NULL, mesgA.str(), promptA.str(), MB_OK|MB_TASKMODAL|MB_ICONERROR); - } - - char prevbuf[ _MAX_PATH ]; - char curbuf[ _MAX_PATH ]; - - strcpy(prevbuf, TheGlobalData->getPath_UserData().str()); - strcat(prevbuf, RELEASECRASH_FILE_NAME_PREV); - strcpy(curbuf, TheGlobalData->getPath_UserData().str()); - strcat(curbuf, RELEASECRASH_FILE_NAME); - - remove(prevbuf); - rename(curbuf, prevbuf); - - theReleaseCrashLogFile = fopen(curbuf, "w"); - if (theReleaseCrashLogFile) - { - fprintf(theReleaseCrashLogFile, "Release Crash at %s; Reason %ls\n", getCurrentTimeString(), mesg.str()); - - const int STACKTRACE_SIZE = 12; - const int STACKTRACE_SKIP = 6; - void* stacktrace[STACKTRACE_SIZE]; - ::FillStackAddresses(stacktrace, STACKTRACE_SIZE, STACKTRACE_SKIP); - ::StackDumpFromAddresses(stacktrace, STACKTRACE_SIZE, releaseCrashLogOutput); - - fflush(theReleaseCrashLogFile); - fclose(theReleaseCrashLogFile); - theReleaseCrashLogFile = NULL; - } - - _exit(1); -} diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index f8d493bded2..7653a203a6c 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -27,7 +27,7 @@ set(GAMEENGINE_SRC Include/Common/CustomMatchPreferences.h Include/Common/DamageFX.h Include/Common/DataChunk.h - Include/Common/Debug.h +# Include/Common/Debug.h Include/Common/Dict.h Include/Common/Directory.h Include/Common/DisabledTypes.h @@ -636,7 +636,7 @@ set(GAMEENGINE_SRC Source/Common/System/CDManager.cpp Source/Common/System/CriticalSection.cpp Source/Common/System/DataChunk.cpp - Source/Common/System/Debug.cpp +# Source/Common/System/Debug.cpp Source/Common/System/Directory.cpp Source/Common/System/DisabledTypes.cpp Source/Common/System/encrypt.cpp diff --git a/scripts/cpp/unify_move_files.py b/scripts/cpp/unify_move_files.py index c6c62bd9205..4c8c298d8b1 100644 --- a/scripts/cpp/unify_move_files.py +++ b/scripts/cpp/unify_move_files.py @@ -138,6 +138,9 @@ def main(): #unify_file(Game.ZEROHOUR, "GameEngine/Include/GameLogic/LogicRandomValue.h", Game.CORE, "GameEngine/Include/GameLogic/LogicRandomValue.h") #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/RandomValue.cpp", Game.CORE, "GameEngine/Source/Common/RandomValue.cpp") + #unify_file(Game.ZEROHOUR, "GameEngine/Include/Common/Debug.h", Game.CORE, "GameEngine/Include/Common/Debug.h") + #unify_file(Game.ZEROHOUR, "GameEngine/Source/Common/System/Debug.cpp", Game.CORE, "GameEngine/Source/Common/System/Debug.cpp") + return From 9f0a55eece7b13738bdc927475d493e6e43f9e23 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sun, 3 Aug 2025 23:58:09 +0200 Subject: [PATCH 23/83] feat(controlbar): Implement full viewport support for 'Control Bar Pro' Addons (#1412) Adds a -forcefullviewport command line argument and a ViewportHeightScale=0..1 field for GameData.ini --- .../GameEngine/Include/Common/GlobalData.h | 2 ++ .../Include/GameClient/ControlBar.h | 4 +++- .../GameEngine/Source/Common/CommandLine.cpp | 12 ++++++++++++ .../GameEngine/Source/Common/GameEngine.cpp | 3 ++- .../GameEngine/Source/Common/GlobalData.cpp | 19 +++++++++++++++++++ .../GameClient/GUI/ControlBar/ControlBar.cpp | 16 +++++++++++++--- .../GUI/GUICallbacks/ControlBarCallback.cpp | 16 ++++++++-------- .../GameEngine/Source/GameClient/InGameUI.cpp | 7 +++---- .../GameEngine/Include/Common/GlobalData.h | 2 ++ .../Include/GameClient/ControlBar.h | 4 +++- .../GameEngine/Source/Common/CommandLine.cpp | 12 ++++++++++++ .../GameEngine/Source/Common/GameEngine.cpp | 1 + .../GameEngine/Source/Common/GlobalData.cpp | 19 +++++++++++++++++++ .../GameClient/GUI/ControlBar/ControlBar.cpp | 16 +++++++++++++--- .../GUI/GUICallbacks/ControlBarCallback.cpp | 16 ++++++++-------- .../GameEngine/Source/GameClient/InGameUI.cpp | 7 +++---- 16 files changed, 123 insertions(+), 33 deletions(-) diff --git a/Generals/Code/GameEngine/Include/Common/GlobalData.h b/Generals/Code/GameEngine/Include/Common/GlobalData.h index 18c5c9cf0ce..842b05fa00d 100644 --- a/Generals/Code/GameEngine/Include/Common/GlobalData.h +++ b/Generals/Code/GameEngine/Include/Common/GlobalData.h @@ -91,6 +91,7 @@ class GlobalData : public SubsystemInterface Bool setTimeOfDay( TimeOfDay tod ); ///< Use this function to set the Time of day; static void parseGameDataDefinition( INI* ini ); + void parseCustomDefinition(); //----------------------------------------------------------------------------------------------- struct TerrainLighting @@ -178,6 +179,7 @@ class GlobalData : public SubsystemInterface Real m_skyBoxPositionZ; Real m_drawSkyBox; Real m_skyBoxScale; + Real m_viewportHeightScale; // The height scale of the tactical view ranging 0..1. Used to hide the world behind the Control Bar. Real m_cameraPitch; Real m_cameraYaw; Real m_cameraHeight; diff --git a/Generals/Code/GameEngine/Include/GameClient/ControlBar.h b/Generals/Code/GameEngine/Include/GameClient/ControlBar.h index ff4a2b2a991..ca76445c649 100644 --- a/Generals/Code/GameEngine/Include/GameClient/ControlBar.h +++ b/Generals/Code/GameEngine/Include/GameClient/ControlBar.h @@ -690,7 +690,9 @@ class ControlBar : public SubsystemInterface void showSpecialPowerShortcut( void ); void hideSpecialPowerShortcut( void ); void animateSpecialPowerShortcut( Bool isOn ); - + + void setFullViewportHeight(); + void setScaledViewportHeight(); /// set the control bar to the proper scheme based off a player template that's passed in ControlBarSchemeManager *getControlBarSchemeManager( void ) { return m_controlBarSchemeManager; } diff --git a/Generals/Code/GameEngine/Source/Common/CommandLine.cpp b/Generals/Code/GameEngine/Source/Common/CommandLine.cpp index 7e6eea47cfa..003e196b603 100644 --- a/Generals/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/Generals/Code/GameEngine/Source/Common/CommandLine.cpp @@ -161,6 +161,15 @@ Int parseUseWaveEditor(char *args[], int num) return 1; } +//============================================================================= +//============================================================================= +Int parseFullViewport(char *args[], int num) +{ + TheWritableGlobalData->m_viewportHeightScale = 1.0f; + + return 1; +} + #if defined(RTS_DEBUG) //============================================================================= @@ -1171,6 +1180,9 @@ static CommandLineParam paramsForEngineInit[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, + // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. + { "-forcefullviewport", parseFullViewport }, + #if defined(RTS_DEBUG) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index ec2276b8790..efcbb0446f7 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -364,7 +364,8 @@ void GameEngine::init() DEBUG_ASSERTCRASH(TheWritableGlobalData,("TheWritableGlobalData expected to be created")); initSubsystem(TheWritableGlobalData, "TheWritableGlobalData", TheWritableGlobalData, &xferCRC, "Data\\INI\\Default\\GameData.ini", "Data\\INI\\GameData.ini"); - + TheWritableGlobalData->parseCustomDefinition(); + // TheSuperHackers @bugfix helmutbuhler 14/04/2025 // Pump messages during startup to ensure that the application window is correctly // positioned on slower computers and in debug builds by a later call to SetWindowPos. diff --git a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp index dda0fb8d50f..d2b731bbe17 100644 --- a/Generals/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/Generals/Code/GameEngine/Source/Common/GlobalData.cpp @@ -176,6 +176,7 @@ GlobalData* GlobalData::m_theOriginal = NULL; { "SkyBoxPositionZ", INI::parseReal, NULL, offsetof( GlobalData, m_skyBoxPositionZ ) }, { "SkyBoxScale", INI::parseReal, NULL, offsetof( GlobalData, m_skyBoxScale ) }, { "DrawSkyBox", INI::parseBool, NULL, offsetof( GlobalData, m_drawSkyBox ) }, + { "ViewportHeightScale", INI::parseReal, NULL, offsetof( GlobalData, m_viewportHeightScale ) }, { "CameraPitch", INI::parseReal, NULL, offsetof( GlobalData, m_cameraPitch ) }, { "CameraYaw", INI::parseReal, NULL, offsetof( GlobalData, m_cameraYaw ) }, { "CameraHeight", INI::parseReal, NULL, offsetof( GlobalData, m_cameraHeight ) }, @@ -824,6 +825,8 @@ GlobalData::GlobalData() m_particleEdit = FALSE; + m_viewportHeightScale = 0.80f; // Default value for the original Control Bar. + m_cameraPitch = 0.0f; m_cameraYaw = 0.0f; m_cameraHeight = 0.0f; @@ -1193,6 +1196,22 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_yResolution = yres; } +void GlobalData::parseCustomDefinition() +{ + { + // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. + File* file = TheFileSystem->openFile("GenTool/fullviewport.dat", File::READ | File::BINARY); + if (file != NULL) + { + Char value = '0'; + file->read(&value, 1); + if (value != '0') + { + m_viewportHeightScale = 1.0f; + } + } + } +} UnsignedInt GlobalData::generateExeCRC() { diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp index 3f4edb684df..82ff28d2564 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp @@ -3008,7 +3008,7 @@ void ControlBar::setDefaultControlBarConfig( void ) // m_controlBarSchemeManager->setControlBarSchemeByPlayerTemplate(ThePlayerList->getLocalPlayer()->getPlayerTemplate(), FALSE); // } m_currentControlBarStage = CONTROL_BAR_STAGE_DEFAULT; - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + setScaledViewportHeight(); m_contextParent[ CP_MASTER ]->winSetPosition(m_defaultControlBarPosition.x, m_defaultControlBarPosition.y); m_contextParent[ CP_MASTER ]->winHide(FALSE); repopulateBuildTooltipLayout(); @@ -3025,7 +3025,7 @@ void ControlBar::setSquishedControlBarConfig( void ) // m_controlBarResizer->sizeWindowsAlt(); repopulateBuildTooltipLayout(); - TheTacticalView->setHeight((Int)(TheDisplay->getHeight())); + setFullViewportHeight(); m_controlBarSchemeManager->setControlBarSchemeByPlayerTemplate(ThePlayerList->getLocalPlayer()->getPlayerTemplate(), TRUE); } @@ -3041,7 +3041,7 @@ void ControlBar::setLowControlBarConfig( void ) ICoord2D pos; pos.x = m_defaultControlBarPosition.x; pos.y = TheDisplay->getHeight() - .1 * TheDisplay->getHeight(); - TheTacticalView->setHeight((Int)(TheDisplay->getHeight())); + setFullViewportHeight(); m_contextParent[ CP_MASTER ]->winSetPosition(pos.x, pos.y); m_contextParent[ CP_MASTER ]->winHide(FALSE); setUpDownImages(); @@ -3506,3 +3506,13 @@ void ControlBar::hideSpecialPowerShortcut( void ) m_specialPowerShortcutParent->winHide(TRUE); } + +void ControlBar::setFullViewportHeight() +{ + TheTacticalView->setHeight(TheDisplay->getHeight()); +} + +void ControlBar::setScaledViewportHeight() +{ + TheTacticalView->setHeight(TheDisplay->getHeight() * TheGlobalData->m_viewportHeightScale); +} diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp index 35cf66fc0d1..a5186df6615 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp @@ -501,7 +501,7 @@ extern void toggleReplayControls( void ); //------------------------------------------------------------------------------------------------- void ShowControlBar( Bool immediate ) { - if (!TheWindowManager || !TheControlBar || !TheTacticalView) + if (!TheWindowManager || !TheControlBar) return; showReplayControls(); @@ -514,7 +514,7 @@ void ShowControlBar( Bool immediate ) if (window) { TheControlBar->switchControlBarStage(CONTROL_BAR_STAGE_DEFAULT); - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + TheControlBar->setScaledViewportHeight(); if (TheControlBar->m_animateWindowManager && !immediate) { @@ -536,7 +536,7 @@ void ShowControlBar( Bool immediate ) //------------------------------------------------------------------------------------------------- void HideControlBar( Bool immediate ) { - if (!TheWindowManager || !TheControlBar || !TheTacticalView) + if (!TheWindowManager || !TheControlBar) return; hideReplayControls(); @@ -549,9 +549,9 @@ void HideControlBar( Bool immediate ) if (window) { #ifdef SLIDE_LETTERBOX - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + TheControlBar->setScaledViewportHeight(); #else - TheTacticalView->setHeight(TheDisplay->getHeight()); + TheControlBar->setFullViewportHeight(); #endif if (immediate) { @@ -574,7 +574,7 @@ void HideControlBar( Bool immediate ) //------------------------------------------------------------------------------------------------- void ToggleControlBar( Bool immediate ) { - if (!TheWindowManager || !TheControlBar || !TheTacticalView) + if (!TheWindowManager || !TheControlBar) return; toggleReplayControls(); @@ -589,7 +589,7 @@ void ToggleControlBar( Bool immediate ) TheControlBar->showSpecialPowerShortcut(); //now hidden, we're making it visible again so shrink viewport under the window - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + TheControlBar->setScaledViewportHeight(); window->winHide(FALSE); TheControlBar->switchControlBarStage(CONTROL_BAR_STAGE_DEFAULT); @@ -604,7 +604,7 @@ void ToggleControlBar( Bool immediate ) else { TheControlBar->hideSpecialPowerShortcut(); - TheTacticalView->setHeight(TheDisplay->getHeight()); + TheControlBar->setFullViewportHeight(); window->winHide(TRUE); } } diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index ae7332dbc1a..4bd139207c4 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1166,10 +1166,9 @@ void InGameUI::init( void ) TheTacticalView->init(); TheDisplay->attachView( TheTacticalView ); - // make the tactical display the full screen width for now - TheTacticalView->setWidth( TheDisplay->getWidth()); - // make the tactical display 0.76 of full screen so no drawing under GUI. - TheTacticalView->setHeight( TheDisplay->getHeight() * 0.77f); + // make the tactical display the full screen width and height + TheTacticalView->setWidth( TheDisplay->getWidth() ); + TheTacticalView->setHeight( TheDisplay->getHeight() ); } TheTacticalView->setDefaultView(0.0f, 0.0f, 1.0f); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 0b25e9db91d..0370d11586e 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -93,6 +93,7 @@ class GlobalData : public SubsystemInterface Bool setTimeOfDay( TimeOfDay tod ); ///< Use this function to set the Time of day; static void parseGameDataDefinition( INI* ini ); + void parseCustomDefinition(); //----------------------------------------------------------------------------------------------- struct TerrainLighting @@ -184,6 +185,7 @@ class GlobalData : public SubsystemInterface Real m_skyBoxPositionZ; Real m_drawSkyBox; Real m_skyBoxScale; + Real m_viewportHeightScale; // The height scale of the tactical view ranging 0..1. Used to hide the world behind the Control Bar. Real m_cameraPitch; Real m_cameraYaw; Real m_cameraHeight; diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h index d625d1f4a3f..5a0bd695406 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h @@ -704,7 +704,9 @@ class ControlBar : public SubsystemInterface void showSpecialPowerShortcut( void ); void hideSpecialPowerShortcut( void ); void animateSpecialPowerShortcut( Bool isOn ); - + + void setFullViewportHeight(); + void setScaledViewportHeight(); /// set the control bar to the proper scheme based off a player template that's passed in ControlBarSchemeManager *getControlBarSchemeManager( void ) { return m_controlBarSchemeManager; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index fb64d4b88f3..2cdc208133c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -161,6 +161,15 @@ Int parseUseWaveEditor(char *args[], int num) return 1; } +//============================================================================= +//============================================================================= +Int parseFullViewport(char *args[], int num) +{ + TheWritableGlobalData->m_viewportHeightScale = 1.0f; + + return 1; +} + #if defined(RTS_DEBUG) //============================================================================= @@ -1171,6 +1180,9 @@ static CommandLineParam paramsForEngineInit[] = { "-quickstart", parseQuickStart }, { "-useWaveEditor", parseUseWaveEditor }, + // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. + { "-forcefullviewport", parseFullViewport }, + #if defined(RTS_DEBUG) { "-noaudio", parseNoAudio }, { "-map", parseMapName }, diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index ea3140a0cdf..953f678bc7d 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -428,6 +428,7 @@ void GameEngine::init() DEBUG_ASSERTCRASH(TheWritableGlobalData,("TheWritableGlobalData expected to be created")); initSubsystem(TheWritableGlobalData, "TheWritableGlobalData", TheWritableGlobalData, &xferCRC, "Data\\INI\\Default\\GameData.ini", "Data\\INI\\GameData.ini"); + TheWritableGlobalData->parseCustomDefinition(); #ifdef DUMP_PERF_STATS/////////////////////////////////////////////////////////////////////////// diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index df912e90b5d..e00a5e15579 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -176,6 +176,7 @@ GlobalData* GlobalData::m_theOriginal = NULL; { "SkyBoxPositionZ", INI::parseReal, NULL, offsetof( GlobalData, m_skyBoxPositionZ ) }, { "SkyBoxScale", INI::parseReal, NULL, offsetof( GlobalData, m_skyBoxScale ) }, { "DrawSkyBox", INI::parseBool, NULL, offsetof( GlobalData, m_drawSkyBox ) }, + { "ViewportHeightScale", INI::parseReal, NULL, offsetof( GlobalData, m_viewportHeightScale ) }, { "CameraPitch", INI::parseReal, NULL, offsetof( GlobalData, m_cameraPitch ) }, { "CameraYaw", INI::parseReal, NULL, offsetof( GlobalData, m_cameraYaw ) }, { "CameraHeight", INI::parseReal, NULL, offsetof( GlobalData, m_cameraHeight ) }, @@ -830,6 +831,8 @@ GlobalData::GlobalData() m_particleEdit = FALSE; + m_viewportHeightScale = 0.80f; // Default value for the original Control Bar. + m_cameraPitch = 0.0f; m_cameraYaw = 0.0f; m_cameraHeight = 0.0f; @@ -1221,6 +1224,22 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_yResolution = yres; } +void GlobalData::parseCustomDefinition() +{ + { + // TheSuperHackers @feature xezon 03/08/2025 Force full viewport for 'Control Bar Pro' Addons like GenTool did it. + File* file = TheFileSystem->openFile("GenTool/fullviewport.dat", File::READ | File::BINARY); + if (file != NULL) + { + Char value = '0'; + file->read(&value, 1); + if (value != '0') + { + m_viewportHeightScale = 1.0f; + } + } + } +} UnsignedInt GlobalData::generateExeCRC() { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp index 0648e1451ae..67eff6a49d7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp @@ -3031,7 +3031,7 @@ void ControlBar::setDefaultControlBarConfig( void ) // m_controlBarSchemeManager->setControlBarSchemeByPlayerTemplate(ThePlayerList->getLocalPlayer()->getPlayerTemplate(), FALSE); // } m_currentControlBarStage = CONTROL_BAR_STAGE_DEFAULT; - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + setScaledViewportHeight(); m_contextParent[ CP_MASTER ]->winSetPosition(m_defaultControlBarPosition.x, m_defaultControlBarPosition.y); m_contextParent[ CP_MASTER ]->winHide(FALSE); repopulateBuildTooltipLayout(); @@ -3048,7 +3048,7 @@ void ControlBar::setSquishedControlBarConfig( void ) // m_controlBarResizer->sizeWindowsAlt(); repopulateBuildTooltipLayout(); - TheTacticalView->setHeight((Int)(TheDisplay->getHeight())); + setFullViewportHeight(); m_controlBarSchemeManager->setControlBarSchemeByPlayerTemplate(ThePlayerList->getLocalPlayer()->getPlayerTemplate(), TRUE); } @@ -3064,7 +3064,7 @@ void ControlBar::setLowControlBarConfig( void ) ICoord2D pos; pos.x = m_defaultControlBarPosition.x; pos.y = TheDisplay->getHeight() - .1 * TheDisplay->getHeight(); - TheTacticalView->setHeight((Int)(TheDisplay->getHeight())); + setFullViewportHeight(); m_contextParent[ CP_MASTER ]->winSetPosition(pos.x, pos.y); m_contextParent[ CP_MASTER ]->winHide(FALSE); setUpDownImages(); @@ -3770,3 +3770,13 @@ void ControlBar::hideSpecialPowerShortcut( void ) m_specialPowerShortcutParent->winHide(TRUE); } + +void ControlBar::setFullViewportHeight() +{ + TheTacticalView->setHeight(TheDisplay->getHeight()); +} + +void ControlBar::setScaledViewportHeight() +{ + TheTacticalView->setHeight(TheDisplay->getHeight() * TheGlobalData->m_viewportHeightScale); +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp index dda81bc4efe..fa1e12a638f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/ControlBarCallback.cpp @@ -500,7 +500,7 @@ extern void toggleReplayControls( void ); //------------------------------------------------------------------------------------------------- void ShowControlBar( Bool immediate ) { - if (!TheWindowManager || !TheControlBar || !TheTacticalView) + if (!TheWindowManager || !TheControlBar) return; showReplayControls(); @@ -513,7 +513,7 @@ void ShowControlBar( Bool immediate ) if (window) { TheControlBar->switchControlBarStage(CONTROL_BAR_STAGE_DEFAULT); - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + TheControlBar->setScaledViewportHeight(); if (TheControlBar->m_animateWindowManager && !immediate) { @@ -535,7 +535,7 @@ void ShowControlBar( Bool immediate ) //------------------------------------------------------------------------------------------------- void HideControlBar( Bool immediate ) { - if (!TheWindowManager || !TheControlBar || !TheTacticalView) + if (!TheWindowManager || !TheControlBar) return; hideReplayControls(); @@ -548,9 +548,9 @@ void HideControlBar( Bool immediate ) if (window) { #ifdef SLIDE_LETTERBOX - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + TheControlBar->setScaledViewportHeight(); #else - TheTacticalView->setHeight(TheDisplay->getHeight()); + TheControlBar->setFullViewportHeight(); #endif if (immediate) { @@ -573,7 +573,7 @@ void HideControlBar( Bool immediate ) //------------------------------------------------------------------------------------------------- void ToggleControlBar( Bool immediate ) { - if (!TheWindowManager || !TheControlBar || !TheTacticalView) + if (!TheWindowManager || !TheControlBar) return; toggleReplayControls(); @@ -588,7 +588,7 @@ void ToggleControlBar( Bool immediate ) TheControlBar->showSpecialPowerShortcut(); //now hidden, we're making it visible again so shrink viewport under the window - TheTacticalView->setHeight((Int)(TheDisplay->getHeight() * 0.80f)); + TheControlBar->setScaledViewportHeight(); window->winHide(FALSE); TheControlBar->switchControlBarStage(CONTROL_BAR_STAGE_DEFAULT); @@ -603,7 +603,7 @@ void ToggleControlBar( Bool immediate ) else { TheControlBar->hideSpecialPowerShortcut(); - TheTacticalView->setHeight(TheDisplay->getHeight()); + TheControlBar->setFullViewportHeight(); window->winHide(TRUE); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 81e719ed45b..04e459cde9e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1196,10 +1196,9 @@ void InGameUI::init( void ) TheTacticalView->init(); TheDisplay->attachView( TheTacticalView ); - // make the tactical display the full screen width for now - TheTacticalView->setWidth( TheDisplay->getWidth()); - // make the tactical display 0.76 of full screen so no drawing under GUI. - TheTacticalView->setHeight( TheDisplay->getHeight() * 0.77f); + // make the tactical display the full screen width and height + TheTacticalView->setWidth( TheDisplay->getWidth() ); + TheTacticalView->setHeight( TheDisplay->getHeight() ); } TheTacticalView->setDefaultView(0.0f, 0.0f, 1.0f); From dec4d7d687d143071edd38c1d3e91da06d413895 Mon Sep 17 00:00:00 2001 From: helmutbuhler Date: Mon, 4 Aug 2025 06:37:51 +0000 Subject: [PATCH 24/83] ci: Implement Replay Checker (#1366) --- .github/workflows/build-toolchain.yml | 4 +- .github/workflows/check-replays.yml | 240 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 43 ++++- .gitignore | 1 + .gitmodules | 3 + CMakePresets.json | 28 +++ GeneralsReplays | 1 + TESTING.md | 14 ++ 8 files changed, 329 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/check-replays.yml create mode 100644 .gitmodules create mode 160000 GeneralsReplays create mode 100644 TESTING.md diff --git a/.github/workflows/build-toolchain.yml b/.github/workflows/build-toolchain.yml index 86a28770180..216d8d82c63 100644 --- a/.github/workflows/build-toolchain.yml +++ b/.github/workflows/build-toolchain.yml @@ -28,9 +28,9 @@ on: jobs: build: - name: Preset ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} + name: ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} runs-on: windows-2022 - timeout-minutes: 40 + timeout-minutes: 20 steps: - name: Checkout Code uses: actions/checkout@v4 diff --git a/.github/workflows/check-replays.yml b/.github/workflows/check-replays.yml new file mode 100644 index 00000000000..7dca0fba615 --- /dev/null +++ b/.github/workflows/check-replays.yml @@ -0,0 +1,240 @@ +name: check-replays + +permissions: + contents: read + pull-requests: write + +on: + workflow_call: + inputs: + game: + required: true + type: string + description: "Game to check (only GeneralsMD for now)" + userdata: + required: true + type: string + description: "Path to folder with replays and maps" + preset: + required: true + type: string + description: "CMake preset" + +jobs: + build: + name: ${{ inputs.preset }} + runs-on: windows-latest + timeout-minutes: 15 + env: + GAME_PATH: C:\GameData + GENERALS_PATH: C:\GameData\Generals + GENERALSMD_PATH: C:\GameData\GeneralsMD + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Download Game Artifact + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.game }}-${{ inputs.preset }} + path: build + + - name: Cache Game Data + id: cache-gamedata + uses: actions/cache@v4 + with: + path: ${{ env.GAME_PATH }} + key: gamedata-permanent-cache-v3 + + - name: Download Game Data from Cloudflare R2 + if: ${{ steps.cache-gamedata.outputs.cache-hit != 'true' }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }} + EXPECTED_HASH_GENERALS: "37A351AA430199D1F05DEB9E404857DCE7B461A6AC272C5D4A0B5652CDB06372" + EXPECTED_HASH_GENERALSMD: "6837FE1E3009A4C239406C39B1598216C0943EE8ED46BB10626767029AC05E21" + shell: pwsh + run: | + # Download trimmed gamedata of both Generals 1.08 and Generals Zero Hour 1.04. + # This data cannot be used for playing because it's + # missing textures, audio and gui files. But it's enough for replay checking. + # It's also encrypted because it's not allowed to distribute these files. + + if (-not $env:AWS_ACCESS_KEY_ID -or -not $env:AWS_SECRET_ACCESS_KEY -or -not $env:AWS_ENDPOINT_URL) { + $ok1 = [bool]$env:AWS_ACCESS_KEY_ID + $ok2 = [bool]$env:AWS_SECRET_ACCESS_KEY + $ok3 = [bool]$env:AWS_ENDPOINT_URL + Write-Host "One or more required secrets are not set or are empty. R2_ACCESS_KEY_ID: $ok1, R2_SECRET_ACCESS_KEY: $ok2, R2_ENDPOINT_URL: $ok3" + exit 1 + } + + # Download Generals Game Files + # The archive contains these files: + # BINKW32.DLL + # English.big + # INI.big + # Maps.big + # mss32.dll + # W3D.big + # Data\Scripts\MultiplayerScripts.scb + # Data\Scripts\SkirmishScripts.scb + + Write-Host "Downloading Game Data for Generals" -ForegroundColor Cyan + aws s3 cp s3://github-ci/generals108_gamedata_trimmed.7z generals108_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL + + Write-Host "Verifying File Integrity" -ForegroundColor Cyan + $fileHash = (Get-FileHash -Path generals108_gamedata_trimmed.7z -Algorithm SHA256).Hash + Write-Host "Downloaded file SHA256: $fileHash" + Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALS" + if ($fileHash -ne $env:EXPECTED_HASH_GENERALS) { + Write-Error "Hash verification failed! File may be corrupted or tampered with." + exit 1 + } + + Write-Host "Extracting Archive" -ForegroundColor Cyan + & 7z x generals108_gamedata_trimmed.7z -o$env:GENERALS_PATH + Remove-Item generals108_gamedata_trimmed.7z -Verbose + + # Download GeneralsMD (ZH) Game Files + # The archive contains these files: + # BINKW32.DLL + # INIZH.big + # MapsZH.big + # mss32.dll + # W3DZH.big + # Data\Scripts\MultiplayerScripts.scb + # Data\Scripts\Scripts.ini + # Data\Scripts\SkirmishScripts.scb + + Write-Host "Downloading Game Data for GeneralsMD" -ForegroundColor Cyan + aws s3 cp s3://github-ci/zerohour104_gamedata_trimmed.7z zerohour104_gamedata_trimmed.7z --endpoint-url $env:AWS_ENDPOINT_URL + + Write-Host "Verifying File Integrity" -ForegroundColor Cyan + $fileHash = (Get-FileHash -Path zerohour104_gamedata_trimmed.7z -Algorithm SHA256).Hash + Write-Host "Downloaded file SHA256: $fileHash" + Write-Host "Expected file SHA256: $env:EXPECTED_HASH_GENERALSMD" + if ($fileHash -ne $env:EXPECTED_HASH_GENERALSMD) { + Write-Error "Hash verification failed! File may be corrupted or tampered with." + exit 1 + } + + Write-Host "Extracting Archive" -ForegroundColor Cyan + & 7z x zerohour104_gamedata_trimmed.7z -o$env:GENERALSMD_PATH + Remove-Item zerohour104_gamedata_trimmed.7z -Verbose + + - name: Set Up Game Data + shell: pwsh + run: | + $source = "$env:GAME_PATH\${{ inputs.game }}" + $destination = "build" + Copy-Item -Path $source\* -Destination $destination -Recurse -Force + + - name: Set Generals InstallPath in Registry + shell: pwsh + run: | + # Zero Hour loads some Generals files and needs this registry key to find the + # Generals data files. + + $regPath = "HKCU:\SOFTWARE\Electronic Arts\EA Games\Generals" + $installPath = "$env:GENERALS_PATH\" + + # Ensure the key exists + if (-not (Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + + # Set the InstallPath value + Set-ItemProperty -Path $regPath -Name InstallPath -Value $installPath -Type String + Write-Host "Registry key set: $regPath -> InstallPath = $installPath" + + - name: Move Replays and Maps to User Dir + shell: pwsh + run: | + # These files are expected in the user dir, so we move them here. + + $source = "${{ inputs.userdata }}\Replays" + $destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Replays" + Write-Host "Move replays to $destination" + New-Item -ItemType Directory -Path $destination -Force | Out-Null + Move-Item -Path "$source\*" -Destination $destination -Force + + $source = "${{ inputs.userdata }}\Maps" + $destination = "$env:USERPROFILE\Documents\Command and Conquer Generals Zero Hour Data\Maps" + Write-Host "Move maps to $destination" + New-Item -ItemType Directory -Path $destination -Force | Out-Null + Move-Item -Path "$source\*" -Destination $destination -Force + + - name: Run Replay Compatibility Tests + shell: pwsh + run: | + $exePath = "build/generalszh.exe" + $arguments = "-jobs 4 -headless -replay *.rep" + $timeoutSeconds = 10*60 + $stdoutPath = "stdout.log" + $stderrPath = "stderr.log" + + if (-not (Test-Path $exePath)) { + Write-Host "ERROR: Executable not found at $exePath" + exit 1 + } + + # Note that the game is a gui application. That means we need to redirect console output to a file + # in order to retrieve it. + # Clean previous logs + Remove-Item $stdoutPath, $stderrPath -ErrorAction SilentlyContinue + + # Start the process + Write-Host "Run $exePath $arguments" + $process = Start-Process -FilePath $exePath ` + -ArgumentList $arguments ` + -RedirectStandardOutput $stdoutPath ` + -RedirectStandardError $stderrPath ` + -PassThru + + # Wait with timeout + $exited = $process.WaitForExit($timeoutSeconds * 1000) + + if (-not $exited) { + Write-Host "ERROR: Process still running after $timeoutSeconds seconds. Killing process..." + Stop-Process -Id $process.Id -Force + } + + # Read output + Write-Host "=== STDOUT ===" + Get-Content $stdoutPath + + if ((Test-Path $stderrPath) -and (Get-Item $stderrPath).Length -gt 0) { + Write-Host "`n=== STDERR ===" + Get-Content $stderrPath + } + + if (-not $exited) { + exit 1 + } + + # Check exit code + $exitCode = $process.ExitCode + + # The above doesn't work on all Windows versions. If not, try this: (see https://stackoverflow.com/a/16018287) + #$process.HasExited | Out-Null # Needs to be called for the command below to work correctly + #$exitCode = $process.GetType().GetField('exitCode', 'NonPublic, Instance').GetValue($process) + #Write-Host "exit code $exitCode" + + if ($exitCode -ne 0) { + Write-Host "ERROR: Process failed with exit code $exitCode" + exit $exitCode + } + + Write-Host "Success!" + + - name: Upload Debug Log + if: always() + uses: actions/upload-artifact@v4 + with: + name: Replay-Debug-Log-${{ inputs.preset }} + path: build/DebugLogFile*.txt + retention-days: 30 + if-no-files-found: ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed86c978b3b..2d0a8efd65c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,7 @@ jobs: generalsmd: - 'GeneralsMD/**' shared: - - '.github/workflows/build-toolchain.yml' - - '.github/workflows/ci.yml' + - '.github/workflows/**' - 'CMakeLists.txt' - 'CMakePresets.json' - 'cmake/**' @@ -100,7 +99,9 @@ jobs: extras: ${{ matrix.extras }} secrets: inherit - build-generalsmd: + # Note build-generalsmd is split into two jobs for vc6 and win32 because replaycheck-generalsmd + # only requires the vc6 build and compiling vc6 is much faster than win32 + build-generalsmd-vc6: name: Build GeneralsMD${{ matrix.preset && '' }} needs: detect-changes if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} @@ -116,6 +117,25 @@ jobs: - preset: "vc6-debug" tools: true extras: true + - preset: "vc6-releaselog" + tools: true + extras: true + fail-fast: false + uses: ./.github/workflows/build-toolchain.yml + with: + game: "GeneralsMD" + preset: ${{ matrix.preset }} + tools: ${{ matrix.tools }} + extras: ${{ matrix.extras }} + secrets: inherit + + build-generalsmd-win32: + name: Build GeneralsMD${{ matrix.preset && '' }} + needs: detect-changes + if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} + strategy: + matrix: + include: - preset: "win32" tools: true extras: true @@ -143,3 +163,20 @@ jobs: tools: ${{ matrix.tools }} extras: ${{ matrix.extras }} secrets: inherit + + replaycheck-generalsmd: + name: Replay Check GeneralsMD${{ matrix.preset && '' }} + needs: build-generalsmd-vc6 + if: ${{ github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.generalsmd == 'true' || needs.detect-changes.outputs.shared == 'true' }} + strategy: + matrix: + include: + - preset: "vc6+t+e" + - preset: "vc6-releaselog+t+e" # optimized build with logging and crashing enabled should be compatible, so we test that here. + fail-fast: false + uses: ./.github/workflows/check-replays.yml + with: + game: "GeneralsMD" + userdata: "GeneralsReplays/GeneralsZH/1.04" + preset: ${{ matrix.preset }} + secrets: inherit diff --git a/.gitignore b/.gitignore index e5e550b48da..9ae9ce42880 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ !.gitignore !.gitattributes !.github +!.gitmodules *.user *.ncb diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..09d4419ccc5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "GeneralsReplays"] + path = GeneralsReplays + url = https://github.com/TheSuperHackers/GeneralsReplays diff --git a/CMakePresets.json b/CMakePresets.json index 51610bf95ca..6dd63816a84 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -44,6 +44,15 @@ "RTS_BUILD_OPTION_DEBUG": "ON" } }, + { + "name": "vc6-releaselog", + "displayName": "Windows 32bit VC6 Release Logging", + "inherits": "vc6", + "cacheVariables": { + "RTS_DEBUG_LOGGING": "ON", + "RTS_DEBUG_CRASHING": "ON" + } + }, { "name": "default", "displayName": "Default Config (don't use directly!)", @@ -163,6 +172,12 @@ "displayName": "Build Windows 32bit VC6 Debug", "description": "Build Windows 32bit VC6 Debug" }, + { + "name": "vc6-releaselog", + "configurePreset": "vc6-releaselog", + "displayName": "Build Windows 32bit VC6 Release Logging", + "description": "Build Windows 32bit VC6 Release Logging" + }, { "name": "win32", "configurePreset": "win32", @@ -253,6 +268,19 @@ } ] }, + { + "name": "vc6-releaselog", + "steps": [ + { + "type": "configure", + "name": "vc6-releaselog" + }, + { + "type": "build", + "name": "vc6-releaselog" + } + ] + }, { "name": "win32", "steps": [ diff --git a/GeneralsReplays b/GeneralsReplays new file mode 160000 index 00000000000..af0c61ccdcc --- /dev/null +++ b/GeneralsReplays @@ -0,0 +1 @@ +Subproject commit af0c61ccdccbf06750b0ecd2de99fe65842bfc1a diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000000..bcfed391427 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,14 @@ +# Test Replays + +The GeneralsReplays folder contains replays and the required maps that are tested in CI to ensure that the game is retail compatible. + +You can also test with these replays locally: +- Copy the replays into a subfolder in your `%USERPROFILE%/Documents/Command and Conquer Generals Zero Hour Data/Replays` folder. +- Copy the maps into `%USERPROFILE%/Documents/Command and Conquer Generals Zero Hour Data/Maps` +- Start the test with this: (copy into a .bat file next to your executable) +``` +START /B /W generalszh.exe -jobs 4 -headless -replay subfolder/*.rep > replay_check.log +echo %errorlevel% +PAUSE +``` +It will run the game in the background and check that each replay is compatible. You need to use a VC6 build with optimizations and RTS_BUILD_OPTION_DEBUG = OFF, otherwise the game won't be compatible. \ No newline at end of file From 3db937327b0975a8c5064c77613f8f34b12a1e9a Mon Sep 17 00:00:00 2001 From: helmutbuhler Date: Mon, 4 Aug 2025 07:28:39 +0000 Subject: [PATCH 25/83] ci: Fix up Replay Checker (#1417) --- .github/workflows/check-replays.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-replays.yml b/.github/workflows/check-replays.yml index 7dca0fba615..7fceb7dced6 100644 --- a/.github/workflows/check-replays.yml +++ b/.github/workflows/check-replays.yml @@ -46,7 +46,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.GAME_PATH }} - key: gamedata-permanent-cache-v3 + key: gamedata-permanent-cache-v4 - name: Download Game Data from Cloudflare R2 if: ${{ steps.cache-gamedata.outputs.cache-hit != 'true' }} @@ -95,7 +95,8 @@ jobs: } Write-Host "Extracting Archive" -ForegroundColor Cyan - & 7z x generals108_gamedata_trimmed.7z -o$env:GENERALS_PATH + $extractPath = $env:GENERALS_PATH + & 7z x generals108_gamedata_trimmed.7z -o"$extractPath" Remove-Item generals108_gamedata_trimmed.7z -Verbose # Download GeneralsMD (ZH) Game Files @@ -122,7 +123,8 @@ jobs: } Write-Host "Extracting Archive" -ForegroundColor Cyan - & 7z x zerohour104_gamedata_trimmed.7z -o$env:GENERALSMD_PATH + $extractPath = $env:GENERALSMD_PATH + & 7z x zerohour104_gamedata_trimmed.7z -o"$extractPath" Remove-Item zerohour104_gamedata_trimmed.7z -Verbose - name: Set Up Game Data From 287a23886742f001f72fce0d9cdb2baa3a77d006 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:45:09 -0400 Subject: [PATCH 26/83] - Latest network tweaks from test branch --- .../GameEngine/Source/GameNetwork/ConnectionManager.cpp | 8 -------- .../Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp | 7 +++++-- GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp | 8 ++------ .../Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp | 4 ++-- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp index d93a2fd76de..57f24b0014a 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -1163,17 +1163,9 @@ void ConnectionManager::sendRemoteCommand(NetCommandRef *msg) { if (allCommandsReady(msg->getCommand()->getExecutionFrame(), TRUE)) { UnsignedInt cushion = msg->getCommand()->getExecutionFrame() - TheGameLogic->getFrame(); if ((cushion < m_smallestPacketArrivalCushion) || (m_smallestPacketArrivalCushion == -1)) { -#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) - m_smallestPacketArrivalCushion = (float)cushion * 1.5f; -#else m_smallestPacketArrivalCushion = cushion; -#endif } -#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) - m_frameMetrics.addCushion((float)cushion * 1.5f); -#else m_frameMetrics.addCushion(cushion); -#endif // DEBUG_LOG(("Adding %d to cushion for frame %d", cushion, msg->getCommand()->getExecutionFrame())); } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp index c04f0f42956..eaa3ce04092 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp @@ -21,7 +21,10 @@ void NetworkLog(ELogVerbosity logVerbosity, const char* fmt, ...) auto now = std::chrono::system_clock::now(); auto in_time_t = std::chrono::system_clock::to_time_t(now); - std::stringstream ss; + + m_strNetworkLogFileName = std::format("{}\\GeneralsOnlineData\\GeneralsOnline.log", TheGlobalData->getPath_UserData().str()); + /* + std::stringstream ss; #if defined(_DEBUG) if (IsDebuggerPresent()) @@ -37,7 +40,7 @@ void NetworkLog(ELogVerbosity logVerbosity, const char* fmt, ...) #else ss << "GeneralsOnline.log"; #endif - m_strNetworkLogFileName = ss.str(); +*/ std::ofstream overwriteFile(m_strNetworkLogFileName); // log start msg diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp index ecd66148d2e..d66b08e79dd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/Network.cpp @@ -820,11 +820,7 @@ Bool Network::timeForNewFrame() { Real runAheadPercentage = m_runAhead * (TheGlobalData->m_networkRunAheadSlack / (Real)100.0); // If we are at least 50% into our slack, we need to slow down. if (cushion < runAheadPercentage) { __int64 oldFrameDelay = frameDelay; -#if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) - frameDelay += oldFrameDelay / 5; // temporarily decrease the frame rate by 20%. -#else - frameDelay += oldFrameDelay / 10; // temporarily decrease the frame rate by 20%. -#endif + frameDelay += oldFrameDelay / 10; // temporarily decrease the frame rate by 20%. // DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f. Adjusting frameDelay from %I64d to %I64d", cushion, runAheadPercentage, oldFrameDelay, frameDelay)); m_didSelfSlug = TRUE; // } else { @@ -837,7 +833,7 @@ Bool Network::timeForNewFrame() { // DEBUG_LOG(("Allowing a new frame, frameDelay = %I64d, curTime - m_nextFrameTime = %I64d", frameDelay, curTime - m_nextFrameTime)); // if (m_nextFrameTime + frameDelay < curTime) { - if ((m_nextFrameTime + (2 * frameDelay)) < curTime) { + if ((m_nextFrameTime + ((GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER * 2) * frameDelay)) < curTime) { // If we get too far behind on our framerate we need to reset the nextFrameTime thing. m_nextFrameTime = curTime; // DEBUG_LOG(("Initializing m_nextFrameTime to %I64d", m_nextFrameTime)); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp index dc1e7df8604..038388a48bd 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/NetworkUtil.cpp @@ -29,8 +29,8 @@ #if defined(GENERALS_ONLINE) #if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) -Int MAX_FRAMES_AHEAD = 128 * 2; -Int MIN_RUNAHEAD = 10; +Int MAX_FRAMES_AHEAD = 128; +Int MIN_RUNAHEAD = 5; Int FRAME_DATA_LENGTH = (MAX_FRAMES_AHEAD + 1) * 2; Int FRAMES_TO_KEEP = (MAX_FRAMES_AHEAD / 2) + 1; #else From 9d6ebc702b0c91da8a245c1413dabb57738146c6 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:59:15 -0400 Subject: [PATCH 27/83] reduce logging --- .../Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index 826526f0cb0..d20e9497e15 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -524,9 +524,16 @@ NetworkMesh::NetworkMesh() SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged(OnSteamNetConnectionStatusChanged); + ESteamNetworkingSocketsDebugOutputType logType = +#if defined(_DEBUG) + ESteamNetworkingSocketsDebugOutputType::k_ESteamNetworkingSocketsDebugOutputType_Debug +#else + ESteamNetworkingSocketsDebugOutputType::k_ESteamNetworkingSocketsDebugOutputType_Msg +#endif + ; - SteamNetworkingUtils()->SetGlobalConfigValueInt32(k_ESteamNetworkingConfig_LogLevel_P2PRendezvous, k_ESteamNetworkingSocketsDebugOutputType_Debug); - SteamNetworkingUtils()->SetDebugOutputFunction(ESteamNetworkingSocketsDebugOutputType::k_ESteamNetworkingSocketsDebugOutputType_Debug, [](ESteamNetworkingSocketsDebugOutputType nType, const char* pszMsg) + SteamNetworkingUtils()->SetGlobalConfigValueInt32(k_ESteamNetworkingConfig_LogLevel_P2PRendezvous, logType); + SteamNetworkingUtils()->SetDebugOutputFunction(logType, [](ESteamNetworkingSocketsDebugOutputType nType, const char* pszMsg) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[STEAM NETWORKING LOGFUNC] %s", pszMsg); }); From 40afe821e3d2a3eb69572406b125c101b1721055 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 2 Aug 2025 19:59:48 +0200 Subject: [PATCH 28/83] unify(video): Merge Video code (#1410) --- Core/GameEngineDevice/CMakeLists.txt | 2 +- Generals/Code/GameEngineDevice/CMakeLists.txt | 2 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 2 +- .../Source/W3DDevice/GameClient/W3DShroud.cpp | 3 +- .../W3DDevice/GameClient/W3DVideoBuffer.cpp | 5 +- .../Source/WWVegas/WW3D2/textureloader.cpp | 59 ++++++++++++------- .../Source/WWVegas/WW3D2/textureloader.h | 2 +- .../Source/WWVegas/WW3D2/texturethumbnail.cpp | 3 +- .../Code/GameEngineDevice/CMakeLists.txt | 2 +- .../VideoDevice/Bink/BinkVideoPlayer.cpp | 2 +- .../W3DDevice/GameClient/W3DDisplay.cpp | 2 +- .../W3DDevice/GameClient/W3DVideoBuffer.cpp | 2 +- 12 files changed, 52 insertions(+), 34 deletions(-) diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 9996b8a8eac..da9169a8464 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -70,7 +70,7 @@ set(GAMEENGINEDEVICE_SRC # Include/W3DDevice/GameClient/W3DTerrainTracks.h # Include/W3DDevice/GameClient/W3DTerrainVisual.h # Include/W3DDevice/GameClient/W3DTreeBuffer.h -# Include/W3DDevice/GameClient/W3DVideobuffer.h +# Include/W3DDevice/GameClient/W3DVideoBuffer.h # Include/W3DDevice/GameClient/W3DView.h # Include/W3DDevice/GameClient/W3DVolumetricShadow.h # Include/W3DDevice/GameClient/W3DWater.h diff --git a/Generals/Code/GameEngineDevice/CMakeLists.txt b/Generals/Code/GameEngineDevice/CMakeLists.txt index bf2aef874bd..8cb339812c8 100644 --- a/Generals/Code/GameEngineDevice/CMakeLists.txt +++ b/Generals/Code/GameEngineDevice/CMakeLists.txt @@ -59,7 +59,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DTerrainTracks.h Include/W3DDevice/GameClient/W3DTerrainVisual.h Include/W3DDevice/GameClient/W3DTreeBuffer.h - Include/W3DDevice/GameClient/W3DVideobuffer.h + Include/W3DDevice/GameClient/W3DVideoBuffer.h Include/W3DDevice/GameClient/W3DView.h Include/W3DDevice/GameClient/W3DVolumetricShadow.h Include/W3DDevice/GameClient/W3DWater.h diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index a03ed940b88..cf477abe061 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -74,7 +74,7 @@ static void drawFramerateBar(void); #include "W3DDevice/GameClient/W3DScene.h" #include "W3DDevice/GameClient/W3DTerrainTracks.h" #include "W3DDevice/GameClient/W3DWater.h" -#include "W3DDevice/GameClient/W3DVideobuffer.h" +#include "W3DDevice/GameClient/W3DVideoBuffer.h" #include "W3DDevice/GameClient/W3DShaderManager.h" #include "W3DDevice/GameClient/W3DDebugDisplay.h" #include "W3DDevice/GameClient/W3DProjectedShadow.h" diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShroud.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShroud.cpp index be441cbf743..b70abf8fe0e 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShroud.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DShroud.cpp @@ -128,8 +128,9 @@ void W3DShroud::init(WorldHeightMap *pMap, Real worldCellSizeX, Real worldCellSi dstTextureWidth=m_numMaxVisibleCellsX=REAL_TO_INT_FLOOR((Real)(pMap->getDrawWidth()-1)*MAP_XY_FACTOR/m_cellWidth)+1; dstTextureHeight=m_numMaxVisibleCellsY=REAL_TO_INT_FLOOR((Real)(pMap->getDrawHeight()-1)*MAP_XY_FACTOR/m_cellHeight)+1; dstTextureWidth += 2; //enlarge by 2 pixels so we can have a border color all the way around. + unsigned int depth = 1; dstTextureHeight += 2; //enlarge by 2 pixels so we can have border color all the way around. - TextureLoader::Validate_Texture_Size((unsigned int &)dstTextureWidth,(unsigned int &)dstTextureHeight); + TextureLoader::Validate_Texture_Size((unsigned int &)dstTextureWidth,(unsigned int &)dstTextureHeight, depth); } UnsignedInt srcWidth,srcHeight; diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp index 4f7701d094c..0d17ff3b2d8 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp @@ -48,7 +48,7 @@ #include "Common/GameMemory.h" #include "WW3D2/texture.h" #include "WW3D2/textureloader.h" -#include "W3DDevice/GameClient/W3DVideobuffer.h" +#include "W3DDevice/GameClient/W3DVideoBuffer.h" //---------------------------------------------------------------------------- // Externals @@ -122,7 +122,8 @@ Bool W3DVideoBuffer::allocate( UnsignedInt width, UnsignedInt height ) m_height = height; m_textureWidth = width;; m_textureHeight = height;; - TextureLoader::Validate_Texture_Size( m_textureWidth, m_textureHeight); + unsigned int temp_depth=1; + TextureLoader::Validate_Texture_Size( m_textureWidth, m_textureHeight, temp_depth); WW3DFormat w3dFormat = TypeToW3DFormat( m_format ); diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp index 341d8e4c450..c9554b12769 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.cpp @@ -125,51 +125,66 @@ void TextureLoader::Deinit() // // ---------------------------------------------------------------------------- -void TextureLoader::Validate_Texture_Size(unsigned& width, unsigned& height) +void TextureLoader::Validate_Texture_Size +( + unsigned& width, + unsigned& height, + unsigned& depth +) { const D3DCAPS8& dx8caps=DX8Wrapper::Get_Current_Caps()->Get_DX8_Caps(); unsigned poweroftwowidth = 1; - while (poweroftwowidth < width) { + while (poweroftwowidth < width) + { poweroftwowidth <<= 1; } unsigned poweroftwoheight = 1; - while (poweroftwoheight < height) { + while (poweroftwoheight < height) + { poweroftwoheight <<= 1; } -// unsigned size = MAX (width, height); -// unsigned poweroftwosize = 1; -// while (poweroftwosize < size) { -// poweroftwosize <<= 1; -// } + unsigned poweroftwodepth = 1; + while (poweroftwodepth< depth) + { + poweroftwodepth <<= 1; + } - if (poweroftwowidth>dx8caps.MaxTextureWidth) { + if (poweroftwowidth>dx8caps.MaxTextureWidth) + { poweroftwowidth=dx8caps.MaxTextureWidth; } - if (poweroftwoheight>dx8caps.MaxTextureHeight) { + if (poweroftwoheight>dx8caps.MaxTextureHeight) + { poweroftwoheight=dx8caps.MaxTextureHeight; } + if (poweroftwodepth>dx8caps.MaxVolumeExtent) + { + poweroftwodepth=dx8caps.MaxVolumeExtent; + } - if (poweroftwowidth>poweroftwoheight) { - while (poweroftwowidth/poweroftwoheight>8) { + if (poweroftwowidth>poweroftwoheight) + { + while (poweroftwowidth/poweroftwoheight>8) + { poweroftwoheight*=2; } } - else { - while (poweroftwoheight/poweroftwowidth>8) { + else + { + while (poweroftwoheight/poweroftwowidth>8) + { poweroftwowidth*=2; } } -// width = height = poweroftwosize; width=poweroftwowidth; height=poweroftwoheight; + depth=poweroftwodepth; } - - IDirect3DTexture8* TextureLoader::Load_Thumbnail(const StringClass& filename,WW3DFormat texture_format) { ThumbnailClass* thumb=ThumbnailClass::Peek_Instance(filename); @@ -1031,10 +1046,10 @@ void TextureLoadTaskClass::Begin_Texture_Load() DDSFileClass dds_file(Texture->Get_Full_Path(),Get_Reduction()); if (dds_file.Is_Available()) { // Destination size will be the next power of two square from the larger width and height... - unsigned width, height; + unsigned width, height, depth; width=dds_file.Get_Width(0); height=dds_file.Get_Height(0); - TextureLoader::Validate_Texture_Size(width,height); + TextureLoader::Validate_Texture_Size(width,height,depth); // If the size doesn't match, try and see if texture reduction would help... (mainly for // cases where loaded texture is larger than hardware limit) @@ -1042,7 +1057,7 @@ void TextureLoadTaskClass::Begin_Texture_Load() for (unsigned i=1;i>ReductionFactor; unsigned ow=width; unsigned oh=height; - TextureLoader::Validate_Texture_Size(width,height); + TextureLoader::Validate_Texture_Size(width,height,depth); if (width!=ow || height!=oh) { WWDEBUG_SAY(("Invalid texture size, scaling required. Texture: %s, size: %d x %d -> %d x %d",Texture->Get_Full_Path().str(),ow,oh,width,height)); } diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.h b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.h index 7fd9091edb1..5f02b36bfdd 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.h +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/textureloader.h @@ -41,7 +41,7 @@ class TextureLoader static void Deinit(); // Modify given texture size to nearest valid size on current hardware. - static void Validate_Texture_Size(unsigned& width, unsigned& height); + static void Validate_Texture_Size(unsigned& width, unsigned& height, unsigned& depth); // Adds a loading task to the system. The task if processed in a separate // thread as soon as possible. The task will appear in finished tasks list diff --git a/Generals/Code/Libraries/Source/WWVegas/WW3D2/texturethumbnail.cpp b/Generals/Code/Libraries/Source/WWVegas/WW3D2/texturethumbnail.cpp index 4a9ebd2f2a0..c617becde4b 100644 --- a/Generals/Code/Libraries/Source/WWVegas/WW3D2/texturethumbnail.cpp +++ b/Generals/Code/Libraries/Source/WWVegas/WW3D2/texturethumbnail.cpp @@ -92,7 +92,8 @@ ThumbnailClass::ThumbnailClass(const StringClass& filename) // Destination size will be the next power of two square from the larger width and height... Width=targa.Header.Width>>reduction_factor; Height=targa.Header.Height>>reduction_factor; - TextureLoader::Validate_Texture_Size(Width,Height); + unsigned depth; + TextureLoader::Validate_Texture_Size(Width,Height,depth); unsigned src_width=targa.Header.Width; unsigned src_height=targa.Header.Height; diff --git a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt index b352d5f4200..c9dc9f0c15d 100644 --- a/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngineDevice/CMakeLists.txt @@ -70,7 +70,7 @@ set(GAMEENGINEDEVICE_SRC Include/W3DDevice/GameClient/W3DTerrainTracks.h Include/W3DDevice/GameClient/W3DTerrainVisual.h Include/W3DDevice/GameClient/W3DTreeBuffer.h - Include/W3DDevice/GameClient/W3DVideobuffer.h + Include/W3DDevice/GameClient/W3DVideoBuffer.h Include/W3DDevice/GameClient/W3DView.h Include/W3DDevice/GameClient/W3DVolumetricShadow.h Include/W3DDevice/GameClient/W3DWater.h diff --git a/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp b/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp index 9cd7f34dee7..819d1e1dbe4 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp @@ -225,7 +225,7 @@ VideoStreamInterface* BinkVideoPlayer::open( AsciiString movieTitle ) const Video* pVideo = getVideo(movieTitle); if (pVideo) { DEBUG_LOG(("BinkVideoPlayer::createStream() - About to open bink file")); - + if (TheGlobalData->m_modDir.isNotEmpty()) { char filePath[ _MAX_PATH ]; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 679a9e29d0e..8ea6b951046 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -75,7 +75,7 @@ static void drawFramerateBar(void); #include "W3DDevice/GameClient/W3DScene.h" #include "W3DDevice/GameClient/W3DTerrainTracks.h" #include "W3DDevice/GameClient/W3DWater.h" -#include "W3DDevice/GameClient/W3DVideobuffer.h" +#include "W3DDevice/GameClient/W3DVideoBuffer.h" #include "W3DDevice/GameClient/W3DShaderManager.h" #include "W3DDevice/GameClient/W3DDebugDisplay.h" #include "W3DDevice/GameClient/W3DProjectedShadow.h" diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp index 8ff77192ef2..e71dbf9ef75 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp @@ -48,7 +48,7 @@ #include "Common/GameMemory.h" #include "WW3D2/texture.h" #include "WW3D2/textureloader.h" -#include "W3DDevice/GameClient/W3DVideobuffer.h" +#include "W3DDevice/GameClient/W3DVideoBuffer.h" //---------------------------------------------------------------------------- // Externals From f9913a565ad5d357f1bbd77345ed4863349adc79 Mon Sep 17 00:00:00 2001 From: xezon <4720891+xezon@users.noreply.github.com> Date: Sat, 2 Aug 2025 23:42:18 +0200 Subject: [PATCH 29/83] unify(video): Move Video files to Core (#1410) --- Core/GameEngine/CMakeLists.txt | 12 +- .../Include/GameClient/VideoPlayer.h | 0 .../Include/GameClient/WindowVideoManager.h | 0 .../GameEngine/Source/Common/INI/INIVideo.cpp | 0 .../GameClient/GUI/WindowVideoManager.cpp | 0 .../Source/GameClient/VideoPlayer.cpp | 0 .../Source/GameClient/VideoStream.cpp | 0 Core/GameEngineDevice/CMakeLists.txt | 41 +- .../VideoDevice/Bink/BinkVideoPlayer.h | 0 .../Include/VideoDevice/FFmpeg/FFmpegFile.h | 0 .../VideoDevice/FFmpeg/FFmpegVideoPlayer.h | 0 .../W3DDevice/GameClient/W3DVideoBuffer.h | 0 .../VideoDevice/Bink/BinkVideoPlayer.cpp | 0 .../Source/VideoDevice/FFmpeg/FFmpegFile.cpp | 0 .../VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp | 0 .../W3DDevice/GameClient/W3DVideoBuffer.cpp | 0 Generals/Code/GameEngine/CMakeLists.txt | 10 +- .../Include/GameClient/VideoPlayer.h | 317 ----------- .../Include/GameClient/WindowVideoManager.h | 187 ------- .../GameEngine/Source/Common/INI/INIVideo.cpp | 61 --- .../GameClient/GUI/WindowVideoManager.cpp | 420 -------------- .../Source/GameClient/VideoPlayer.cpp | 512 ------------------ .../Source/GameClient/VideoStream.cpp | 91 ---- Generals/Code/GameEngineDevice/CMakeLists.txt | 8 +- .../VideoDevice/Bink/BinkVideoPlayer.h | 143 ----- .../W3DDevice/GameClient/W3DVideobuffer.h | 105 ---- .../VideoDevice/Bink/BinkVideoPlayer.cpp | 448 --------------- .../W3DDevice/GameClient/W3DVideoBuffer.cpp | 282 ---------- GeneralsMD/Code/GameEngine/CMakeLists.txt | 12 +- .../Code/GameEngineDevice/CMakeLists.txt | 26 +- scripts/cpp/unify_move_files.py | 15 + 31 files changed, 60 insertions(+), 2630 deletions(-) rename {GeneralsMD/Code => Core}/GameEngine/Include/GameClient/VideoPlayer.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Include/GameClient/WindowVideoManager.h (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/Common/INI/INIVideo.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/GameClient/GUI/WindowVideoManager.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/GameClient/VideoPlayer.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngine/Source/GameClient/VideoStream.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Include/VideoDevice/Bink/BinkVideoPlayer.h (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegFile.h (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h (100%) rename GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideobuffer.h => Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideoBuffer.h (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegFile.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp (100%) rename {GeneralsMD/Code => Core}/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp (100%) delete mode 100644 Generals/Code/GameEngine/Include/GameClient/VideoPlayer.h delete mode 100644 Generals/Code/GameEngine/Include/GameClient/WindowVideoManager.h delete mode 100644 Generals/Code/GameEngine/Source/Common/INI/INIVideo.cpp delete mode 100644 Generals/Code/GameEngine/Source/GameClient/GUI/WindowVideoManager.cpp delete mode 100644 Generals/Code/GameEngine/Source/GameClient/VideoPlayer.cpp delete mode 100644 Generals/Code/GameEngine/Source/GameClient/VideoStream.cpp delete mode 100644 Generals/Code/GameEngineDevice/Include/VideoDevice/Bink/BinkVideoPlayer.h delete mode 100644 Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideobuffer.h delete mode 100644 Generals/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp delete mode 100644 Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 689e0004307..0c91e47cc5d 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -224,11 +224,11 @@ set(GAMEENGINE_SRC # Include/GameClient/Statistics.h # Include/GameClient/TerrainRoads.h # Include/GameClient/TerrainVisual.h -# Include/GameClient/VideoPlayer.h + Include/GameClient/VideoPlayer.h # Include/GameClient/View.h # Include/GameClient/Water.h # Include/GameClient/WindowLayout.h -# Include/GameClient/WindowVideoManager.h + Include/GameClient/WindowVideoManager.h # Include/GameClient/WindowXlat.h # Include/GameClient/WinInstanceData.h # Include/GameLogic/AI.h @@ -597,7 +597,7 @@ set(GAMEENGINE_SRC # Source/Common/INI/INITerrainBridge.cpp # Source/Common/INI/INITerrainRoad.cpp # Source/Common/INI/INIUpgrade.cpp -# Source/Common/INI/INIVideo.cpp + Source/Common/INI/INIVideo.cpp # Source/Common/INI/INIWater.cpp # Source/Common/INI/INIWeapon.cpp # Source/Common/INI/INIWebpageURL.cpp @@ -791,7 +791,7 @@ set(GAMEENGINE_SRC # Source/GameClient/GUI/Shell/Shell.cpp # Source/GameClient/GUI/Shell/ShellMenuScheme.cpp # Source/GameClient/GUI/WindowLayout.cpp -# Source/GameClient/GUI/WindowVideoManager.cpp + Source/GameClient/GUI/WindowVideoManager.cpp # Source/GameClient/GUI/WinInstanceData.cpp # Source/GameClient/InGameUI.cpp # Source/GameClient/Input/Keyboard.cpp @@ -823,8 +823,8 @@ set(GAMEENGINE_SRC # Source/GameClient/System/Smudge.cpp # Source/GameClient/Terrain/TerrainRoads.cpp # Source/GameClient/Terrain/TerrainVisual.cpp -# Source/GameClient/VideoPlayer.cpp -# Source/GameClient/VideoStream.cpp + Source/GameClient/VideoPlayer.cpp + Source/GameClient/VideoStream.cpp # Source/GameClient/View.cpp # Source/GameClient/Water.cpp # Source/GameLogic/AI/AI.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/VideoPlayer.h b/Core/GameEngine/Include/GameClient/VideoPlayer.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/GameClient/VideoPlayer.h rename to Core/GameEngine/Include/GameClient/VideoPlayer.h diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/WindowVideoManager.h b/Core/GameEngine/Include/GameClient/WindowVideoManager.h similarity index 100% rename from GeneralsMD/Code/GameEngine/Include/GameClient/WindowVideoManager.h rename to Core/GameEngine/Include/GameClient/WindowVideoManager.h diff --git a/GeneralsMD/Code/GameEngine/Source/Common/INI/INIVideo.cpp b/Core/GameEngine/Source/Common/INI/INIVideo.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/Common/INI/INIVideo.cpp rename to Core/GameEngine/Source/Common/INI/INIVideo.cpp diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/WindowVideoManager.cpp b/Core/GameEngine/Source/GameClient/GUI/WindowVideoManager.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/GameClient/GUI/WindowVideoManager.cpp rename to Core/GameEngine/Source/GameClient/GUI/WindowVideoManager.cpp diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/VideoPlayer.cpp b/Core/GameEngine/Source/GameClient/VideoPlayer.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/GameClient/VideoPlayer.cpp rename to Core/GameEngine/Source/GameClient/VideoPlayer.cpp diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/VideoStream.cpp b/Core/GameEngine/Source/GameClient/VideoStream.cpp similarity index 100% rename from GeneralsMD/Code/GameEngine/Source/GameClient/VideoStream.cpp rename to Core/GameEngine/Source/GameClient/VideoStream.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index da9169a8464..404c1eb60ec 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -1,6 +1,6 @@ set(GAMEENGINEDEVICE_SRC Include/MilesAudioDevice/MilesAudioManager.h -# Include/VideoDevice/Bink/BinkVideoPlayer.h + Include/VideoDevice/Bink/BinkVideoPlayer.h # Include/W3DDevice/Common/W3DConvert.h # Include/W3DDevice/Common/W3DFunctionLexicon.h # Include/W3DDevice/Common/W3DModuleFactory.h @@ -70,7 +70,7 @@ set(GAMEENGINEDEVICE_SRC # Include/W3DDevice/GameClient/W3DTerrainTracks.h # Include/W3DDevice/GameClient/W3DTerrainVisual.h # Include/W3DDevice/GameClient/W3DTreeBuffer.h -# Include/W3DDevice/GameClient/W3DVideoBuffer.h + Include/W3DDevice/GameClient/W3DVideoBuffer.h # Include/W3DDevice/GameClient/W3DView.h # Include/W3DDevice/GameClient/W3DVolumetricShadow.h # Include/W3DDevice/GameClient/W3DWater.h @@ -91,7 +91,7 @@ set(GAMEENGINEDEVICE_SRC # Include/Win32Device/GameClient/Win32DIMouse.h # Include/Win32Device/GameClient/Win32Mouse.h Source/MilesAudioDevice/MilesAudioManager.cpp -# Source/VideoDevice/Bink/BinkVideoPlayer.cpp + Source/VideoDevice/Bink/BinkVideoPlayer.cpp # Source/W3DDevice/Common/System/W3DFunctionLexicon.cpp # Source/W3DDevice/Common/System/W3DRadar.cpp # Source/W3DDevice/Common/Thing/W3DModuleFactory.cpp @@ -172,7 +172,7 @@ set(GAMEENGINEDEVICE_SRC # Source/W3DDevice/GameClient/W3DTerrainTracks.cpp # Source/W3DDevice/GameClient/W3DTerrainVisual.cpp # Source/W3DDevice/GameClient/W3DTreeBuffer.cpp -# Source/W3DDevice/GameClient/W3DVideoBuffer.cpp + Source/W3DDevice/GameClient/W3DVideoBuffer.cpp # Source/W3DDevice/GameClient/W3DView.cpp # Source/W3DDevice/GameClient/W3dWaypointBuffer.cpp # Source/W3DDevice/GameClient/W3DWebBrowser.cpp @@ -229,20 +229,19 @@ target_link_libraries(corei_gameenginedevice_public INTERFACE milesstub ) -#if(RTS_BUILD_OPTION_FFMPEG) -# find_package(FFMPEG REQUIRED) -# -# if(FFMPEG_FOUND) -# target_sources(z_gameenginedevice PRIVATE -# Include/VideoDevice/FFmpeg/FFmpegFile.h -# Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h -# Source/VideoDevice/FFmpeg/FFmpegFile.cpp -# Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp -# ) -# -# target_include_directories(z_gameenginedevice PRIVATE ${FFMPEG_INCLUDE_DIRS}) -# target_link_directories(z_gameenginedevice PRIVATE ${FFMPEG_LIBRARY_DIRS}) -# target_link_libraries(z_gameenginedevice PRIVATE ${FFMPEG_LIBRARIES}) -# target_compile_definitions(z_gameenginedevice PUBLIC RTS_HAS_FFMPEG) -# endif() -#endif() +if(RTS_BUILD_OPTION_FFMPEG) + find_package(FFMPEG REQUIRED) + + if(FFMPEG_FOUND) + target_sources(corei_gameenginedevice_private INTERFACE + Include/VideoDevice/FFmpeg/FFmpegFile.h + Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h + Source/VideoDevice/FFmpeg/FFmpegFile.cpp + Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp + ) + target_include_directories(corei_gameenginedevice_private INTERFACE ${FFMPEG_INCLUDE_DIRS}) + target_link_directories(corei_gameenginedevice_private INTERFACE ${FFMPEG_LIBRARY_DIRS}) + target_link_libraries(corei_gameenginedevice_private INTERFACE ${FFMPEG_LIBRARIES}) + target_compile_definitions(corei_gameenginedevice_public INTERFACE RTS_HAS_FFMPEG) + endif() +endif() diff --git a/GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/Bink/BinkVideoPlayer.h b/Core/GameEngineDevice/Include/VideoDevice/Bink/BinkVideoPlayer.h similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/Bink/BinkVideoPlayer.h rename to Core/GameEngineDevice/Include/VideoDevice/Bink/BinkVideoPlayer.h diff --git a/GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegFile.h b/Core/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegFile.h similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegFile.h rename to Core/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegFile.h diff --git a/GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h b/Core/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h rename to Core/GameEngineDevice/Include/VideoDevice/FFmpeg/FFmpegVideoPlayer.h diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideobuffer.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideoBuffer.h similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideobuffer.h rename to Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DVideoBuffer.h diff --git a/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp b/Core/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp rename to Core/GameEngineDevice/Source/VideoDevice/Bink/BinkVideoPlayer.cpp diff --git a/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegFile.cpp b/Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegFile.cpp similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegFile.cpp rename to Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegFile.cpp diff --git a/GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp b/Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp rename to Core/GameEngineDevice/Source/VideoDevice/FFmpeg/FFmpegVideoPlayer.cpp diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp similarity index 100% rename from GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp rename to Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DVideoBuffer.cpp diff --git a/Generals/Code/GameEngine/CMakeLists.txt b/Generals/Code/GameEngine/CMakeLists.txt index 1bb7173ff51..41fd59b68b8 100644 --- a/Generals/Code/GameEngine/CMakeLists.txt +++ b/Generals/Code/GameEngine/CMakeLists.txt @@ -213,11 +213,11 @@ set(GAMEENGINE_SRC Include/GameClient/Statistics.h Include/GameClient/TerrainRoads.h Include/GameClient/TerrainVisual.h - Include/GameClient/VideoPlayer.h +# Include/GameClient/VideoPlayer.h Include/GameClient/View.h Include/GameClient/Water.h Include/GameClient/WindowLayout.h - Include/GameClient/WindowVideoManager.h +# Include/GameClient/WindowVideoManager.h Include/GameClient/WindowXlat.h Include/GameClient/WinInstanceData.h Include/GameLogic/AI.h @@ -556,7 +556,7 @@ set(GAMEENGINE_SRC Source/Common/INI/INITerrainBridge.cpp Source/Common/INI/INITerrainRoad.cpp Source/Common/INI/INIUpgrade.cpp - Source/Common/INI/INIVideo.cpp +# Source/Common/INI/INIVideo.cpp Source/Common/INI/INIWater.cpp Source/Common/INI/INIWeapon.cpp Source/Common/INI/INIWebpageURL.cpp @@ -746,7 +746,7 @@ set(GAMEENGINE_SRC Source/GameClient/GUI/Shell/Shell.cpp Source/GameClient/GUI/Shell/ShellMenuScheme.cpp Source/GameClient/GUI/WindowLayout.cpp - Source/GameClient/GUI/WindowVideoManager.cpp +# Source/GameClient/GUI/WindowVideoManager.cpp Source/GameClient/GUI/WinInstanceData.cpp Source/GameClient/InGameUI.cpp Source/GameClient/Input/Keyboard.cpp @@ -775,7 +775,7 @@ set(GAMEENGINE_SRC Source/GameClient/System/RayEffect.cpp Source/GameClient/Terrain/TerrainRoads.cpp Source/GameClient/Terrain/TerrainVisual.cpp - Source/GameClient/VideoPlayer.cpp +# Source/GameClient/VideoPlayer.cpp Source/GameClient/View.cpp Source/GameClient/Water.cpp Source/GameLogic/AI/AI.cpp diff --git a/Generals/Code/GameEngine/Include/GameClient/VideoPlayer.h b/Generals/Code/GameEngine/Include/GameClient/VideoPlayer.h deleted file mode 100644 index 1d9a365a133..00000000000 --- a/Generals/Code/GameEngine/Include/GameClient/VideoPlayer.h +++ /dev/null @@ -1,317 +0,0 @@ -/* -** Command & Conquer Generals(tm) -** Copyright 2025 Electronic Arts Inc. -** -** This program is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program. If not, see . -*/ - -//////////////////////////////////////////////////////////////////////////////// -// // -// (c) 2001-2003 Electronic Arts Inc. // -// // -//////////////////////////////////////////////////////////////////////////////// - -//---------------------------------------------------------------------------- -// -// Westwood Studios Pacific. -// -// Confidential Information -// Copyright (C) 2001 - All Rights Reserved -// -//---------------------------------------------------------------------------- -// -// Project: Generals -// -// File name: GameClient/VideoPlayer.h -// -// Created: 10/22/01 -// -//---------------------------------------------------------------------------- - -#pragma once - -#ifndef __GAMECLIENT_VIDEOPLAYER_H_ -#define __GAMECLIENT_VIDEOPLAYER_H_ - - -//---------------------------------------------------------------------------- -// Includes -//---------------------------------------------------------------------------- - -#include -#include "WWMath/rect.h" -#include "Common/SubsystemInterface.h" -#include "Common/AsciiString.h" -#include "Common/INI.h" -#include "Common/STLTypedefs.h" - -//---------------------------------------------------------------------------- -// Forward References -//---------------------------------------------------------------------------- - -struct Video; -class VideoPlayer; - -//---------------------------------------------------------------------------- -// Type Defines -//---------------------------------------------------------------------------- -typedef std::vector