/*
Copyright 2016 Peter Repukat - FlatspotSoftware
Licensed under the Apache License , Version 2.0 ( the " License " ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an " AS IS " BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
*/
# include "SteamTargetRenderer.h"
std : : atomic < bool > SteamTargetRenderer : : overlayOpen = false ;
HHOOK SteamTargetRenderer : : hook = nullptr ;
std : : atomic < bool > SteamTargetRenderer : : bHookSteam = false ;
SteamTargetRenderer : : SteamTargetRenderer ( int & argc , char * * argv ) : QApplication ( argc , argv )
{
getSteamOverlay ( ) ;
loadLogo ( ) ;
SetConsoleCtrlHandler ( reinterpret_cast < PHANDLER_ROUTINE > ( ConsoleCtrlCallback ) , true ) ;
if ( this - > arguments ( ) . size ( ) = = 1 )
{
std : : cerr < < " Target configuration file must be specified! " < < std : : endl ;
MessageBoxW ( NULL , L " Target configuration file must be specified! " , L " GloSC-SteamTarget " , MB_OK ) ;
QTimer : : singleShot ( 0 , this , [ ] ( )
{
QApplication : : exit ( 1 ) ;
} ) ; //call after ctor
} else {
QSettings settings ( this - > arguments ( ) . at ( 1 ) , QSettings : : IniFormat ) ;
settings . beginGroup ( " BaseConf " ) ;
const QStringList childKeys = settings . childKeys ( ) ;
for ( auto & childkey : childKeys )
{
if ( childkey = = " bDrawDebugEdges " )
{
bDrawDebugEdges = settings . value ( childkey ) . toBool ( ) ;
}
else if ( childkey = = " bEnableOverlay " ) {
bDrawOverlay = settings . value ( childkey ) . toBool ( ) ;
}
else if ( childkey = = " bEnableControllers " ) {
bEnableControllers = settings . value ( childkey ) . toBool ( ) ;
}
else if ( childkey = = " bHookSteam " ) {
bHookSteam = settings . value ( childkey ) . toBool ( ) ;
}
else if ( childkey = = " bUseDesktopConfig " ) {
bUseDesktopConfig = settings . value ( childkey ) . toBool ( ) ;
}
}
settings . endGroup ( ) ;
# ifndef NDEBUG
bDrawDebugEdges = true ;
# endif // NDEBUG
sfCshape = sf : : CircleShape ( 100.f ) ;
sfCshape . setFillColor ( sf : : Color ( 128 , 128 , 128 , 128 ) ) ;
sfCshape . setOrigin ( sf : : Vector2f ( 100 , 100 ) ) ;
sf : : VideoMode mode = sf : : VideoMode : : getDesktopMode ( ) ;
sfWindow . create ( sf : : VideoMode ( mode . width - 16 , mode . height - 32 ) , " GloSC_OverlayWindow " ) ; //Window is too large ; always 16 and 32 pixels? - sf::Style::None breaks transparency!
sfWindow . setFramerateLimit ( iRefreshRate ) ;
sfWindow . setPosition ( sf : : Vector2i ( 0 , 0 ) ) ;
makeSfWindowTransparent ( sfWindow ) ;
sfWindow . setActive ( false ) ;
consoleHwnd = GetConsoleWindow ( ) ; //We need a console for a dirty hack to make sure we stay in game bindings
//QT Windows cause trouble with the overlay, so we cannot use them
# ifdef NDEBUG
ShowWindow ( consoleHwnd , SW_HIDE ) ;
# endif // NDEBUG
if ( bEnableControllers )
controllerThread . run ( ) ;
QTimer : : singleShot ( 2000 , this , & SteamTargetRenderer : : launchApp ) ; // lets steam do its thing
if ( hmodGameOverlayRenderer ! = nullptr )
{
//Hook MessageQueue to detect if overlay gets opened / closed
//Steam Posts a Message with 0x14FA / 0x14F7 when the overlay gets opened / closed
hook = SetWindowsHookEx ( WH_GETMESSAGE , HookCallback , nullptr , GetCurrentThreadId ( ) ) ;
}
if ( bUseDesktopConfig )
{
bHookSteam = false ;
QTimer : : singleShot ( 1000 , this , [ ] ( )
{
HWND taskbar = FindWindow ( L " Shell_TrayWnd " , nullptr ) ;
SetFocus ( taskbar ) ;
SetForegroundWindow ( taskbar ) ;
} ) ;
}
}
}
SteamTargetRenderer : : ~ SteamTargetRenderer ( )
{
if ( hmodGameOverlayRenderer ! = nullptr )
{
UnhookWindowsHookEx ( hook ) ;
}
renderThread . join ( ) ;
if ( controllerThread . isRunning ( ) )
controllerThread . stop ( ) ;
}
void SteamTargetRenderer : : run ( )
{
renderThread = std : : thread ( & SteamTargetRenderer : : RunSfWindowLoop , this ) ;
}
void SteamTargetRenderer : : stop ( )
{
bRunLoop = false ;
unhookBindings ( ) ;
QApplication : : exit ( 0 ) ;
}
void SteamTargetRenderer : : RunSfWindowLoop ( )
{
if ( ! bRunLoop )
return ;
sfWindow . setActive ( true ) ;
bool focusSwitchNeeded = true ;
if ( bDrawOverlay )
SetWindowPos ( sfWindow . getSystemHandle ( ) , HWND_TOPMOST , 0 , 0 , 0 , 0 , SWP_NOMOVE | SWP_NOSIZE | SWP_ASYNCWINDOWPOS ) ;
else
ShowWindow ( consoleHwnd , SW_SHOW ) ;
while ( sfWindow . isOpen ( ) & & bRunLoop )
{
sf : : Event event ;
while ( sfWindow . pollEvent ( event ) )
{
if ( event . type = = sf : : Event : : Closed )
sfWindow . close ( ) ;
}
sfWindow . clear ( sf : : Color : : Transparent ) ;
if ( bDrawDebugEdges )
drawDebugEdges ( ) ;
//This ensures that we stay in game binding, even if focused application changes! (Why does this work? Well, i dunno... ask Valve...)
//Only works with a console window
//Causes trouble as soon as there is more than the consoleWindow and the overlayWindow
//This is trying to avoid hooking Steam.exe
//----
//alternatively / additionaly, we can just hook steam and make our lives so much easier
//we inject and hook here to spare IPC and let the dll grab the steam appID of the launched process when the config switches (config switches w/ focus)
if ( focusSwitchNeeded )
{
if ( bHookSteam )
hookBindings ( ) ; //cleanup - unhooking / unloading of dll is managed by the GloSC gamelauncher rather than here
focusSwitchNeeded = false ;
if ( ! bUseDesktopConfig )
{
SetFocus ( consoleHwnd ) ;
sf : : Clock clock ;
while ( ! SetForegroundWindow ( consoleHwnd ) & & clock . getElapsedTime ( ) . asMilliseconds ( ) < 1000 ) //try to forcefully set foreground window
{
Sleep ( 1 ) ;
}
}
}
//Dirty hack to make the steamoverlay work properly and still keep Apps Controllerconfig when closing overlay.
//even if hooking steam, this ensures the overlay stays working
if ( hmodGameOverlayRenderer ! = NULL )
{
if ( overlayOpen )
{
if ( ! bNeedFocusSwitch )
{
bNeedFocusSwitch = true ;
hwForeGroundWindow = GetForegroundWindow ( ) ;
std : : cout < < " Saving current ForegorundWindow HWND: " < < hwForeGroundWindow < < std : : endl ;
std : : cout < < " Activating OverlayWindow " < < std : : endl ;
SetWindowLong ( sfWindow . getSystemHandle ( ) , GWL_EXSTYLE , WS_EX_LAYERED ) ; //make overlay window clickable
//Actually activate the overlaywindow
SetFocus ( sfWindow . getSystemHandle ( ) ) ;
if ( ! bUseDesktopConfig )
{
//by activating the consolewindow **and bringing it to the foreground** we can trick steam so the controller stays in game bindings
SetFocus ( consoleHwnd ) ;
sf : : Clock clock ;
while ( ! SetForegroundWindow ( consoleHwnd ) & & clock . getElapsedTime ( ) . asMilliseconds ( ) < 1000 ) //try to forcefully set foreground window
{
Sleep ( 1 ) ;
}
}
//Move the mouse cursor inside the overlaywindow
//this is neccessary because steam doesn't want to switch to big picture bindings if mouse isn't inside
SetCursorPos ( 16 , 16 ) ;
}
sfWindow . draw ( backgroundSprite ) ;
} else {
if ( bNeedFocusSwitch )
{
std : : cout < < " Deactivating OverlayWindow " < < std : : endl ;
//make overlaywindow clickthrough - WS_EX_TRANSPARENT - again
SetWindowLong ( sfWindow . getSystemHandle ( ) , GWL_EXSTYLE , WS_EX_LAYERED | WS_EX_TRANSPARENT ) ;
std : : cout < < " Switching to previously focused window " < < std : : endl ;
//switch back the the previosly focused window
SetFocus ( hwForeGroundWindow ) ;
sf : : Clock clock ;
while ( ! SetForegroundWindow ( hwForeGroundWindow ) & & clock . getElapsedTime ( ) . asMilliseconds ( ) < 1000 ) //try to forcefully set foreground window
{
Sleep ( 1 ) ;
}
bNeedFocusSwitch = false ;
}
}
}
sfWindow . display ( ) ;
}
stop ( ) ;
}
void SteamTargetRenderer : : getSteamOverlay ( )
{
hmodGameOverlayRenderer = GetModuleHandle ( overlayModuleName ) ;
if ( hmodGameOverlayRenderer ! = nullptr )
{
std : : cout < < overlayModuleName < < " found; Module at: 0x " < < hmodGameOverlayRenderer < < std : : endl ;
}
}
void SteamTargetRenderer : : makeSfWindowTransparent ( sf : : RenderWindow & window )
{
HWND hwnd = window . getSystemHandle ( ) ;
SetWindowLong ( hwnd , GWL_STYLE , WS_VISIBLE | WS_POPUP & ~ WS_CAPTION ) ;
SetWindowLong ( hwnd , GWL_EXSTYLE , WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST ) ;
MARGINS margins ;
margins . cxLeftWidth = - 1 ;
DwmExtendFrameIntoClientArea ( hwnd , & margins ) ;
SetWindowPos ( hwnd , HWND_TOPMOST , 0 , 0 , 0 , 0 , SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE ) ;
window . clear ( sf : : Color : : Transparent ) ;
window . display ( ) ;
}
void SteamTargetRenderer : : drawDebugEdges ( )
{
sfCshape . setPosition ( sf : : Vector2f ( - 25 , - 25 ) ) ;
sfWindow . draw ( sfCshape ) ;
sfCshape . setPosition ( sf : : Vector2f ( sfWindow . getSize ( ) . x + 25 , - 25 ) ) ;
sfWindow . draw ( sfCshape ) ;
sfCshape . setPosition ( sf : : Vector2f ( - 25 , sfWindow . getSize ( ) . y ) ) ;
sfWindow . draw ( sfCshape ) ;
sfCshape . setPosition ( sf : : Vector2f ( sfWindow . getSize ( ) . x , sfWindow . getSize ( ) . y ) ) ;
sfWindow . draw ( sfCshape ) ;
}
void SteamTargetRenderer : : hookBindings ( ) const
{
std : : cout < < " Hooking Steam... " < < std : : endl ;
QString dir = QDir : : toNativeSeparators ( QCoreApplication : : applicationDirPath ( ) ) ;
dir = dir . mid ( 0 , dir . lastIndexOf ( " \\ " ) ) ;
QProcess proc ;
proc . setNativeArguments ( " --inject " ) ;
proc . setWorkingDirectory ( dir ) ;
proc . start ( " ./Injector.exe " , QIODevice : : ReadOnly ) ;
proc . waitForStarted ( ) ;
proc . waitForFinished ( ) ;
if ( QString : : fromStdString ( proc . readAll ( ) . toStdString ( ) ) . contains ( " Inject success! " ) ) //if we have injected (and patched the function)
{
std : : cout < < " Successfully hooked Steam! " < < std : : endl ;
//tell the GloSC_GameLauncher that we have hooked steam
//it will deal with checking if the target is still alive and unload the dll / unhook then
// - ensures unloading / unhooking even if this process crashes or gets unexpectedly killed
QSharedMemory sharedMemInstance ( " GloSC_GameLauncher " ) ;
if ( ! sharedMemInstance . create ( 1024 ) & & sharedMemInstance . error ( ) = = QSharedMemory : : AlreadyExists )
{
QBuffer buffer ;
QDataStream dataStream ( & buffer ) ;
QStringList stringList ;
sharedMemInstance . attach ( ) ;
sharedMemInstance . lock ( ) ;
buffer . setData ( static_cast < const char * > ( sharedMemInstance . constData ( ) ) , sharedMemInstance . size ( ) ) ;
buffer . open ( QBuffer : : ReadOnly ) ;
dataStream > > stringList ;
buffer . close ( ) ;
int i = stringList . indexOf ( IsSteamHooked ) + 1 ;
stringList . replace ( i , " 1 " ) ;
buffer . open ( QBuffer : : ReadWrite ) ;
QDataStream out ( & buffer ) ;
out < < stringList ;
int size = buffer . size ( ) ;
char * to = static_cast < char * > ( sharedMemInstance . data ( ) ) ;
const char * from = buffer . data ( ) . data ( ) ;
memcpy ( to , from , qMin ( sharedMemInstance . size ( ) , size ) ) ;
buffer . close ( ) ;
sharedMemInstance . unlock ( ) ;
sharedMemInstance . detach ( ) ;
}
} else {
std : : cout < < " Hooking Steam failed! " < < std : : endl ;
MessageBoxW ( NULL , L " Hooking Steam failed! " , L " GloSC-SteamTarget " , MB_OK ) ;
}
}
void SteamTargetRenderer : : loadLogo ( )
{
HRSRC rsrcData = FindResource ( NULL , L " ICOPNG " , RT_RCDATA ) ;
DWORD rsrcDataSize = SizeofResource ( NULL , rsrcData ) ;
HGLOBAL grsrcData = LoadResource ( NULL , rsrcData ) ;
LPVOID firstByte = LockResource ( grsrcData ) ;
spriteTexture = std : : make_unique < sf : : Texture > ( ) ;
spriteTexture - > loadFromMemory ( firstByte , rsrcDataSize ) ;
backgroundSprite . setTexture ( * spriteTexture ) ;
backgroundSprite . setOrigin ( sf : : Vector2f ( spriteTexture - > getSize ( ) . x / 2.f , spriteTexture - > getSize ( ) . y / 2 ) ) ;
sf : : VideoMode winSize = sf : : VideoMode : : getDesktopMode ( ) ;
backgroundSprite . setPosition ( sf : : Vector2f ( winSize . width / 2.f , winSize . height / 2.f ) ) ;
}
LRESULT WINAPI SteamTargetRenderer : : HookCallback ( int nCode , WPARAM wParam , LPARAM lParam )
{
if ( nCode > = 0 )
{
PMSG msg = reinterpret_cast < PMSG > ( lParam ) ;
if ( msg - > message = = 0x14FA ) //Posted when the overlay gets opened
{
overlayOpen = true ;
std : : cout < < " Overlay Opened! \n " ;
}
else if ( msg - > message = = 0x14F7 | | msg - > message = = 512 )
{
overlayOpen = false ;
std : : cout < < " Overlay closed! \n " ;
}
}
return CallNextHookEx ( hook , nCode , wParam , lParam ) ;
}
void SteamTargetRenderer : : unhookBindings ( )
{
if ( bHookSteam )
{
QString dir = QDir : : toNativeSeparators ( QCoreApplication : : applicationDirPath ( ) ) ;
dir = dir . mid ( 0 , dir . lastIndexOf ( " \\ " ) ) ;
QProcess proc ;
proc . setNativeArguments ( " --eject " ) ;
proc . setWorkingDirectory ( dir ) ;
proc . start ( " ./Injector.exe " , QIODevice : : ReadOnly ) ;
proc . waitForStarted ( ) ;
proc . waitForFinished ( ) ;
}
}
BOOL SteamTargetRenderer : : ConsoleCtrlCallback ( DWORD dwCtrlType )
{
if ( dwCtrlType = = CTRL_CLOSE_EVENT | | dwCtrlType = = CTRL_BREAK_EVENT | | dwCtrlType = = CTRL_C_EVENT )
{
unhookBindings ( ) ;
return true ;
}
return false ;
}
void SteamTargetRenderer : : launchApp ( )
{
SetPriorityClass ( GetCurrentProcess ( ) , HIGH_PRIORITY_CLASS ) ;
bool launchGame = false ;
bool closeWhenDone = false ;
QString type = " Win32 " ;
QString path = " " ;
QString args ;
QSettings settings ( this - > arguments ( ) . at ( 1 ) , QSettings : : IniFormat ) ;
settings . beginGroup ( " LaunchGame " ) ;
const QStringList childKeys = settings . childKeys ( ) ;
for ( auto & childkey : childKeys )
{
if ( childkey = = " bLaunchGame " )
launchGame = settings . value ( childkey ) . toBool ( ) ;
else if ( childkey = = " Type " )
type = settings . value ( childkey ) . toString ( ) ;
else if ( childkey = = " Path " )
path = settings . value ( childkey ) . toString ( ) ;
else if ( childkey = = " Args " )
args = settings . value ( childkey ) . toString ( ) ;
else if ( childkey = = " bCloseWhenDone " )
closeWhenDone = settings . value ( " bCloseWhenDone " ) . toBool ( ) ;
}
settings . endGroup ( ) ;
if ( launchGame )
{
QSharedMemory sharedMemInstance ( " GloSC_GameLauncher " ) ;
if ( ! sharedMemInstance . create ( 1024 ) & & sharedMemInstance . error ( ) = = QSharedMemory : : AlreadyExists )
{
QBuffer buffer ;
QDataStream dataStream ( & buffer ) ;
QStringList stringList ;
sharedMemInstance . attach ( ) ;
sharedMemInstance . lock ( ) ;
buffer . setData ( static_cast < const char * > ( sharedMemInstance . constData ( ) ) , sharedMemInstance . size ( ) ) ;
buffer . open ( QBuffer : : ReadOnly ) ;
dataStream > > stringList ;
buffer . close ( ) ;
int lgt_index = stringList . indexOf ( LaunchGame ) ;
stringList . replace ( lgt_index + 1 , type ) ;
stringList . replace ( lgt_index + 2 , path ) ;
stringList . replace ( lgt_index + 3 , args ) ;
buffer . open ( QBuffer : : ReadWrite ) ;
QDataStream out ( & buffer ) ;
out < < stringList ;
int size = buffer . size ( ) ;
char * to = static_cast < char * > ( sharedMemInstance . data ( ) ) ;
const char * from = buffer . data ( ) . data ( ) ;
memcpy ( to , from , qMin ( sharedMemInstance . size ( ) , size ) ) ;
buffer . close ( ) ;
sharedMemInstance . unlock ( ) ;
sharedMemInstance . detach ( ) ;
if ( closeWhenDone )
{
updateTimer . setInterval ( 1111 ) ;
connect ( & updateTimer , SIGNAL ( timeout ( ) ) , this , SLOT ( checkSharedMem ( ) ) ) ;
updateTimer . start ( ) ;
}
}
}
}
void SteamTargetRenderer : : checkSharedMem ( )
{
QSharedMemory sharedMemInstance ( " GloSC_GameLauncher " ) ;
if ( ! sharedMemInstance . create ( 1024 ) & & sharedMemInstance . error ( ) = = QSharedMemory : : AlreadyExists )
{
QBuffer buffer ;
QDataStream in ( & buffer ) ;
QStringList stringList ;
sharedMemInstance . attach ( ) ;
sharedMemInstance . lock ( ) ;
buffer . setData ( static_cast < const char * > ( sharedMemInstance . constData ( ) ) , sharedMemInstance . size ( ) ) ;
buffer . open ( QBuffer : : ReadOnly ) ;
in > > stringList ;
buffer . close ( ) ;
sharedMemInstance . unlock ( ) ;
sharedMemInstance . detach ( ) ;
int close_index = stringList . indexOf ( LaunchedProcessFinished ) + 1 ;
if ( close_index > 0 & & stringList . at ( close_index ) . toInt ( ) = = 1 )
bRunLoop = false ;
}
}