import { EventEmitter , Injectable } from '@angular/core' ; import { Achievement } from '../../models/achievement' ; import { AchievementProgress , AchievementsProgress } from '../../models/achievement/achievement-progress' ; import { HsRawAchievement } from '../../models/achievement/hs-raw-achievement' ; import { CompletedAchievement } from '../../models/completed-achievement' ; import { GameEvent } from '../../models/game-event' ; import { MemoryUpdate } from '../../models/memory/memory-update' ; import { Events } from '../events.service' ; import { FeatureFlags } from '../feature-flags' ; import { GameEventsEmitterService } from '../game-events-emitter.service' ; import { AchievementCompletedEvent } from '../mainwindow/store/events/achievements/achievement-completed-event' ; import { AchievementsUpdatedEvent } from '../mainwindow/store/events/achievements/achievements-updated-event' ; import { MainWindowStoreEvent } from '../mainwindow/store/events/main-window-store-event' ; import { MainWindowStoreService } from '../mainwindow/store/main-window-store.service' ; import { OverwolfService } from '../overwolf.service' ; import { MemoryInspectionService } from '../plugins/memory-inspection.service' ; import { PreferencesService } from '../preferences.service' ; import { ProcessingQueue } from '../processing-queue.service' ; import { HsAchievementInfo , HsAchievementsInfo } from './achievements-info' ; import { AchievementsManager } from './achievements-manager.service' ; import { AchievementsStorageService } from './achievements-storage.service' ; import { Challenge } from './achievements/challenges/challenge' ; import { AchievementsLoaderService } from './data/achievements-loader.service' ; import { RemoteAchievementsService } from './remote-achievements.service' ; @ Injectable ( ) export class AchievementsMonitor { private processingQueue = new ProcessingQueue < InternalEvent > ( ( eventQueue ) => this . processQueue ( eventQueue ) , 1000 , 'achievement-monitor' ) ; private lastReceivedTimestamp ; private achievementQuotas : { [ achievementId ] : number ; } ; private previousAchievements : readonly HsAchievementInfo [ ] ; private spectating : boolean ; private achievementsProgressInterval ; private stateUpdater : EventEmitter < MainWindowStoreEvent > ; constructor ( private readonly gameEvents , private readonly achievementLoader , private readonly events , private readonly store , private readonly remoteAchievements , private readonly achievementsStorage , private readonly achievementsManager , private readonly memory , private readonly ow , private readonly prefs ) { this . lastReceivedTimestamp = Date . now ( ) ; this . gameEvents . allEvents . subscribe ( ( gameEvent ) => { this . handleEvent ( gameEvent ) ; if ( gameEvent . type === GameEvent . GAME_START ) { this . assignPreviousAchievements ( ) ; if ( FeatureFlags . SHOW_CONSTRUCTED_SECONDARY_WINDOW ) { this . startAchievementsProgressDetection ( ) ; } } else if ( gameEvent . type === GameEvent . GAME_END ) { this . stopAchievementsProgressDetection ( ) ; } else if ( gameEvent . type === GameEvent . SPECTATING ) { this . spectating = gameEvent . additionalData . spectating ; } } ) ; this . events . on ( Events . MEMORY_UPDATE ) . subscribe ( async ( event ) => { const prefs = await this . prefs . getPreferences ( ) ; const changes = event . data [ 0 ] ; if ( changes . DisplayingAchievementToast && prefs . achievementsDisplayNotifications2 ) { setTimeout ( ( ) => { this . detectNewAchievementFromMemory ( ) ; } , 500 ) ; } } ) ; this . events . on ( Events . STORE_READY ) . subscribe ( ( event ) => this . startGlobalAchievementsProgressDetection ( ) ) ; this . init ( ) ; } private async init ( ) { const rawAchievements = await this . remoteAchievements . loadHsRawAchievements ( ) ; this . achievementQuotas = { } ; for ( const ach of rawAchievements ) { this . achievementQuotas [ ach . id ] = ach . quota ; } if ( await this . ow . inGame ( ) ) { this . assignPreviousAchievements ( ) ; } this . ow . addGameInfoUpdatedListener ( async ( res ) => { if ( this . ow . exitGame ( res ) ) { this . previousAchievements = null ; } else if ( ( await this . ow . inGame ( ) ) && res . gameChanged ) { if ( ! this . previousAchievements ) { this . assignPreviousAchievements ( ) ; } } } ) ; setTimeout ( ( ) => { this . stateUpdater = this . ow . getMainWindow ( ) . mainWindowStoreUpdater ; } ) ; } private async detectNewAchievementFromMemory ( retriesLeft = 10 , forceAchievementsUpdate = false ) { if ( retriesLeft < 0 ) { return ; } console . debug ( '[achievement-monitor] detecting achievements from memory' ) ; const [ existingAchievements , achievementsProgress ] = await Promise . all ( [ this . achievementsManager . getAchievements ( ) , this . memory . getInGameAchievementsProgressInfo ( ) , ] ) ; if ( process . env . NODE_ENV !== 'production' ) { console . debug ( '[achievement-monitor] retrieved achievements from memory' , { existingAchievements : existingAchievements , achievementsProgress : achievementsProgress , achievementQuotas : this . achievementQuotas , previousAchievements : this . previousAchievements , prorgressWithQuota : ( achievementsProgress ?. achievements || [ ] ) ?. filter ( ( progress ) => progress . progress >= this . achievementQuotas [ progress . id ] ) , } ) ; } else { console . debug ( '[achievement-monitor] retrieved achievements from memory' ) ; } const computedProgress = achievementsProgress ?. achievements ?. length ? achievementsProgress . achievements : this . achievementsDiff ( this . previousAchievements , existingAchievements ) ; if ( process . env . NODE_ENV !== 'production' ) { console . debug ( '[achievement-monitor] computed progress' , computedProgress , achievementsProgress , this . previousAchievements , existingAchievements ) ; } const unlockedAchievements = computedProgress ?. filter ( ( progress ) => progress . progress >= this . achievementQuotas [ progress . id ] ) . map ( ( progress ) => progress . id ) . map ( ( id ) => existingAchievements . find ( ( ach ) => ach . id === id ) || { id : id , progress : 0 , completed : false , } ) . filter ( ( ach ) => ! ach . completed || ( this . previousAchievements && ! this . previousAchievements . find ( ( a ) => a . id === ach . id ) ?. completed ) ) ; console . log ( '[achievement-monitor] unlocked achievements' , unlockedAchievements ?. map ( ( a ) => a . id ) ) ; if ( ! unlockedAchievements . length ) { if ( process . env . NODE_ENV !== 'production' ) { console . debug ( '[achievement-monitor] nothing from memory' , existingAchievements , achievementsProgress , ( achievementsProgress ?. achievements || [ ] ) ?. filter ( ( progress ) => progress . progress >= this . achievementQuotas [ progress . id ] ) , unlockedAchievements ) ; } setTimeout ( ( ) => { this . detectNewAchievementFromMemory ( retriesLeft - 1 , true ) ; return ; } , 150 ) ; return ; } const achievements = await Promise . all ( unlockedAchievements . map ( ( ach ) => this . achievementLoader . getAchievement ( `hearthstone_game_${ ach . id }` ) ) ) ; console . log ( '[achievement-monitor] built achievements, emitting events' , achievements ?. map ( ( a ) => a ?. id ) ) ; await Promise . all ( achievements . map ( ( ach ) => this . sendUnlockEventFromAchievement ( ach ) ) ) ; this . previousAchievements = existingAchievements ; } private achievementsDiff ( previousAchievements , currentAchievements ) { if ( ! previousAchievements ?. length ) { return currentAchievements ?? [ ] ; } if ( ! currentAchievements ?. length ) { return previousAchievements ?? [ ] ; } const allKeys = [ ... previousAchievements . map ( ( a ) => a . id ) , ... currentAchievements . map ( ( a ) => a . id ) ] . filter ( ( id ) => id ) ; if ( ! allKeys ?. length ) { return [ ] ; } const uniqueKeys = [ ... new Set ( allKeys ) ] ; return uniqueKeys . map ( ( achievementId ) => { const previousAchievement = previousAchievements . find ( ( a ) => a . id === achievementId ) ; const currentAchievement = currentAchievements . find ( ( a ) => a . id === achievementId ) ; const progress = ( currentAchievement ?. progress ?? 0 ) - ( previousAchievement ?. progress ?? 0 ) ; if ( progress <= 0 ) { return null ; } return { id : achievementId , progress : progress , completed : currentAchievement ?. completed || previousAchievement ?. completed , } ; } ) . filter ( ( a ) => a ) ; } private async handleEvent ( gameEvent ) { const prefs = await this . prefs . getPreferences ( ) ; if ( this . spectating || ! prefs . achievementsEnabled2 ) { return ; } for ( const challenge of await this . achievementLoader . getChallengeModules ( ) ) { try { challenge . detect ( gameEvent , ( ) => { this . sendUnlockEvent ( challenge ) ; } ) ; } catch ( e ) { console . error ( 'Exception while trying to handle challenge' , challenge . achievementId , e ) ; } } } private async sendUnlockEvent ( challenge ) { const achievement = await this . achievementLoader . getAchievement ( challenge . achievementId ) ; if ( ! achievement ) { console . warn ( 'trying to send unlock event for empty achievement' , challenge . achievementId ) ; return ; } await this . sendUnlockEventFromAchievement ( achievement ) ; } private async sendUnlockEventFromAchievement ( achievement ) { if ( ! achievement ) { console . warn ( 'trying to send unlock event for empty achievement' ) ; return ; } const autoGrantAchievements = await this . achievementLoader . getAchievementsById ( achievement . linkedAchievementIds ) ; const allAchievements = autoGrantAchievements . length > 0 ? [ achievement , ... autoGrantAchievements ] : [ achievement ] ; for ( const achv of allAchievements ) { const existingAchievement = this . achievementsStorage . getAchievement ( achv . id ) ; if ( existingAchievement . numberOfCompletions >= 1 ) { continue ; } const completedAchievement = new CompletedAchievement ( existingAchievement . id , existingAchievement . numberOfCompletions + 1 ) ; console . log ( '[achievement-monitor] starting process of completed achievement' , achievement . id ) ; const mergedAchievement = Object . assign ( new Achievement ( ) , achv , { numberOfCompletions : completedAchievement . numberOfCompletions , } as Achievement ) ; this . achievementsStorage . save ( completedAchievement ) ; console . log ( '[achievement-monitor] saved achievement' , achievement . id ) ; this . remoteAchievements . publishRemoteAchievement ( mergedAchievement ) ; console . log ( '[achievement-monitor] broadcasting event completion event' , achievement . id ) ; this . enqueue ( { achievement : mergedAchievement } as InternalEvent ) ; } } private enqueue ( event ) { this . lastReceivedTimestamp = Date . now ( ) ; this . processingQueue . enqueue ( event ) ; } private async processQueue ( eventQueue ) { if ( Date . now ( ) - this . lastReceivedTimestamp < 500 ) { return eventQueue ; } const candidate = eventQueue [ 0 ] ; const betterCandidate = eventQueue . filter ( ( event ) => event . achievement . type === candidate . achievement . type ) . sort ( ( a , b ) => b . achievement . priority - a . achievement . priority ) [ 0 ] ; this . events . broadcast ( Events . ACHIEVEMENT_COMPLETE , betterCandidate . achievement ) ; this . prepareAchievementCompletedEvent ( betterCandidate . achievement ) ; return eventQueue . filter ( ( event ) => event . achievement . type !== betterCandidate . achievement . type ) ; } private async prepareAchievementCompletedEvent ( achievement ) { this . store . stateUpdater . next ( new AchievementCompletedEvent ( achievement ) ) ; } private async startAchievementsProgressDetection ( ) { this . achievementsProgressInterval = setInterval ( ( ) => this . detectAchievementProgress ( ) , 5000 ) ; } private async stopAchievementsProgressDetection ( ) { if ( this . achievementsProgressInterval ) { clearInterval ( this . achievementsProgressInterval ) ; } } private async assignPreviousAchievements ( ) { const existingAchievements = await this . achievementsManager . getAchievements ( true ) ; if ( ! existingAchievements ) { return ; } if ( ! this . previousAchievements ) { console . log ( '[achievements-monitor] assigning previous achievements' , existingAchievements . length ) ; this . previousAchievements = existingAchievements ; } } private async detectAchievementProgress ( ) { const achievementsProgress = await this . memory . getInGameAchievementsProgressInfo ( ) ; const allAchievements = await this . achievementLoader . getAchievements ( ) ; if ( ! achievementsProgress ?. achievements ?. length ) { return ; } const achievementInfos = achievementsProgress . achievements . map ( ( ach ) => { const ref = allAchievements . find ( ( a ) => a . id === `hearthstone_game_${ ach . id }` ) ; const quota = this . achievementQuotas [ ach . id ] ; return { id : ref . id , progress : ach . progress , quota : quota , completed : ach . completed || ref . numberOfCompletions > 0 || ach . progress >= quota , text : ref . text , name : ref . name , step : ref . priority , type : ref . type , ref : ref , } ; } ) ; const achievementsInfo = { achievements : achievementInfos , } ; this . events . broadcast ( Events . ACHIEVEMENT_PROGRESSION , achievementsInfo ) ; } private previousState : readonly HsAchievementInfo [ ] = [ ] ; private async startGlobalAchievementsProgressDetection ( ) { setInterval ( async ( ) => { const inGame = await this . ow . inGame ( ) ; if ( ! inGame ) { return ; } const prefs = await this . prefs . getPreferences ( ) ; if ( ! prefs . achievementsEnabled2 || ! prefs . achievementsLiveTracking2 ) { return ; } const currentState = await this . achievementsManager . getAchievements ( true , false ) ; const diff = this . achievementsDiff ( this . previousState , currentState ) ; if ( diff ?. length ) { this . stateUpdater . next ( new AchievementsUpdatedEvent ( diff ) ) ; } this . previousState = currentState ; } , 15 * 1000 ) ; } } interface InternalEvent { readonly achievement : Achievement ; }