//========= Copyright Valve Corporation, All rights reserved. ============// // ccs.cpp - "con command server" // Not to be confused with the game's server - this is a completely different thing that allows multiple game clients to connect to a single client, // so con commands can be sent simultaneously to multiple running game clients, screen captures can be sent in real-time to a single client for comparison purposes, // and all convar's can be diff'd between all connected clients and the server. // The server portion of this code needs socketlib, which hasn't been ported to Linux yet, and shouldn't be shipped because it could be a security risk // (it's only intended for primarily graphics debugging and detailed comparisons of GL vs. D3D9). #include "host_state.h" //#define CON_COMMAND_SERVER_SUPPORT #include "tier2/tier2.h" #include "materialsystem/imaterialsystemhardwareconfig.h" #ifdef CON_COMMAND_SERVER_SUPPORT #include #include "socketlib/socketlib.h" #define MINIZ_NO_ARCHIVE_APIS #include "../../thirdparty/miniz/miniz.c" #include "../../thirdparty/miniz/simple_bitmap.h" #define STBI_NO_STDIO #include "../../thirdparty/stb_image/stb_image.c" #include "ivideomode.h" #endif #include "cmd.h" #include "filesystem.h" #include "render.h" #include "icliententitylist.h" #include "client.h" #include "icliententity.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" //----------------------------------------------------------------------------- #ifdef STAGING_ONLY CON_COMMAND_F( ccs_write_convars, "Write all convars to file.", FCVAR_CHEAT ) { FILE* pFile = fopen( "convars.txt", "w" ); if ( !pFile ) { ConMsg( "Unable to open convars.txt\n" ); return; } ICvar::Iterator iter( g_pCVar ); for ( iter.SetFirst() ; iter.IsValid() ; iter.Next() ) { ConCommandBase *var = iter.Get(); ConVar *pConVar = dynamic_cast< ConVar* >( var ); if ( !pConVar ) continue; const char *pName = pConVar->GetName(); const char *pVal = pConVar->GetString(); if ( ( !pName ) || ( !pVal ) ) continue; fprintf( pFile, "%s=%s\n", pName, pVal ); } fclose( pFile ); ConMsg( "Wrote all convars to convars.txt\n" ); } #endif //----------------------------------------------------------------------------- #ifdef CON_COMMAND_SERVER_SUPPORT class frame_buf_window { friend LRESULT CALLBACK static_window_proc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK static_window_proc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { frame_buf_window* pApp = reinterpret_cast(GetWindowLong(hWnd, 0)); if (!pApp) pApp = frame_buf_window::m_pCur_app; Assert(pApp); return pApp->window_proc(hWnd, message, wParam, lParam); } public: frame_buf_window(const char* pTitle = "frame_buf_window", int width = 640, int height = 480, int scaleX = 1, int scaleY = 1); virtual ~frame_buf_window(); // Return true if you handle the window message. typedef bool (*user_window_proc_ptr_t)(LRESULT& hres, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, void *pData); void set_window_proc_callback(user_window_proc_ptr_t windowProcPtr, void *pData) { m_pWindow_proc = windowProcPtr; m_pWindow_proc_data = pData; } int width(void) const { return m_width; } int height(void) const { return m_height; } void update(void); void close(void); const simple_bgr_bitmap& frameBuffer(void) const { return m_frame_buffer; } simple_bgr_bitmap& frameBuffer(void) { return m_frame_buffer; } private: int m_width, m_height; int m_orig_width, m_orig_height; int m_orig_x, m_orig_y; int m_scale_x, m_scale_y; WNDCLASS m_window_class; HWND m_window; char m_bitmap_info[16 + sizeof(BITMAPINFO)]; BITMAPINFO* m_pBitmap_hdr; HDC m_windowHDC; simple_bgr_bitmap m_frame_buffer; static frame_buf_window* m_pCur_app; user_window_proc_ptr_t m_pWindow_proc; void *m_pWindow_proc_data; void create_window(const char* pTitle); void create_bitmap(void); LRESULT window_proc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); }; // class frame_buf_window frame_buf_window* frame_buf_window::m_pCur_app; frame_buf_window::frame_buf_window(const char* pTitle, int width, int height, int scaleX, int scaleY) : m_width(width), m_height(height), m_orig_width(0), m_orig_height(0), m_orig_x(0), m_orig_y(0), m_window(0), m_pBitmap_hdr(NULL), m_windowHDC(0), m_pWindow_proc(NULL), m_pWindow_proc_data(NULL) { m_scale_x = scaleX; m_scale_y = scaleY; create_window(pTitle); create_bitmap(); } frame_buf_window::~frame_buf_window() { close(); } void frame_buf_window::update(void) { if ( m_window ) { InvalidateRect(m_window, NULL, FALSE); SendMessage(m_window, WM_PAINT, 0, 0); MSG msg; while (PeekMessage(&msg, m_window, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } //Sleep(0); } } void frame_buf_window::close(void) { m_frame_buffer.clear(); if (m_window) { ReleaseDC(m_window, m_windowHDC); DestroyWindow(m_window); m_window = 0; m_windowHDC = 0; } } void frame_buf_window::create_window(const char* pTitle) { memset( &m_window_class, 0, sizeof( m_window_class ) ); m_window_class.style = CS_OWNDC | CS_VREDRAW | CS_HREDRAW; m_window_class.lpfnWndProc = static_window_proc; m_window_class.cbWndExtra = sizeof(DWORD); m_window_class.hCursor = LoadCursor(0, IDC_ARROW); m_window_class.lpszClassName = pTitle; m_window_class.hbrBackground = (HBRUSH)GetStockObject(NULL_BRUSH); //GetStockObject(NULL_BRUSH); RegisterClass(&m_window_class); RECT rect; rect.left = rect.top = 0; rect.right = m_width * m_scale_x; rect.bottom = m_height * m_scale_y; AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, 0); rect.right -= rect.left; rect.bottom -= rect.top; m_orig_x = (GetSystemMetrics(SM_CXSCREEN) - rect.right) / 2; m_orig_y = (GetSystemMetrics(SM_CYSCREEN) - rect.bottom) / 2; m_orig_width = rect.right; m_orig_height = rect.bottom; m_pCur_app = this; m_window = CreateWindowEx( 0, pTitle, pTitle, WS_OVERLAPPEDWINDOW, m_orig_x, m_orig_y, rect.right, rect.bottom, 0, 0, 0, 0); SetWindowLong(m_window, 0, reinterpret_cast(this)); m_pCur_app = NULL; ShowWindow(m_window, SW_NORMAL); } void frame_buf_window::create_bitmap(void) { memset( &m_bitmap_info, 0, sizeof( m_bitmap_info ) ); m_pBitmap_hdr = reinterpret_cast(&m_bitmap_info); m_pBitmap_hdr->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); m_pBitmap_hdr->bmiHeader.biWidth = m_width; m_pBitmap_hdr->bmiHeader.biHeight = -m_height; m_pBitmap_hdr->bmiHeader.biBitCount = 24; m_pBitmap_hdr->bmiHeader.biPlanes = 1; m_pBitmap_hdr->bmiHeader.biCompression = BI_RGB;//BI_BITFIELDS; // Only really needed for 32bpp BI_BITFIELDS reinterpret_cast(m_pBitmap_hdr->bmiColors)[0] = 0x00FF0000; reinterpret_cast(m_pBitmap_hdr->bmiColors)[1] = 0x0000FF00; reinterpret_cast(m_pBitmap_hdr->bmiColors)[2] = 0x000000FF; m_windowHDC = GetDC(m_window); m_frame_buffer.init( m_width, m_height ); m_frame_buffer.cls( 30, 30, 30 ); //m_frame_buffer.draw_text( 50, 200, 2, 255, 127, 128, "This is a test!" ); } LRESULT frame_buf_window::window_proc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (m_pWindow_proc) { LRESULT lres; bool status = m_pWindow_proc(lres, hWnd, message, wParam, lParam, m_pWindow_proc_data); if (status) return lres; } switch (message) { case WM_PAINT: { if (m_frame_buffer.is_valid()) { RECT window_size; GetClientRect(hWnd, &window_size); StretchDIBits(m_windowHDC, 0, 0, window_size.right, window_size.bottom, 0, 0, m_width, m_height, m_frame_buffer.get_ptr(), m_pBitmap_hdr, DIB_RGB_COLORS, SRCCOPY); ValidateRect(hWnd, NULL); } break; } case WM_KEYDOWN: { const int cEscCode = 27; if (cEscCode != (wParam & 0xFF)) break; } case WM_CLOSE: { close(); break; } } return DefWindowProc( hWnd, message, wParam, lParam ); } class CConCommandServerProtocolTypes { public: enum { cProtocolPort = 3779, cMaxClients = 4 }; struct PacketHeader_t { uint16 m_nID; uint16 m_nType; uint32 m_nTotalSize; }; struct SetCameraPosPacket_t : PacketHeader_t { Vector m_Pos; QAngle m_Angle; }; struct ScreenshotPacket_t : PacketHeader_t { uint m_nScreenshotID; char m_szFilename[256]; }; enum { cPacketHeaderID = 0x1234, cPacketHeaderSize = sizeof( PacketHeader_t ), }; enum PacketTypes_t { cPacketTypeMessage = 0, cPacketTypeCommand = 1, cPacketTypeScreenshotRequest = 2, cPacketTypeScreenshotReply = 3, cPacketTypeSetCameraPos = 4, cPacketTypeConVarDumpRequest = 5, cPacketTypeConVarDumpReply = 6, cPackerTypeTotalMessages }; }; struct DumpedConVar_t { CUtlString m_Name; CUtlString m_Value; bool operator< (const DumpedConVar_t & rhs ) const { return V_strcmp( m_Name.Get(), rhs.m_Name.Get() ) < 0; } }; typedef CUtlVector DumpedConVarVector_t; static void DumpConVars( DumpedConVarVector_t &conVars ) { ICvar::Iterator iter( g_pCVar ); for ( iter.SetFirst() ; iter.IsValid() ; iter.Next() ) { ConCommandBase *var = iter.Get(); ConVar *pConVar = dynamic_cast< ConVar* >( var ); if ( !pConVar ) continue; const char *pName = pConVar->GetName(); const char *pVal = pConVar->GetString(); if ( ( !pName ) || ( !pVal ) ) continue; int nIndex = conVars.AddToTail(); conVars[nIndex].m_Name.Set( pName ); conVars[nIndex].m_Value.Set( pVal ); } } class CConCommandConnection : public CConCommandServerProtocolTypes { public: CConCommandConnection() : m_pSocket( NULL ), m_bDeleteSocket( false ), m_nEndpointIndex( -1 ), m_bClientFlag( false ), m_bReceivedNewCameraPos( false ), m_nSendBufOfs( 0 ), m_bHasNewScreenshot( false ), m_nScreenshotID( 0 ) { memset( &m_NewCameraPos, 0, sizeof( m_NewCameraPos ) ); } ~CConCommandConnection() { Deinit(); } bool IsConnected() const { return m_nEndpointIndex >= 0; } bool Init( const char *pAddress ) { Deinit(); m_nEndpointIndex = 0; m_bClientFlag = true; m_RecvBuf.EnsureCapacity( 4096 ); m_SendBuf.EnsureCapacity( 4096 ); m_RecvBuf.SetCountNonDestructively( 0 ); m_SendBuf.SetCountNonDestructively( 0 ); m_nSendBufOfs = 0; m_pSocket = new CSocketConnection; m_bDeleteSocket = true; m_pSocket->Init( CT_CLIENT, SP_TCP ); SocketErrorCode_t err = m_pSocket->ConnectToServer( pAddress, cProtocolPort ); if ( err != SOCKET_SUCCESS ) { Warning( "CONCMDSRV: Failed connecting to con cmd server \"%s\"!\n", pAddress ); Deinit(); return false; } uint n = 0; while ( m_pSocket->GetEndpointSocketState( 0 ) == SSTATE_CONNECTION_IN_PROGRESS ) { bool bIsConnected = false; err = m_pSocket->PollClientConnectionState( &bIsConnected ); if ( err != SOCKET_SUCCESS ) { Warning( "CONCMDSRV: Failed connecting to con cmd server \"%s\"!\n", pAddress ); Deinit(); return false; } if ( bIsConnected ) break; ThreadSleep( 10 ); if ( ++n >= 500 ) { Warning( "CONCMDSRV: Failed connecting to con cmd server \"%s\"!\n", pAddress ); Deinit(); return false; } } if ( m_pSocket->GetEndpointSocketState( 0 ) != SSTATE_CONNECTED ) { Warning( "CONCMDSRV: Failed connecting to con cmd server \"%s\"!\n", pAddress ); Deinit(); return false; } int nFlag = 1; m_pSocket->SetSocketOpt( 0, IPPROTO_TCP, TCP_NODELAY, &nFlag, sizeof( int ) ); return TickConnection(); } bool Init( CSocketConnection *pSocket, int nEndpointIndex, bool bClientFlag ) { Deinit(); int nFlag = 1; pSocket->SetSocketOpt( nEndpointIndex, IPPROTO_TCP, TCP_NODELAY, &nFlag, sizeof( int ) ); m_pSocket = pSocket; m_bDeleteSocket = false; m_nEndpointIndex = nEndpointIndex; m_bClientFlag = bClientFlag; m_RecvBuf.EnsureCapacity( 4096 ); m_SendBuf.EnsureCapacity( 4096 ); m_RecvBuf.SetCountNonDestructively( 0 ); m_SendBuf.SetCountNonDestructively( 0 ); m_nSendBufOfs = 0; return TickConnection(); } void Deinit() { if ( m_nEndpointIndex < 0 ) return; ConMsg( "CONCMDSRV: Disconnecting endpoint %i\n", m_nEndpointIndex ); if ( m_pSocket ) { m_pSocket->ResetEndpoint( m_nEndpointIndex ); if ( m_bDeleteSocket ) { m_pSocket->Cleanup(); delete m_pSocket; } m_pSocket = NULL; } m_nEndpointIndex = -1; m_bDeleteSocket = false; m_bClientFlag = false; m_RecvBuf.SetCountNonDestructively( 0 ); m_SendBuf.SetCountNonDestructively( 0 ); m_nSendBufOfs = 0; m_bHasNewScreenshot = false; m_nScreenshotID = 0; m_screenshot.clear(); m_bReceivedNewCameraPos = false; memset( &m_NewCameraPos, 0, sizeof( m_NewCameraPos ) ); } bool SendData( const void * pPacket, uint nSize ) { if ( m_nEndpointIndex < 0 ) return false; m_SendBuf.AddMultipleToTail( nSize, static_cast< const uint8 * >( pPacket ) ); return TickConnectionSend(); } bool TickConnectionRecv( ) { for ( ; ; ) { uint8 buf[4096]; int nBytesRead = 0; SocketErrorCode_t nReadErr = m_pSocket->ReadFromEndpoint( m_nEndpointIndex, buf, sizeof( buf ), &nBytesRead ); if ( nReadErr == SOCKET_ERR_READ_OPERATION_WOULD_BLOCK ) break; else if ( nReadErr != SOCKET_SUCCESS ) { Warning( "CONCMDSRV: Failed receiving data from client %i\n", m_nEndpointIndex ); Deinit(); return false; } m_RecvBuf.AddMultipleToTail( nBytesRead, buf ); if ( m_RecvBuf.Count() >= cPacketHeaderSize ) { if ( !ProcessMessages() ) { Warning( "CONCMDSRV: Failed processing messages from client %i\n", m_nEndpointIndex ); Deinit(); return false; } } } return true; } bool TickConnectionSend( ) { while ( m_nSendBufOfs < m_SendBuf.Count() ) { int nBytesWritten = 0; SocketErrorCode_t nWriteErr = m_pSocket->WriteToEndpoint( m_nEndpointIndex, &m_SendBuf[ m_nSendBufOfs ], m_SendBuf.Count() - m_nSendBufOfs, &nBytesWritten ); if ( nWriteErr == SOCKET_ERR_WRITE_OPERATION_WOULD_BLOCK ) break; else if ( nWriteErr != SOCKET_SUCCESS ) { Warning( "CONCMDSRV: Failed sending data to client %i\n", m_nEndpointIndex ); Deinit(); return false; } m_nSendBufOfs += nBytesWritten; if ( m_nSendBufOfs == m_SendBuf.Count() ) { m_SendBuf.SetCountNonDestructively( 0 ); m_nSendBufOfs = 0; break; } } return true; } bool TickConnection() { if ( !IsConnected() ) return false; if ( !TickConnectionSend() ) return false; if ( !TickConnectionRecv() ) return false; if ( m_bReceivedNewCameraPos ) { m_bReceivedNewCameraPos = false; char buf[256]; //V_snprintf( buf, sizeof( buf ), "setpos_exact %f %f %f;setang_exact %f %f %f\n", m_NewCameraPos.m_Pos.x, m_NewCameraPos.m_Pos.y, m_NewCameraPos.m_Pos.z, m_NewCameraPos.m_Angle.x, m_NewCameraPos.m_Angle.y, m_NewCameraPos.m_Angle.z ); V_snprintf( buf, sizeof( buf ), "setpos_exact 0x%X 0x%X 0x%X;setang_exact 0x%X 0x%X 0x%X\n", *(DWORD*)&m_NewCameraPos.m_Pos.x, *(DWORD*)&m_NewCameraPos.m_Pos.y, *(DWORD*)&m_NewCameraPos.m_Pos.z, *(DWORD*)&m_NewCameraPos.m_Angle.x, *(DWORD*)&m_NewCameraPos.m_Angle.y, *(DWORD*)&m_NewCameraPos.m_Angle.z ); Cbuf_AddText( buf ); Cbuf_Execute(); } return true; } bool HasNewScreenshotFlag() const { return m_bHasNewScreenshot; } void ClearNewScreenshotFlag() { m_bHasNewScreenshot = false; } uint GetScreenshotID() const { return m_nScreenshotID; } simple_bitmap &GetScreenshot() { return m_screenshot; } bool HasNewConVarDumpFlag() const { return m_bHasNewConVarDump; } void ClearHasNewConVarDumpFlag() { m_bHasNewConVarDump = false; } DumpedConVarVector_t &GetConVarDumpVector() { return m_DumpedConVars; } const DumpedConVarVector_t &GetConVarDumpVector() const { return m_DumpedConVars; } private: CSocketConnection *m_pSocket; bool m_bDeleteSocket; int m_nEndpointIndex; bool m_bClientFlag; CUtlVector m_RecvBuf; CUtlVector m_SendBuf; int m_nSendBufOfs; bool m_bReceivedNewCameraPos; SetCameraPosPacket_t m_NewCameraPos; simple_bitmap m_screenshot; bool m_bHasNewScreenshot; uint m_nScreenshotID; bool m_bHasNewConVarDump; DumpedConVarVector_t m_DumpedConVars; bool ProcessMessages() { while ( m_RecvBuf.Count() >= cPacketHeaderSize ) { const int nCurRecvBufSize = m_RecvBuf.Count(); const PacketHeader_t header( *reinterpret_cast( &m_RecvBuf[0] ) ); if ( header.m_nID != cPacketHeaderID ) return false; if ( header.m_nTotalSize < cPacketHeaderSize ) return false; if ( header.m_nType >= cPackerTypeTotalMessages ) return false; if ( (uint32)m_RecvBuf.Count() < header.m_nTotalSize ) break; const uint nNumMessageBytes = header.m_nTotalSize - cPacketHeaderSize; const uint8 *pMsg = nNumMessageBytes ? &m_RecvBuf[cPacketHeaderSize] : NULL; switch ( header.m_nType ) { case cPacketTypeMessage: { if ( nNumMessageBytes ) { ConMsg( "CONCMDSRV: Message from client %i: ", m_nEndpointIndex ); CUtlVectorFixedGrowable buf; buf.SetCount( nNumMessageBytes + 1 ); memcpy( &buf[0], pMsg, nNumMessageBytes ); buf[nNumMessageBytes] = '\0'; ConMsg( "%s\n", &buf[0] ); } break; } case cPacketTypeScreenshotRequest: { const ScreenshotPacket_t &requestPacket = *reinterpret_cast( &m_RecvBuf[0] ); if ( requestPacket.m_nTotalSize != sizeof( ScreenshotPacket_t ) ) { Warning( "CONCMDSRV: Invalid screenshot request packet!\n" ); return false; } if ( !videomode ) break; uint nWidth = videomode->GetModeWidth(); uint nHeight = videomode->GetModeHeight(); if ( !nWidth || !nHeight ) break; static void *s_pBuf; if ( !s_pBuf ) { s_pBuf = malloc( nWidth * nHeight * 3 ); } videomode->ReadScreenPixels( 0, 0, nWidth, nHeight, s_pBuf, IMAGE_FORMAT_RGB888 ); size_t nPNGSize = 0; void *pPNGData = tdefl_write_image_to_png_file_in_memory( s_pBuf, nWidth, nHeight, 3, &nPNGSize, true ); if ( pPNGData ) { ScreenshotPacket_t replyPacket; replyPacket.m_nID = cPacketHeaderID; replyPacket.m_nTotalSize = sizeof( replyPacket ) + nPNGSize; replyPacket.m_nType = cPacketTypeScreenshotReply; V_strcpy( replyPacket.m_szFilename, requestPacket.m_szFilename ); replyPacket.m_nScreenshotID = requestPacket.m_nScreenshotID; if ( !SendData( &replyPacket, sizeof( replyPacket ) ) ) { free( pPNGData ); return false; } if ( !SendData( pPNGData, nPNGSize ) ) { free( pPNGData ); return false; } free( pPNGData ); } break; } case cPacketTypeScreenshotReply: { const ScreenshotPacket_t &replyPacket = *reinterpret_cast( &m_RecvBuf[0] ); if ( replyPacket.m_nTotalSize <= sizeof( ScreenshotPacket_t ) ) { Warning( "CONCMDSRV: Invalid screenshot reply packet!\n" ); return false; } const void *pPNGData = &m_RecvBuf[sizeof(ScreenshotPacket_t)]; uint nPNGDataSize = header.m_nTotalSize - sizeof(ScreenshotPacket_t); if ( !replyPacket.m_nScreenshotID ) { char szFilename[512]; V_snprintf( szFilename, sizeof( szFilename ), "%s_%u.png", replyPacket.m_szFilename, m_nEndpointIndex ); FileHandle_t fh = g_pFullFileSystem->Open( szFilename, "wb" ); if ( fh ) { g_pFullFileSystem->Write( pPNGData, nPNGDataSize, fh ); g_pFullFileSystem->Close(fh); } } else { int nWidth = 0, nHeight = 0, nComp = 3; unsigned char *pImageData = stbi_load_from_memory( reinterpret_cast< stbi_uc const * >( pPNGData ), nPNGDataSize, &nWidth, &nHeight, &nComp, 3); if ( !pImageData ) { Warning( "CONCMDSRV: Failed unpacking PNG screenshot!\n" ); return false; } m_bHasNewScreenshot = true; m_nScreenshotID = replyPacket.m_nScreenshotID; if ( ( (int)m_screenshot.width() != nWidth ) || ( (int)m_screenshot.height() != nHeight ) ) { m_screenshot.init( nWidth, nHeight ); } memcpy( m_screenshot.get_ptr(), pImageData, nWidth * nHeight * 3 ); stbi_image_free( pImageData ); } break; } case cPacketTypeCommand: { if ( nNumMessageBytes ) { //ConMsg( "CONCMDSRV: Console command from client %i: ", m_nEndpointIndex ); CUtlVectorFixedGrowable buf; buf.SetCount( nNumMessageBytes + 1 ); memcpy( &buf[0], pMsg, nNumMessageBytes ); buf[nNumMessageBytes] = '\0'; //ConMsg( "\"%s\"\n", &buf[0] ); Cbuf_AddText( &buf[0] ); Cbuf_AddText( "\n" ); Cbuf_Execute(); } break; } case cPacketTypeSetCameraPos: { const SetCameraPosPacket_t &setCameraPosPacket = reinterpret_cast( m_RecvBuf[0] ); if ( setCameraPosPacket.m_nTotalSize != sizeof( SetCameraPosPacket_t ) ) { Warning( "CONCMDSRV: Invalid set camera pos packet!\n" ); return false; } m_bReceivedNewCameraPos = true; m_NewCameraPos = setCameraPosPacket; break; } case cPacketTypeConVarDumpRequest: { CUtlVector conVarData; DumpedConVarVector_t conVars; DumpConVars( conVars ); for ( int i = 0; i < conVars.Count(); i++ ) { const char *pName = conVars[i].m_Name.Get(); const char *pVal = conVars[i].m_Value.Get(); if ( ( !pName ) || ( !pVal ) ) continue; conVarData.AddMultipleToTail( V_strlen( pName ) + 1, reinterpret_cast( pName ) ); conVarData.AddMultipleToTail( V_strlen( pVal ) + 1, reinterpret_cast( pVal ) ); } PacketHeader_t replyPacket; replyPacket.m_nID = cPacketHeaderID; replyPacket.m_nTotalSize = sizeof( replyPacket ) + conVarData.Count(); replyPacket.m_nType = cPacketTypeConVarDumpReply; if ( !SendData( &replyPacket, sizeof( replyPacket ) ) ) return false; if ( conVarData.Count() ) { if ( !SendData( &conVarData[0], conVarData.Count() ) ) return false; } break; } case cPacketTypeConVarDumpReply: { m_bHasNewConVarDump = true; m_DumpedConVars.SetCountNonDestructively( 0 ); m_DumpedConVars.EnsureCapacity( 4096 ); const char *pCur = reinterpret_cast( pMsg ); const char *pEnd = reinterpret_cast( pMsg ) + nNumMessageBytes; while ( pCur < pEnd ) { const uint nBytesLeft = pEnd - pCur; uint nNameLen = V_strlen( pCur ); if ( ( nNameLen + 1 ) >= nBytesLeft ) { Warning( "CONCMDSRV: Failed parsing convar dump reply!\n" ); Assert( 0 ); return false; } uint nValLen = V_strlen( pCur + nNameLen + 1 ); uint nTotalLenInBytes = nNameLen + 1 + nValLen + 1; if ( nTotalLenInBytes > nBytesLeft ) { Warning( "CONCMDSRV: Failed parsing convar dump reply!\n" ); Assert( 0 ); return false; } int nIndex = m_DumpedConVars.AddToTail(); m_DumpedConVars[nIndex].m_Name.Set( pCur ); m_DumpedConVars[nIndex].m_Value.Set( pCur + nNameLen + 1 ); pCur += nTotalLenInBytes; } ConMsg( "Received convar dump reply from endpoint %i, %u total convars\n", m_nEndpointIndex, m_DumpedConVars.Count() ); break; } default: { return false; } } // In case the connection gets lost during a send to reply if ( ( m_nEndpointIndex < 0 ) || ( nCurRecvBufSize != m_RecvBuf.Count() ) ) return false; int nNumBytesRemaining = m_RecvBuf.Count() - header.m_nTotalSize; Assert( nNumBytesRemaining >= 0 ); if ( nNumBytesRemaining >= 0 ) { memmove( &m_RecvBuf[0], &m_RecvBuf[0] + header.m_nTotalSize, nNumBytesRemaining ); m_RecvBuf.SetCountNonDestructively( nNumBytesRemaining ); } } return true; } }; ConVar ccs_broadcast_camera( "ccs_broadcast_camera", "0", FCVAR_CHEAT ); ConVar ccs_camera_sync( "ccs_camera_sync", "0", FCVAR_CHEAT ); ConVar ccs_remote_screenshots( "ccs_remote_screenshots", "0", FCVAR_CHEAT ); ConVar ccs_remote_screenshots_delta( "ccs_remote_screenshots_delta", "1", FCVAR_CHEAT ); class CConCommandServer : public CConCommandServerProtocolTypes { static bool UserWindowProc( LRESULT& hres, HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, void *pData ) { CConCommandServer *pServer = static_cast< CConCommandServer * >( pData ); switch ( message ) { case WM_KEYDOWN: { uint cKey = ( wParam & 0xFF ); if ( ( cKey >= '0' ) && ( cKey <= '9' ) ) { pServer->m_nViewEndpointIndex = cKey - '0'; } } } return false; } public: CConCommandServer() : m_bInitialized( false ), m_pFrameBufWindow( NULL ), m_nViewEndpointIndex( 0 ) { } ~CConCommandServer() { Deinit(); } bool Init() { Deinit(); m_Socket.Init( CT_SERVER, SP_TCP ); if ( m_Socket.Listen( cProtocolPort, cMaxClients ) != SOCKET_SUCCESS ) { Warning( "CONCMDSRV: CConCommandServer:Init: Failed to create listen socket!\n" ); return false; } ConMsg( "CONCMDSVR: Listening for connections\n" ); uint nWidth = 1280, nHeight = 1024; if ( videomode ) { nWidth = videomode->GetModeWidth(); nHeight = videomode->GetModeHeight(); } m_pFrameBufWindow = new frame_buf_window( "CCS", nWidth, nHeight ); m_pFrameBufWindow->set_window_proc_callback( UserWindowProc, this ); m_bInitialized = true; return true; } void Deinit() { if ( !m_bInitialized ) return; delete m_pFrameBufWindow; m_pFrameBufWindow = NULL; for ( int i = 0; i < cMaxClients; i++ ) { m_Clients[i].Deinit(); } m_Socket.Cleanup(); m_nViewEndpointIndex = 0; ConMsg( "CONCMDSVR: Deinitialized\n" ); m_bInitialized = false; } bool IsInitialized() { return m_bInitialized; } void TickFrame( float flTime ) { if ( !m_bInitialized ) return; AcceptNewConnections(); if ( ccs_broadcast_camera.GetBool() ) { Vector vecOrigin = MainViewOrigin(); QAngle angles( 0, 0, 0 ); IClientEntity *localPlayer = entitylist ? entitylist->GetClientEntity( cl.m_nPlayerSlot + 1 ) : NULL; if ( localPlayer ) { vecOrigin = localPlayer->GetAbsOrigin(); angles = localPlayer->GetAbsAngles(); } SetCameraPosPacket_t setCameraPacket; setCameraPacket.m_nID = cPacketHeaderID; setCameraPacket.m_nType = cPacketTypeSetCameraPos; setCameraPacket.m_nTotalSize = sizeof( setCameraPacket ); setCameraPacket.m_Pos = vecOrigin; setCameraPacket.m_Angle = angles; SendDataToAllClients( &setCameraPacket, sizeof( setCameraPacket ) ); if ( ccs_camera_sync.GetBool() ) { ccs_camera_sync.SetValue( false ); char buf[256]; V_snprintf( buf, sizeof( buf ), "setpos_exact 0x%X 0x%X 0x%X;setang_exact 0x%X 0x%X 0x%X\n", *(DWORD*)&setCameraPacket.m_Pos.x, *(DWORD*)&setCameraPacket.m_Pos.y, *(DWORD*)&setCameraPacket.m_Pos.z, *(DWORD*)&setCameraPacket.m_Angle.x, *(DWORD*)&setCameraPacket.m_Angle.y, *(DWORD*)&setCameraPacket.m_Angle.z ); Cbuf_AddText( buf ); Cbuf_Execute(); } } if ( ccs_remote_screenshots.GetBool() ) { static double flTotalTime; flTotalTime += flTime; if ( flTotalTime > .5f ) { flTotalTime = 0.0f; ScreenshotPacket_t screenshotPacket; screenshotPacket.m_nID = cPacketHeaderID; screenshotPacket.m_nType = cPacketTypeScreenshotRequest; screenshotPacket.m_nTotalSize = sizeof( screenshotPacket ); screenshotPacket.m_szFilename[0] = '\0'; screenshotPacket.m_nScreenshotID = 1; SendDataToAllClients( &screenshotPacket, sizeof( screenshotPacket ) ); } } bool bHasUpdatedScreenshot = false; simple_bgr_bitmap &frameBuf = m_pFrameBufWindow->frameBuffer(); for ( int i = 0; i < cMaxClients; i++ ) { CConCommandConnection &client = m_Clients[i]; if ( !client.IsConnected() ) continue; client.TickConnection(); if ( client.HasNewConVarDumpFlag() ) { client.ClearHasNewConVarDumpFlag(); diffClientsDumpedConVars( i, client.GetConVarDumpVector() ); } if ( m_nViewEndpointIndex == i ) { if ( client.HasNewScreenshotFlag() ) { client.ClearNewScreenshotFlag(); simple_bitmap &clientScreenshot = client.GetScreenshot(); if ( ccs_remote_screenshots_delta.GetBool() ) { if ( videomode && !bHasUpdatedScreenshot ) { bHasUpdatedScreenshot = true; uint nScreenWidth = videomode->GetModeWidth(); uint nScreenHeight = videomode->GetModeHeight(); if ( ( m_screenshot.width() != nScreenWidth ) || ( m_screenshot.height() != nScreenHeight ) ) { m_screenshot.init( nScreenWidth, nScreenHeight ); } videomode->ReadScreenPixels( 0, 0, nScreenWidth, nScreenHeight, m_screenshot.get_ptr(), IMAGE_FORMAT_RGB888 ); } uint mx = MIN( clientScreenshot.width(), m_screenshot.width() ); mx = MIN( mx, frameBuf.width() ); uint my = MIN( clientScreenshot.height(), m_screenshot.height() ); my = MIN( my, frameBuf.height() ); for ( uint y = 0; y < my; ++y ) { const uint8 *pSrc1 = clientScreenshot.get_scanline( y ); const uint8 *pSrc2 = m_screenshot.get_scanline( y ); uint8 *pDst = frameBuf.get_scanline( y ); for ( uint x = 0; x < mx; ++x ) { int r = 2 * ( pSrc1[0] - pSrc2[0] ) + 128; int g = 2 * ( pSrc1[1] - pSrc2[1] ) + 128; int b = 2 * ( pSrc1[2] - pSrc2[2] ) + 128; if ( ( r | g | b ) & 0xFFFFFF00 ) { if ( r & 0xFFFFFF00 ) { r = (~( r >> 31 )) & 0xFF; } if ( g & 0xFFFFFF00 ) { g = (~( g >> 31 )) & 0xFF; } if ( b & 0xFFFFFF00 ) { b = (~( b >> 31 )) & 0xFF; } } pDst[0] = (uint8)b; pDst[1] = (uint8)g; pDst[2] = (uint8)r; pSrc1 += 3; pSrc2 += 3; pDst += 3; } } } else { frameBuf.blit( 0, 0, (simple_bgr_bitmap &)clientScreenshot, true ); } } } } if ( m_pFrameBufWindow ) { m_pFrameBufWindow->update(); } } void SendMessageToClients( const char * pCmd ) { if ( !m_bInitialized ) return; int n = V_strlen( pCmd ); char buf[4096]; if ( ( n >= 2 ) && ( pCmd[0] == '\"' ) && ( pCmd[ n - 1 ] == '\"' ) ) { memcpy( buf, pCmd + 1, n - 2 ); buf[n - 2] = '\0'; pCmd = buf; } SendStringToClients( pCmd, cPacketTypeMessage ); } void SendConCommandToClients( const char * pCmd, bool bExecLocally = false ) { int n = V_strlen( pCmd ); char buf[4096]; if ( ( n >= 2 ) && ( pCmd[0] == '\"' ) && ( pCmd[ n - 1 ] == '\"' ) ) { memcpy( buf, pCmd + 1, n - 2 ); buf[n - 2] = '\0'; pCmd = buf; } if ( m_bInitialized ) { SendStringToClients( pCmd, cPacketTypeCommand ); } if ( bExecLocally ) { Cbuf_AddText( pCmd ); Cbuf_AddText( "\n" ); Cbuf_Execute(); } } void SendCameraPosAndAngleToClients( Vector &vecOrigin, QAngle &angles ) { if ( !m_bInitialized ) return; SetCameraPosPacket_t packet; packet.m_nID = cPacketHeaderID; packet.m_nType = cPacketTypeSetCameraPos; packet.m_nTotalSize = sizeof( packet ); packet.m_Pos = vecOrigin; packet.m_Angle = angles; SendDataToAllClients( &packet, sizeof( packet ) ); } void SendScreenshotRequestToClients( const char *pFilename ) { if ( !m_bInitialized ) return; ScreenshotPacket_t packet; packet.m_nID = cPacketHeaderID; packet.m_nType = cPacketTypeScreenshotRequest; packet.m_nTotalSize = sizeof( packet ); V_strncpy( packet.m_szFilename, pFilename, sizeof( packet.m_szFilename ) ); packet.m_nScreenshotID = 0; SendDataToAllClients( &packet, sizeof( packet ) ); } void DiffConVarsOfAllClients() { if ( !m_bInitialized ) return; PacketHeader_t packet; packet.m_nID = cPacketHeaderID; packet.m_nTotalSize = sizeof( PacketHeader_t ); packet.m_nType = cPacketTypeConVarDumpRequest; SendDataToAllClients( &packet, sizeof( packet ) ); } private: bool m_bInitialized; CSocketConnection m_Socket; CConCommandConnection m_Clients[cMaxClients]; frame_buf_window *m_pFrameBufWindow; simple_bitmap m_screenshot; int m_nViewEndpointIndex; void AcceptNewConnections() { for ( ; ; ) { int nNewEndpointIndex = -1; SocketErrorCode_t err = m_Socket.TryAcceptIncomingConnection( &nNewEndpointIndex ); if ( ( err != SOCKET_SUCCESS ) || ( nNewEndpointIndex < 0 ) ) break; int i; for ( i = 0; i < cMaxClients; i++ ) { if ( !m_Clients[i].IsConnected() ) break; } if ( i == cMaxClients ) { m_Socket.ResetEndpoint( nNewEndpointIndex ); Warning( "CONCMDSRV: Too many active connections!\n" ); break; } if ( !m_Clients[i].Init( &m_Socket, nNewEndpointIndex, false ) ) { Warning( "CONCMDSRV: Failed accepting connection from endpoint %i\n", nNewEndpointIndex ); } else { ConMsg( "CONCMDSRV: Accepting connection at endpoint %i\n", nNewEndpointIndex ); } } } bool SendDataToAllClients( const void *p, uint nSize ) { if ( !nSize ) return true; bool bSuccess = true; for ( uint i = 0; i < cMaxClients; i++ ) { if ( !m_Clients[i].IsConnected() ) continue; if ( !m_Clients[i].SendData( p, nSize ) ) { bSuccess = false; } } return bSuccess; } void SendStringToClients( const char *pCmd, PacketTypes_t nType ) { uint8 buf[8192]; uint nStrLen = strlen( pCmd ); const uint nMaxStrLen = sizeof( buf ) - cPacketHeaderSize; nStrLen = MIN( nStrLen, nMaxStrLen ); PacketHeader_t &hdr = reinterpret_cast(buf[0]); hdr.m_nID = cPacketHeaderID; hdr.m_nType = nType; hdr.m_nTotalSize = cPacketHeaderSize + nStrLen; memcpy( buf + cPacketHeaderSize, pCmd, nStrLen ); SendDataToAllClients( buf, hdr.m_nTotalSize ); } static int FindConVar( const DumpedConVarVector_t &sortedConVars, const char *pName ) { int l = 0, h = sortedConVars.Count() - 1; while ( l <= h ) { int m = ( l + h ) >> 1; int d = V_strcmp( sortedConVars[m].m_Name.Get(), pName ); if ( !d ) return m; else if ( d > 0 ) h = m - 1; else l = m + 1; } return -1; } static void DiffConVars( DumpedConVarVector_t &serverConVars, DumpedConVarVector_t &clientConVars ) { if ( serverConVars.Count() ) { std::sort( &serverConVars.Head(), &serverConVars.Tail() + 1 ); } if ( clientConVars.Count() ) { std::sort( &clientConVars.Head(), &clientConVars.Tail() + 1 ); } CUtlVector clientConVarFoundFlags; clientConVarFoundFlags.SetCount( clientConVars.Count() ); memset( &clientConVarFoundFlags.Head(), 0, sizeof( bool ) * clientConVars.Count() ); uint nTotalNotFound = 0; uint nTotalMatches = 0; uint nTotalMismatches = 0; for ( int i = 0; i < serverConVars.Count(); i++ ) { const DumpedConVar_t &serverConVar = serverConVars[i]; const char *pServerConVarName = serverConVar.m_Name.Get(); const char *pServerConVarValue = serverConVar.m_Value.Get(); Assert( FindConVar( serverConVars, pServerConVarName ) != -1 ); int nClientConVarIndex = FindConVar( clientConVars, pServerConVarName ); if ( nClientConVarIndex < 0 ) { Warning( "%s: Can't find server convar on client, value: \"%s\"\n", pServerConVarName, pServerConVarValue ); nTotalNotFound++; } else { clientConVarFoundFlags[nClientConVarIndex] = true; if ( V_strcmp( pServerConVarValue, clientConVars[nClientConVarIndex].m_Value.Get() ) == 0 ) { nTotalMatches++; } else { nTotalMismatches++; Warning( "%s: Convar diff: server=\"%s\", client=\"%s\"\n", pServerConVarName, pServerConVarValue, clientConVars[nClientConVarIndex].m_Value ); } } } uint nTotalUnmatched = 0; for ( int i = 0; i < clientConVars.Count(); ++i ) { if ( !clientConVarFoundFlags[i] ) { nTotalUnmatched++; Warning( "%s: Client convar does not exist on server, value: \"%s\"\n", clientConVars[i].m_Name.Get(), clientConVars[i].m_Value.Get() ); } } ConMsg( "--- Summary:\n"); ConMsg( "Total server convars: %u\n", serverConVars.Count() ); ConMsg( "Total client convars: %u\n", clientConVars.Count() ); ConMsg( "Total server convars not found on client: %u\n", nTotalNotFound ); ConMsg( "Total server convars that match clients: %u\n", nTotalMatches ); ConMsg( "Total server convars that mismatch clients: %u\n", nTotalMismatches ); ConMsg( "Total client convars that don't exist on server: %u\n", nTotalUnmatched ); } void diffClientsDumpedConVars( int nClientIndex, DumpedConVarVector_t &clientConVars ) { DumpedConVarVector_t serverConVars; DumpConVars( serverConVars ); ConMsg( "Convar diff of client %i:\n", nClientIndex ); DiffConVars( serverConVars, clientConVars ); } }; CConCommandServer g_ConCommandServer; CON_COMMAND_F( ccs_start, "Start the con command server.", FCVAR_CHEAT ) { if ( !g_ConCommandServer.IsInitialized() ) { g_ConCommandServer.Init(); } else { Warning( "CONCMDSVR: Already initialized\n" ); } } CON_COMMAND_F( ccs_stop, "Stop the con command server.", FCVAR_CHEAT ) { if ( g_ConCommandServer.IsInitialized() ) { g_ConCommandServer.Deinit(); } else { Warning( "CONCMDSVR: Not initialized\n" ); } } CON_COMMAND_F( ccs_msg, "Send a message to any connected con command server clients.", FCVAR_CHEAT ) { if ( g_ConCommandServer.IsInitialized() ) { if ( args.ArgC() < 2 ) { Warning( "Usage: ccs_msg \"message\"\n" ); return; } g_ConCommandServer.SendMessageToClients( args.ArgS() ); } else { Warning( "CONCMDSVR: Not initialized\n" ); } } CON_COMMAND_F( ccs_cmd, "Send a console message to any connected con command server clients.", FCVAR_CHEAT ) { if ( g_ConCommandServer.IsInitialized() ) { if ( args.ArgC() < 2 ) { ConMsg( "Usage: ccs_cmd \"command\"\n" ); return; } g_ConCommandServer.SendConCommandToClients( args.ArgS() ); } else { Warning( "CONCMDSVR: Not initialized\n" ); } } CON_COMMAND_F( ccs_cmd_local, "Send a console message to any connected con command server clients, and also issue the command locally.", FCVAR_CHEAT ) { if ( g_ConCommandServer.IsInitialized() ) { if ( args.ArgC() < 2 ) { ConMsg( "Usage: ccs_cmd_local \"command\"\n" ); return; } g_ConCommandServer.SendConCommandToClients( args.ArgS(), true ); } else { Warning( "CONCMDSVR: Not initialized\n" ); } } CON_COMMAND_F( ccs_screenshot, "Request screenshots from connected clients", FCVAR_CHEAT ) { if ( g_ConCommandServer.IsInitialized() ) { const char *pBaseFileName = "ccs_screenshot"; if ( args.ArgC() >= 2 ) { pBaseFileName = args.Arg( 1 ); } g_ConCommandServer.SendScreenshotRequestToClients( pBaseFileName ); } else { Warning( "CONCMDSVR: Not initialized\n" ); } } CON_COMMAND_F( ccs_diff_convars, "Diffs server's convars vs. all connected clients", FCVAR_CHEAT ) { if ( g_ConCommandServer.IsInitialized() ) { g_ConCommandServer.DiffConVarsOfAllClients(); } else { Warning( "CONCMDSVR: Not initialized\n" ); } } CON_COMMAND_F( ccs_dump_convars, "Dump all convars to console", FCVAR_CHEAT ) { DumpedConVarVector_t conVars; DumpConVars( conVars ); for ( int i = 0; i < conVars.Count(); i++ ) { ConMsg( "%s %s\n", conVars[i].m_Name.Get(), conVars[i].m_Value.Get() ); } ConMsg( "Dumped %i convars\n", conVars.Count() ); } CConCommandConnection g_ConCommandConnection; CON_COMMAND_F( ccs_connect, "Connect to a con cmd server.", FCVAR_CHEAT ) { if ( args.ArgC() < 2 ) { ConMsg( "Usage: ccs_connect \"address\"\n" ); return; } if ( g_ConCommandConnection.IsConnected() ) { g_ConCommandConnection.Deinit(); Warning( "CONCMDSRV: Disconnected\n" ); } if ( g_ConCommandConnection.Init( args.ArgS() ) ) { ConMsg( "CONCMDSRV: Connected\n" ); } else { Warning( "CONCMDSRV: Failed to connect!\n" ); } } CON_COMMAND_F( ccs_disconnect, "Disconnect from a con cmd server.", FCVAR_CHEAT ) { if ( !g_ConCommandConnection.IsConnected() ) { Warning( "CONCMDSRV: Not connected!\n" ); } else { g_ConCommandConnection.Deinit(); Warning( "CONCMDSRV: Disconnected\n" ); } } #endif // CON_COMMAND_SERVER_SUPPORT void CCS_Init() { } void CCS_Shutdown() { #ifdef CON_COMMAND_SERVER_SUPPORT g_ConCommandConnection.Deinit(); g_ConCommandServer.Deinit(); #endif } void CCS_Tick( float flTime ) { (void)flTime; #ifdef CON_COMMAND_SERVER_SUPPORT if ( g_ConCommandConnection.IsConnected() ) { g_ConCommandConnection.TickConnection(); } if ( g_ConCommandServer.IsInitialized() ) { g_ConCommandServer.TickFrame( flTime ); } #endif }