//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ //===========================================================================// #include "cbase.h" #include "weapon_ifmsteadycam.h" #include "in_buttons.h" #include "usercmd.h" #include "dt_shared.h" #ifdef CLIENT_DLL #include "vgui_controls/Controls.h" #include "vgui/ISurface.h" #include "vgui/IScheme.h" #include "vgui/ILocalize.h" #include "vgui/VGUI.h" #include "tier1/KeyValues.h" #include "toolframework/itoolframework.h" #endif //----------------------------------------------------------------------------- // CWeaponIFMSteadyCam tables. //----------------------------------------------------------------------------- IMPLEMENT_NETWORKCLASS_ALIASED( WeaponIFMSteadyCam, DT_WeaponIFMSteadyCam ) LINK_ENTITY_TO_CLASS( weapon_ifm_steadycam, CWeaponIFMSteadyCam ); #if !( defined( TF_CLIENT_DLL ) || defined( TF_DLL ) ) PRECACHE_WEAPON_REGISTER( weapon_ifm_steadycam ); #endif BEGIN_NETWORK_TABLE( CWeaponIFMSteadyCam, DT_WeaponIFMSteadyCam ) END_NETWORK_TABLE() #ifdef CLIENT_DLL BEGIN_PREDICTION_DATA( CWeaponIFMSteadyCam ) DEFINE_PRED_FIELD( m_bIsLocked, FIELD_BOOLEAN, 0 ), DEFINE_PRED_FIELD( m_bInSpringMode, FIELD_BOOLEAN, 0 ), DEFINE_PRED_FIELD( m_bInDirectMode, FIELD_BOOLEAN, 0 ), DEFINE_PRED_FIELD( m_vecOffset, FIELD_VECTOR, 0 ), DEFINE_PRED_FIELD( m_hLockTarget, FIELD_EHANDLE, 0 ), DEFINE_PRED_FIELD( m_vec2DVelocity, FIELD_VECTOR, 0 ), DEFINE_PRED_FIELD( m_vecActualViewOffset, FIELD_VECTOR, 0 ), DEFINE_PRED_FIELD( m_vecViewOffset, FIELD_VECTOR, 0 ), DEFINE_PRED_FIELD( m_flFOVOffsetY, FIELD_FLOAT, 0 ), END_PREDICTION_DATA() #endif #ifdef GAME_DLL BEGIN_DATADESC( CWeaponIFMSteadyCam ) DEFINE_FIELD( m_hLockTarget, FIELD_EHANDLE ), END_DATADESC() #endif //----------------------------------------------------------------------------- // CWeaponIFMSteadyCam implementation. //----------------------------------------------------------------------------- CWeaponIFMSteadyCam::CWeaponIFMSteadyCam() { #ifdef CLIENT_DLL m_bIsLocked = false; m_bInDirectMode = false; m_bInSpringMode = true; m_vec2DVelocity.Init(); m_vecActualViewOffset.Init(); m_vecViewOffset.Init(); m_flFOVOffsetY = 0.0f; m_vecOffset.Init(); m_hFont = vgui::INVALID_FONT; m_nTextureId = -1; #endif } CWeaponIFMSteadyCam::~CWeaponIFMSteadyCam() { #ifdef CLIENT_DLL if ( vgui::surface() && m_nTextureId != -1 ) { vgui::surface()->DestroyTextureID( m_nTextureId ); m_nTextureId = -1; } #endif } //----------------------------------------------------------------------------- // // Specific methods on the client // //----------------------------------------------------------------------------- #ifdef CLIENT_DLL //----------------------------------------------------------------------------- // Computes a matrix given a forward direction //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::MatrixFromForwardDirection( const Vector &vecForward, matrix3x4_t &mat ) { // Convert desired to quaternion Vector vecLeft( -vecForward.y, vecForward.x, 0.0f ); if ( VectorNormalize( vecLeft ) < 1e-3 ) { vecLeft.Init( 1.0f, 0.0f, 0.0f ); } Vector vecUp; CrossProduct( vecForward, vecLeft, vecUp ); MatrixInitialize( mat, m_vecRelativePosition, vecForward, vecLeft, vecUp ); } //----------------------------------------------------------------------------- // Updates the relative orientation of the camera, spring mode //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::ComputeMouseRay( const VMatrix &steadyCamToPlayer, Vector &vecForward ) { // Create a ray in steadycam space float flMaxD = 1.0f / tan( M_PI * m_flFOV / 360.0f ); // Remap offsets into normalized space int w, h; GetViewportSize( w, h ); float flViewX = ( w != 0 ) ? m_vecViewOffset.x / ( w / 2 ) : 0.0f; float flViewY = ( h != 0 ) ? m_vecViewOffset.y / ( h / 2 ) : 0.0f; flViewX *= flMaxD; flViewY *= flMaxD; Vector vecSelectionDir( 1.0f, -flViewX, -flViewY ); VectorNormalize( vecSelectionDir ); // Rotate the ray into player coordinates Vector3DMultiply( steadyCamToPlayer, vecSelectionDir, vecForward ); } //----------------------------------------------------------------------------- // Updates the relative orientation of the camera, spring mode //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::UpdateDirectRelativeOrientation() { // Compute a player to steadycam matrix VMatrix steadyCamToPlayer; MatrixFromAngles( m_angRelativeAngles, steadyCamToPlayer ); MatrixSetColumn( steadyCamToPlayer, 3, m_vecRelativePosition ); // Compute a forward direction Vector vecCurrentForward; MatrixGetColumn( steadyCamToPlayer, 0, &vecCurrentForward ); // Before any updating occurs, sample the current // world-space direction of the mouse Vector vecDesiredDirection; ComputeMouseRay( steadyCamToPlayer, vecDesiredDirection ); // rebuild a roll-less orientation based on that direction vector matrix3x4_t mat; MatrixFromForwardDirection( vecDesiredDirection, mat ); MatrixAngles( mat, m_angRelativeAngles ); Assert( m_angRelativeAngles.IsValid() ); m_vecActualViewOffset -= m_vecViewOffset; m_vecViewOffset.Init(); } //----------------------------------------------------------------------------- // Updates the relative orientation of the camera when locked //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::UpdateLockedRelativeOrientation() { CBasePlayer *pPlayer = GetPlayerOwner(); if ( !pPlayer ) return; Vector vecDesiredDirection = m_vecOffset; CBaseEntity *pLock = m_hLockTarget.Get(); if ( pLock ) { vecDesiredDirection += pLock->GetAbsOrigin(); } Vector vecAbsOrigin; QAngle angAbsRotation; ComputeAbsCameraTransform( vecAbsOrigin, angAbsRotation ); vecDesiredDirection -= vecAbsOrigin; VectorNormalize( vecDesiredDirection ); matrix3x4_t mat; MatrixFromForwardDirection( vecDesiredDirection, mat ); MatrixAngles( mat, m_angRelativeAngles ); } //----------------------------------------------------------------------------- // Updates the relative orientation of the camera //----------------------------------------------------------------------------- static ConVar ifm_steadycam_rotaterate( "ifm_steadycam_rotaterate", "60", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_zoomspeed( "ifm_steadycam_zoomspeed", "1.0", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_zoomdamp( "ifm_steadycam_zoomdamp", "0.95", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_armspeed( "ifm_steadycam_armspeed", "0.5", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_rotatedamp( "ifm_steadycam_rotatedamp", "0.95", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_mousefactor( "ifm_steadycam_mousefactor", "1.0", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_mousepower( "ifm_steadycam_mousepower", "1.0", FCVAR_ARCHIVE ); void CWeaponIFMSteadyCam::UpdateRelativeOrientation() { if ( m_bIsLocked ) return; if ( m_bInDirectMode ) { UpdateDirectRelativeOrientation(); return; } if ( ( m_vecViewOffset.x == 0.0f ) && ( m_vecViewOffset.y == 0.0f ) ) return; // Compute a player to steadycam matrix VMatrix steadyCamToPlayer; MatrixFromAngles( m_angRelativeAngles, steadyCamToPlayer ); MatrixSetColumn( steadyCamToPlayer, 3, m_vecRelativePosition ); Vector vecCurrentForward; MatrixGetColumn( steadyCamToPlayer, 0, &vecCurrentForward ); // Create a ray in steadycam space float flMaxD = 1.0f / tan( M_PI * m_flFOV / 360.0f ); // Remap offsets into normalized space float flViewX = m_vecViewOffset.x / ( 384 / 2 ); float flViewY = m_vecViewOffset.y / ( 288 / 2 ); flViewX *= flMaxD * ifm_steadycam_mousefactor.GetFloat(); flViewY *= flMaxD * ifm_steadycam_mousefactor.GetFloat(); Vector vecSelectionDir( 1.0f, -flViewX, -flViewY ); VectorNormalize( vecSelectionDir ); // Rotate the ray into player coordinates Vector vecDesiredDirection; Vector3DMultiply( steadyCamToPlayer, vecSelectionDir, vecDesiredDirection ); float flDot = DotProduct( vecDesiredDirection, vecCurrentForward ); flDot = clamp( flDot, -1.0f, 1.0f ); float flAngle = 180.0f * acos( flDot ) / M_PI; if ( flAngle < 1e-3 ) { matrix3x4_t mat; MatrixFromForwardDirection( vecDesiredDirection, mat ); MatrixAngles( mat, m_angRelativeAngles ); return; } Vector vecAxis; CrossProduct( vecCurrentForward, vecDesiredDirection, vecAxis ); VectorNormalize( vecAxis ); float flRotateRate = ifm_steadycam_rotaterate.GetFloat(); if ( flRotateRate < 1.0f ) { flRotateRate = 1.0f; } float flRateFactor = flAngle / flRotateRate; flRateFactor *= flRateFactor * flRateFactor; float flRate = flRateFactor * 30.0f; float flMaxAngle = gpGlobals->frametime * flRate; flAngle = clamp( flAngle, 0.0f, flMaxAngle ); Vector vecNewForard; VMatrix rotation; MatrixBuildRotationAboutAxis( rotation, vecAxis, flAngle ); Vector3DMultiply( rotation, vecCurrentForward, vecNewForard ); matrix3x4_t mat; MatrixFromForwardDirection( vecNewForard, mat ); MatrixAngles( mat, m_angRelativeAngles ); Assert( m_angRelativeAngles.IsValid() ); } //----------------------------------------------------------------------------- // Toggles to springy camera //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::ToggleDirectMode() { m_vecViewOffset.Init(); m_vecActualViewOffset.Init(); m_vec2DVelocity.Init(); m_bInDirectMode = !m_bInDirectMode; } //----------------------------------------------------------------------------- // Targets the camera to always look at a point //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::LockCamera() { m_vecViewOffset.Init(); m_vecActualViewOffset.Init(); m_vec2DVelocity.Init(); m_bIsLocked = !m_bIsLocked; if ( !m_bIsLocked ) { UpdateLockedRelativeOrientation(); return; } CBasePlayer *pPlayer = GetPlayerOwner(); if ( !pPlayer ) return; Vector vTraceStart, vTraceEnd, vTraceDir; QAngle angles; BaseClass::ComputeAbsCameraTransform( vTraceStart, angles ); AngleVectors( angles, &vTraceDir ); VectorMA( vTraceStart, 10000.0f, vTraceDir, vTraceEnd); trace_t tr; UTIL_TraceLine( vTraceStart, vTraceEnd, MASK_ALL, GetPlayerOwner(), COLLISION_GROUP_NONE, &tr ); if ( tr.fraction == 1.0f ) { m_bIsLocked = false; UpdateLockedRelativeOrientation(); return; } m_hLockTarget = tr.m_pEnt; m_vecOffset = tr.endpos; if ( tr.m_pEnt ) { m_vecOffset -= tr.m_pEnt->GetAbsOrigin(); } } //----------------------------------------------------------------------------- // Gets the abs orientation of the camera //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::ComputeAbsCameraTransform( Vector &vecAbsOrigin, QAngle &angAbsRotation ) { CBaseEntity *pLock = m_bIsLocked ? m_hLockTarget.Get() : NULL; CBasePlayer *pPlayer = GetPlayerOwner(); if ( !pLock || !pPlayer ) { BaseClass::ComputeAbsCameraTransform( vecAbsOrigin, angAbsRotation ); return; } Vector vecDesiredDirection = m_vecOffset; if ( pLock ) { vecDesiredDirection += pLock->GetAbsOrigin(); } BaseClass::ComputeAbsCameraTransform( vecAbsOrigin, angAbsRotation ); vecDesiredDirection -= vecAbsOrigin; VectorNormalize( vecDesiredDirection ); matrix3x4_t mat; MatrixFromForwardDirection( vecDesiredDirection, mat ); MatrixAngles( mat, angAbsRotation ); } //----------------------------------------------------------------------------- // Computes the view offset from the actual view offset //----------------------------------------------------------------------------- static ConVar ifm_steadycam_2dspringconstant( "ifm_steadycam_2dspringconstant", "33.0", FCVAR_ARCHIVE ); static ConVar ifm_steadycam_2ddragconstant( "ifm_steadycam_2ddragconstant", "11.0", FCVAR_ARCHIVE ); void CWeaponIFMSteadyCam::ComputeViewOffset() { // Update 2D spring if ( !m_bInSpringMode ) { m_vecViewOffset = m_vecActualViewOffset; return; } Vector2D dir; Vector2DSubtract( m_vecViewOffset.AsVector2D(), m_vecActualViewOffset.AsVector2D(), dir ); float flDist = Vector2DNormalize( dir ); Vector2D vecForce; Vector2DMultiply( dir, -flDist * ifm_steadycam_2dspringconstant.GetFloat(), vecForce ); Vector2DMA( vecForce, -ifm_steadycam_2ddragconstant.GetFloat(), m_vec2DVelocity.AsVector2D(), vecForce ); Vector2DMA( m_vecViewOffset.AsVector2D(), gpGlobals->frametime, m_vec2DVelocity.AsVector2D(), m_vecViewOffset.AsVector2D() ); Vector2DMA( m_vec2DVelocity.AsVector2D(), gpGlobals->frametime, vecForce, m_vec2DVelocity.AsVector2D() ); } //----------------------------------------------------------------------------- // Camera control //----------------------------------------------------------------------------- static ConVar ifm_steadycam_noise( "ifm_steadycam_noise", "0.0", FCVAR_ARCHIVE | FCVAR_REPLICATED ); static ConVar ifm_steadycam_sensitivity( "ifm_steadycam_sensitivity", "1.0", FCVAR_ARCHIVE | FCVAR_REPLICATED ); void CWeaponIFMSteadyCam::ItemPostFrame() { CBasePlayer *pPlayer = GetPlayerOwner(); if ( !pPlayer ) return; float flSensitivity = ifm_steadycam_sensitivity.GetFloat(); Vector2D vecOldActualViewOffset = m_vecActualViewOffset.AsVector2D(); if ( pPlayer->m_nButtons & IN_ATTACK ) { const CUserCmd *pUserCmd = pPlayer->GetCurrentUserCommand(); m_vecActualViewOffset.x += pUserCmd->mousedx * flSensitivity; m_vecActualViewOffset.y += pUserCmd->mousedy * flSensitivity; } else { if ( !m_bIsLocked && !m_bInDirectMode ) { float flDamp = ifm_steadycam_rotatedamp.GetFloat(); m_vecActualViewOffset.x *= flDamp; m_vecActualViewOffset.y *= flDamp; } } // Add noise if ( !m_bIsLocked ) { float flNoise = ifm_steadycam_noise.GetFloat(); if ( flNoise > 0.0f ) { CUniformRandomStream stream; stream.SetSeed( (int)(gpGlobals->curtime * 100) ); CGaussianRandomStream gauss( &stream ); float dx = gauss.RandomFloat( 0.0f, flNoise ); float dy = gauss.RandomFloat( 0.0f, flNoise ); m_vecActualViewOffset.x += dx; m_vecActualViewOffset.y += dy; } } ComputeViewOffset(); if ( pPlayer->m_nButtons & IN_ZOOM ) { const CUserCmd *pUserCmd = pPlayer->GetCurrentUserCommand(); m_flFOVOffsetY += pUserCmd->mousedy * flSensitivity; } else { float flDamp = ifm_steadycam_zoomdamp.GetFloat(); m_flFOVOffsetY *= flDamp; } m_flFOV += m_flFOVOffsetY * ifm_steadycam_zoomspeed.GetFloat() / 1000.0f; m_flFOV = clamp( m_flFOV, 0.5f, 160.0f ); if ( pPlayer->m_nButtons & IN_WALK ) { const CUserCmd *pUserCmd = pPlayer->GetCurrentUserCommand(); m_flArmLength -= ifm_steadycam_armspeed.GetFloat() * pUserCmd->mousedy; } if ( pPlayer->GetImpulse() == 87 ) { ToggleDirectMode(); } if ( pPlayer->GetImpulse() == 89 ) { m_bInSpringMode = !m_bInSpringMode; } if ( pPlayer->m_afButtonPressed & IN_USE ) { LockCamera(); } if ( pPlayer->m_afButtonPressed & IN_ATTACK2 ) { m_bFullScreen = !m_bFullScreen; } if ( pPlayer->GetImpulse() == 88 ) { // Make the view angles exactly match the player m_vecViewOffset.Init(); m_vecActualViewOffset.Init(); m_vecOffset.Init(); m_vec2DVelocity.Init(); m_hLockTarget.Set( NULL ); m_flArmLength = 0.0f; if ( m_bIsLocked ) { LockCamera(); } m_angRelativeAngles = pPlayer->EyeAngles(); m_flFOV = pPlayer->GetFOV(); } UpdateRelativeOrientation(); TransmitRenderInfo(); } //----------------------------------------------------------------------------- // Records the state for the IFM //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::GetToolRecordingState( KeyValues *msg ) { BaseClass::GetToolRecordingState( msg ); static CameraRecordingState_t state; state.m_flFOV = m_flFOV; ComputeAbsCameraTransform( state.m_vecEyePosition, state.m_vecEyeAngles ); msg->SetPtr( "camera", &state ); } //----------------------------------------------------------------------------- // Slams view angles if the mouse is down //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::CreateMove( float flInputSampleTime, CUserCmd *pCmd, const QAngle &vecOldViewAngles ) { BaseClass::CreateMove( flInputSampleTime, pCmd, vecOldViewAngles ); // Block angular movement when IN_ATTACK is pressed if ( pCmd->buttons & (IN_ATTACK | IN_WALK | IN_ZOOM) ) { VectorCopy( vecOldViewAngles, pCmd->viewangles ); } } //----------------------------------------------------------------------------- // Purpose: Draw the weapon's crosshair //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::DrawArmLength( int x, int y, int w, int h, Color clr ) { // Draw a readout for the arm length if ( m_hFont == vgui::INVALID_FONT ) { vgui::HScheme hScheme = vgui::scheme()->GetScheme( "ClientScheme" ); vgui::IScheme *pScheme = vgui::scheme()->GetIScheme( hScheme ); m_hFont = pScheme->GetFont("DefaultVerySmall", false ); Assert( m_hFont != vgui::INVALID_FONT ); } // Create our string char szString[256]; Q_snprintf( szString, sizeof(szString), "Arm Length: %.2f\n", m_flArmLength ); // Convert it to localize friendly unicode wchar_t wcString[256]; g_pVGuiLocalize->ConvertANSIToUnicode( szString, wcString, sizeof(wcString) ); int tw, th; vgui::surface()->GetTextSize( m_hFont, wcString, tw, th ); vgui::surface()->DrawSetTextFont( m_hFont ); // set the font vgui::surface()->DrawSetTextColor( clr ); // white vgui::surface()->DrawSetTextPos( x + w - tw - 10, y + 10 ); // x,y position vgui::surface()->DrawPrintText( wcString, wcslen(wcString) ); // print text } //----------------------------------------------------------------------------- // Purpose: Draw the FOV //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::DrawFOV( int x, int y, int w, int h, Color clrEdges, Color clrTriangle ) { if ( m_nTextureId == -1 ) { m_nTextureId = vgui::surface()->CreateNewTextureID(); vgui::surface()->DrawSetTextureFile( m_nTextureId, "vgui/white", true, false ); } // This is the fov int nSize = 30; int fx = x + w - 10 - nSize; int fy = y + h - 10; int fh = nSize * cos( M_PI * m_flFOV / 360.0f ); int fw = nSize * sin( M_PI * m_flFOV / 360.0f ); vgui::Vertex_t v[3]; v[0].m_Position.Init( fx, fy ); v[0].m_TexCoord.Init( 0.0f, 0.0f ); v[1].m_Position.Init( fx-fw, fy-fh ); v[1].m_TexCoord.Init( 0.0f, 0.0f ); v[2].m_Position.Init( fx+fw, fy-fh ); v[2].m_TexCoord.Init( 0.0f, 0.0f ); vgui::surface()->DrawSetTexture( m_nTextureId ); vgui::surface()->DrawSetColor( clrTriangle ); vgui::surface()->DrawTexturedPolygon( 3, v ); vgui::surface()->DrawSetColor( clrEdges ); vgui::surface()->DrawLine( fx, fy, fx - fw, fy - fh ); vgui::surface()->DrawLine( fx, fy, fx + fw, fy - fh ); } //----------------------------------------------------------------------------- // Purpose: Draw the weapon's crosshair //----------------------------------------------------------------------------- void CWeaponIFMSteadyCam::DrawCrosshair( void ) { BaseClass::DrawCrosshair(); int x, y, w, h; GetOverlayBounds( x, y, w, h ); // Draw the targeting zone around the crosshair int r, g, b, a; gHUD.m_clrYellowish.GetColor( r, g, b, a ); Color gray( 255, 255, 255, 192 ); Color light( r, g, b, 255 ); Color dark( r, g, b, 128 ); Color red( 255, 0, 0, 128 ); DrawArmLength( x, y, w, h, light ); DrawFOV( x, y, w, h, light, dark ); int cx, cy; cx = x + ( w / 2 ); cy = y + ( h / 2 ); // This is the crosshair vgui::surface()->DrawSetColor( gray ); vgui::surface()->DrawFilledRect( cx-10, cy-1, cx-3, cy+1 ); vgui::surface()->DrawFilledRect( cx+3, cy-1, cx+10, cy+1 ); vgui::surface()->DrawFilledRect( cx-1, cy-10, cx+1, cy-3 ); vgui::surface()->DrawFilledRect( cx-1, cy+3, cx+1, cy+10 ); // This is the yellow aiming dot if ( ( m_vecViewOffset.x != 0.0f ) || ( m_vecViewOffset.y != 0.0f ) ) { int ax, ay; ax = cx + m_vecViewOffset.x; ay = cy + m_vecViewOffset.y; vgui::surface()->DrawSetColor( light ); vgui::surface()->DrawFilledRect( ax-2, ay-2, ax+2, ay+2 ); } // This is the red actual dot if ( ( m_vecActualViewOffset.x != 0.0f ) || ( m_vecActualViewOffset.y != 0.0f ) ) { int ax, ay; ax = cx + m_vecActualViewOffset.x; ay = cy + m_vecActualViewOffset.y; vgui::surface()->DrawSetColor( red ); vgui::surface()->DrawFilledRect( ax-2, ay-2, ax+2, ay+2 ); } // This is the purple fov dot if ( m_flFOVOffsetY != 0.0f ) { Color purple( 255, 0, 255, 255 ); int vy = cy + m_flFOVOffsetY; vgui::surface()->DrawSetColor( purple ); vgui::surface()->DrawFilledRect( cx-2, vy-2, cx+2, vy+2 ); } } #endif // CLIENT_DLL //----------------------------------------------------------------------------- // // Specific methods on the server // //----------------------------------------------------------------------------- #ifdef GAME_DLL void CWeaponIFMSteadyCam::ItemPostFrame() { } #endif // GAME_DLL