HearthstoneLogWatcher::HearthstoneLogWatcher( QObject *parent, const QString& path ) : QObject( parent ), mPath( path ), mLastSeekPos( 0 ) { // We used QFileSystemWatcher before but it fails on windows // Windows File Notification seems to be very tricky with files // which are not explicitly flushed (which is the case for the Hearthstone Log) // QFileSystemWatcher fails, manual implemention with FindFirstChangeNotification // fails. So instead of putting too much work into a file-system depending solution // just use a low-overhead polling strategy // // Start/stop timer when hearthstone starts/stops // Otherwise we produce a plethora of idle wake ups connect( &mTimer, &QTimer::timeout, this, &HearthstoneLogWatcher::CheckForLogChanges ); connect( Hearthstone::Instance(), &Hearthstone::GameStarted, this, &HearthstoneLogWatcher::HandleGameStart ); connect( Hearthstone::Instance(), &Hearthstone::GameStopped, this, &HearthstoneLogWatcher::HandleGameStop ); DBG( "Watch log %s", qt2cstr( mPath ) ); QFile file( mPath ); if( file.exists() ) { mLastSeekPos = file.size(); } }
xcb_window_t LinuxWindowCapture::FindWindow( const QString& instanceName, const QString& windowClass ) { xcb_window_t winID = XCB_WINDOW_NONE; QList< xcb_window_t > windows = listWindowsRecursive( QX11Info::appRootWindow() ); xcb_connection_t* xcbConn = QX11Info::connection(); foreach( const xcb_window_t& win, windows ) { xcb_icccm_get_wm_class_reply_t wmNameR; xcb_get_property_cookie_t wmClassC = xcb_icccm_get_wm_class( xcbConn, win ); if ( xcb_icccm_get_wm_class_reply( xcbConn, wmClassC, &wmNameR, nullptr ) ) { if( !qstricmp( wmNameR.class_name, qt2cstr( windowClass ) ) || !qstricmp( wmNameR.instance_name, qt2cstr ( instanceName ) ) ) { winID = win; break; } } }
void Hearthstone::EnableLogging() { QString path = LogConfigPath(); QFile file( path ); bool logModified = false; // Read file contents QString contents; if( file.exists() ) { file.open( QIODevice::ReadOnly | QIODevice::Text ); QTextStream in( &file ); contents = in.readAll(); file.close(); } // Check what modules we have to activate QStringList modulesToActivate; for( int i = 0; i < NUM_LOG_MODULES; i++ ) { const char *moduleName = LOG_MODULE_NAMES[ i ]; QString moduleLine = QString( "[%1]" ).arg( moduleName ) ; if( !contents.contains( moduleLine ) ) { contents += "\n"; contents += moduleLine + "\n"; contents += "LogLevel=1\n"; contents += "FilePrinting=true\n"; DBG( "Activate module %s", moduleName ); logModified = true; } } if( contents.contains( "ConsolePrinting=true" ) ) { contents.replace( "ConsolePrinting=true", "FilePrinting=true" ); DBG( "ConsolePrinting replaced by FilePrinting" ); logModified = true; } // Finally write updated log.config if( logModified ) { DBG( "Log modified. Write new version" ); if( !file.open( QIODevice::WriteOnly | QIODevice::Text ) ) { ERR( "Couldn't create file %s", qt2cstr( path ) ); } else { QTextStream out( &file ); out << contents; } } // Notify about restart if game is running SetRestartRequired( Running() && logModified ); }
void Overlay::UpdateHistoryFor( Player player, const ::CardHistoryList& list ) { QMap< QString, QVariantMap > entries; for( const CardHistoryItem& it : list ) { const QString& cardId = it.cardId; if( cardId.isEmpty() ) { continue; } if( !mCardDB.Contains( cardId ) ) { DBG( "Card %s not found", qt2cstr( cardId ) ); continue; } if( mCardDB.Type( cardId ) == "hero" ) { continue; } if( it.player != player ) { continue; } QVariantMap& entry = entries[ cardId ]; entry[ "count" ] = entry.value( "count", 0 ).toInt() + 1; entry[ "mana" ] = mCardDB.Mana( cardId ); entry[ "name" ] = mCardDB.Name( cardId ); } OverlayHistoryList* ref; if( player == PLAYER_SELF ) { ref = &mPlayerHistory; } else { ref = &mOpponentHistory; } *ref = entries.values(); qSort( ref->begin(), ref->end(), []( const QVariantMap& a, const QVariantMap& b ) { if( a["mana"].toInt() == b["mana"].toInt() ) { return a["name"].toString() < b["name"].toString(); } else { return a["mana"].toInt() < b["mana"].toInt(); } }); }
int Trackobot::Run() { if( IsAlreadyRunning() ) return 1; LOG( "Launch v%s on %s", VERSION, qt2cstr( QDate::currentDate().toString( Qt::ISODate ) ) ); SetupUpdater(); CreateUI(); Initialize(); SetupLogging(); int exitCode = mApp.exec(); // Tear down LOG( "Shutdown" ); return exitCode; }
QString Hearthstone::ReadAgentAttribute( const char *attributeName ) const { #ifdef Q_OS_MAC QString path = "/Users/Shared/Battle.net/Agent/agent.db"; #elif defined Q_OS_WIN wchar_t buffer[ MAX_PATH ]; SHGetSpecialFolderPathW( NULL, buffer, CSIDL_COMMON_APPDATA, FALSE ); QString programData = QString::fromWCharArray( buffer ); QString path = programData + "\\Battle.net\\Agent\\agent.db"; #endif QFile file( path ); if( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) { ERR( "Couldn't open %s (%d)", qt2cstr( path ), file.error() ); return ""; } QString contents = file.readAll(); QJsonDocument doc = QJsonDocument::fromJson( contents.toUtf8() ); QJsonObject root = doc.object(); QJsonObject hs = root["/game/hs_beta"].toObject()["resource"].toObject()["game"].toObject(); return hs[ QString( attributeName ) ].toString(); }
void HearthstoneLogTracker::HandleLogLine( const QString& line ) { if( line.trimmed().isEmpty() || line.startsWith( "(Filename:" ) ) { return; } // CardPlayed / CardReturned / PlayerDied static QRegExp regex( "ProcessChanges.*\\[.*id=(\\d+).*cardId=(\\w+|).*\\].*zone from (.*) ->\\s?(.*)" ); if( regex.indexIn(line) != -1 ) { QStringList captures = regex.capturedTexts(); int id = captures[1].toInt(); QString cardId = captures[2]; QString from = captures[3]; QString to = captures[4]; bool draw = from.contains( "DECK" ) && to.contains( "HAND" ); bool mulligan = from.contains( "HAND" ) && to.contains( "DECK" ); // Discarded cards by playing Soulfire, Doomguard etc. bool discard = from.contains( "HAND" ) && to.contains( "GRAVEYARD" ); if( !draw && !mulligan && !discard ) { if( from.contains( "FRIENDLY HAND" ) ) { CardPlayed( PLAYER_SELF, cardId.toStdString(), id ); } else if( from.contains( "OPPOSING HAND" ) ) { CardPlayed( PLAYER_OPPONENT, cardId.toStdString(), id ); } else if( from.contains( "OPPOSING SECRET" ) && to.contains( "OPPOSING GRAVEYARD" ) ) { SecretResolved( PLAYER_OPPONENT, cardId.toStdString(), id ); } } if( from.contains( "FRIENDLY PLAY" ) && to.contains( "FRIENDLY HAND" ) ) { CardReturned( PLAYER_SELF, cardId.toStdString() ); } DBG( "Card %s from %s -> %s. (draw: %d, mulligan %d, discard %d) [%d]", qt2cstr( cardId ), qt2cstr( from ), qt2cstr( to ), draw, mulligan, discard, id ); } // Outcome static QRegExp regexOutcome( "name=(victory|defeat)_screen_start" ); if( regexOutcome.indexIn(line) != -1 ) { QStringList captures = regexOutcome.capturedTexts(); QString outcome = captures[1]; if( outcome == "victory" ) { emit HandleOutcome( OUTCOME_VICTORY ); } else if( outcome == "defeat" ) { emit HandleOutcome( OUTCOME_DEFEAT ); } emit HandleMatchEnd( mCardHistoryList ); Reset(); } // Coin static QRegExp regexCoin( "ProcessChanges.*zonePos=5.*zone from -> (.*)" ); // unique because from is nothing -> " " if( regexCoin.indexIn(line) != -1 ) { QStringList captures = regexCoin.capturedTexts(); QString to = captures[1]; if( to.contains( "FRIENDLY HAND" ) ) { // I go second because I get the coin emit HandleOrder( ORDER_SECOND ); } else if( to.contains( "OPPOSING HAND" ) ) { // Opponent got coin, so I go first emit HandleOrder( ORDER_FIRST ); } } // Turn Info static QRegExp regexTurn( "change=powerTask.*tag=NEXT_STEP value=MAIN_ACTION" ); if( regexTurn.indexIn(line) != -1 ) { mTurnCounter++; emit HandleTurn( mTurnCounter ); // reset hero power usage on turn change mHeroPowerUsed = false; } // Hero Power static QRegExp regexHeroPowerEquip( "player=(\\d+).*-> FRIENDLY PLAY \\(Hero Power\\)" ); if( regexHeroPowerEquip.indexIn(line) != -1 ) { QStringList captures = regexHeroPowerEquip.capturedTexts(); QString playerId = captures[1]; mHeroPlayerId = playerId.toInt(); DBG( "Hero Power Equip -> My Player Id: %d", mHeroPlayerId ); } static QRegExp regexHeroPower( "PowerProcessor\\.DoTaskListForCard.*cardId=(\\w+).*player=(\\d+)" ); if( regexHeroPower.indexIn(line) != -1 ) { QStringList captures = regexHeroPower.capturedTexts(); QString cardId = captures[1]; int playerId = captures[2].toInt(); Player player = ( playerId == mHeroPlayerId ) ? PLAYER_SELF : PLAYER_OPPONENT; bool isHeroPower = false; for( int i = 0; i < NUM_HERO_POWER_CARDS; i++ ) { if( cardId == HERO_POWER_CARD_IDS[ i ] ) { isHeroPower = true; break; } } // Power log line is emitted multiple times // Make sure we only account for first occurrence // Plus line is emitted when match starts, so ignore turn 0 if( isHeroPower && !mHeroPowerUsed && CurrentTurn() > 0 ) { CardPlayed( player, cardId.toStdString() ); mHeroPowerUsed = true; } } // Hero Equip static QRegExp regexHeroEquip( "cardId=(\\w+).*-> (\\w+) PLAY \\(Hero\\)" ); if( regexHeroEquip.indexIn(line) != -1 ) { QStringList captures = regexHeroEquip.capturedTexts(); QString cardId = captures[1]; QString type = captures[2]; // This log line can be emitted when hero swaps (Lord Jaraxxus) // So make sure we only account for the "initial" playable heroes Class hero = CLASS_UNKNOWN; for( int i = 0; i < NUM_HEROES; i++ ) { // startsWith instead of exact match to support // the new reasonably priced hero skins // (e.g. HERO_01a instead of HERO_01) if( cardId.startsWith( HERO_IDS[ i ] ) ) { hero = ( Class )i; } } // Set solo mode when encountering naxxramas/blackrock mountain heroes if( hero == CLASS_UNKNOWN ) { if( cardId.startsWith("NAX") || cardId.startsWith("BRM") ) { HandleGameMode( MODE_SOLO_ADVENTURES ); } } if( hero != CLASS_UNKNOWN ) { if( type == "FRIENDLY" ) { emit HandleMatchStart(); emit HandleOwnClass( hero ); } else { emit HandleOpponentClass( hero ); } } } // Game Mode // Practice, Casual/Ranked, ScreenForge static QRegExp regexMode( "---(\\w+)---" ); if( regexMode.indexIn(line) != -1 ) { QStringList captures = regexMode.capturedTexts(); QString screen = captures[1]; if( screen == "RegisterScreenPractice" ) { HandleGameMode( MODE_SOLO_ADVENTURES ); } else if( screen == "RegisterScreenTourneys") { HandleGameMode( MODE_CASUAL ); // or ranked resp. } else if( screen == "RegisterScreenForge" ) { HandleGameMode( MODE_ARENA ); } else if( screen == "RegisterScreenFriendly" ) { HandleGameMode( MODE_FRIENDLY ); } } // Tavern Brawl static QRegExp regexTavernBrawl( "SAVE --> NetCacheTavernBrawlRecord" ); if( regexTavernBrawl.indexIn(line) != -1 ) { HandleGameMode( MODE_TAVERN_BRAWL ); } // Rank // Rank events via log are unreliable // Legend // Emitted at the end of the game twice, make sure we capture only the first time static QRegExp regexLegend( "legend rank (\\d+)" ); if( !mLegendTracked && regexLegend.indexIn(line) != -1 ) { QStringList captures = regexLegend.capturedTexts(); int legend = captures[1].toInt(); if( legend > 0 ) { mLegendTracked = true; HandleLegend( legend ); } } // Casual/Ranked distinction static QRegExp regexRanked( "name=rank_window" ); if( regexRanked.indexIn(line) != -1 ) { HandleGameMode( MODE_RANKED ); } // flag current GAME as spectated static QRegExp regexBeginSpectating( "Start Spectator Game" ); if( regexBeginSpectating.indexIn(line) != -1 ) { DBG( "Begin spectator game" ); emit HandleSpectating( true ); } // disable spectating flag if we leave the spectator MODE static QRegExp regexEndSpectating( "End Spectator Mode" ); if( regexEndSpectating.indexIn(line) != -1 ) { DBG( "End spectator mode" ); emit HandleSpectating( false ); } }