import { Injectable } from '@angular/core' ; import { ARENAS , CardClass , GameFormat , GameType , PRACTICE_ALL , ScenarioId , SCENARIO_WITHOUT_RESTART , SceneMode , } from '@firestone-hs/reference-data' ; import { CardsFacadeService } from '@services/cards-facade.service' ; import { DeckDefinition , decode , encode } from 'deckstrings' ; import { Metadata } from '../../models/decktracker/metadata' ; import { DuelsInfo } from '../../models/duels-info' ; import { GameEvent } from '../../models/game-event' ; import { DeckInfoFromMemory } from '../../models/mainwindow/decktracker/deck-info-from-memory' ; import { MemoryUpdate } from '../../models/memory/memory-update' ; import { ApiRunner } from '../api-runner' ; import { Events } from '../events.service' ; import { GameEventsEmitterService } from '../game-events-emitter.service' ; import { getDefaultHeroDbfIdForClass } from '../hs-utils' ; import { OverwolfService } from '../overwolf.service' ; import { MemoryInspectionService } from '../plugins/memory-inspection.service' ; import { groupByFunction } from '../utils' ; import { DeckHandlerService } from './deck-handler.service' ; const DECK_TEMPLATES_URL = `https://static.zerotoheroes.com/hearthstone/data/deck-templates.json?v=2` ; @ Injectable ( ) export class DeckParserService { private readonly deckNameRegex = new RegExp ( 'I \\d*:\\d*:\\d*.\\d* ### (.*)' ) ; private readonly deckstringRegex = new RegExp ( 'I \\d*:\\d*:\\d*.\\d* ([a-zA-Z0-9\\/\\+=]+)$' ) ; private spectating : boolean ; private selectedDeckId : number ; private currentNonGamePlayScene : SceneMode ; private currentScene : SceneMode ; private currentDeck : DeckInfo ; private deckTemplates : readonly DeckInfoFromMemory [ ] ; constructor ( private readonly gameEvents , private readonly events , private readonly memory , private readonly allCards , private readonly ow , private readonly handler , private readonly api ) { this . init ( ) ; } public async getCurrentDeck ( timeout ? ) { if ( ! timeout ) { return this . currentDeck ; } return Promise . race ( [ new Promise < DeckInfo > ( ( resolve ) => { const interval = setInterval ( ( ) => { if ( this . currentDeck ) { clearInterval ( interval ) ; resolve ( this . currentDeck ) ; } } , 1000 ) ; } ) , new Promise < DeckInfo > ( ( resolve ) => { setTimeout ( ( ) => { resolve ( null ) ; } , timeout ) ; } ) , ] ) ; } public async retrieveCurrentDeck ( usePreviousDeckIfSameScenarioId , metadata ) { console . log ( '[deck-parser] retrieving current deck' , this . currentDeck , usePreviousDeckIfSameScenarioId , metadata ) ; if ( this . spectating ) { console . log ( '[deck-parser] spectating, not returning any deck' ) ; return null ; } const shouldUseCachedDeck = metadata . gameType === GameType . GT_VS_AI && ! SCENARIO_WITHOUT_RESTART . includes ( metadata . scenarioId ) ; if ( shouldUseCachedDeck && this . currentDeck ?. deck && ( this . selectedDeckId || this . currentDeck . scenarioId === metadata . scenarioId ) ) { console . log ( '[deck-parser] returning cached deck' , this . currentDeck , metadata , this . selectedDeckId ) ; return this . currentDeck ; } console . log ( '[deck-parser] rebuilding deck' , this . currentDeck ?. scenarioId , metadata . scenarioId ) ; const deckFromMemory = await this . memory . getActiveDeck ( this . selectedDeckId , 2 ) ; console . log ( '[deck-parser] active deck from memory' , this . selectedDeckId , deckFromMemory ) ; let activeDeck = this . currentNonGamePlayScene === SceneMode . PVP_DUNGEON_RUN ? await this . getDuelsInfo ( ) : deckFromMemory ; console . log ( '[deck-parser] active deck after duels' , activeDeck , this . currentNonGamePlayScene ) ; if ( this . isDuelsInfo ( activeDeck ) && activeDeck . Wins === 0 && activeDeck . Losses === 0 ) { console . log ( '[deck-parser] not relying on memory reading for initial Duels deck, ignoring deck from memory' ) ; activeDeck = null ; } console . debug ( '[deck-parser] active deck' , activeDeck , this . isDeckLogged ( metadata . scenarioId ) , metadata . scenarioId , ARENAS ) ; let deckInfo ; if ( activeDeck && activeDeck . DeckList && activeDeck . DeckList . length > 0 ) { console . log ( '[deck-parser] updating active deck' , activeDeck , this . currentDeck ) ; deckInfo = this . updateDeckFromMemory ( activeDeck , metadata . scenarioId , metadata . gameType ) ; } else if ( this . isDeckLogged ( metadata . scenarioId ) ) { console . log ( '[deck-parser] trying to read previous deck from logs' , metadata . scenarioId ) ; deckInfo = await this . readDeckFromLogFile ( metadata . scenarioId , metadata . gameType ) ; } else { console . warn ( '[deck-parser] could not read any deck from memory' ) ; deckInfo = null ; } this . currentDeck = deckInfo ; console . log ( '[deck-parser] set current deck' , this . currentDeck , JSON . stringify ( this . currentDeck ) ) ; return this . currentDeck ; } public async getWhizbangDeck ( deckId , scenarioId , gameType ) { if ( this . spectating ) { console . log ( '[deck-parser] spectating, not returning Whizbang deck' ) ; return ; } const deck = this . deckTemplates . find ( ( deck ) => deck . DeckId === deckId ) ; console . debug ( '[deck-parser] found template deck' , deckId , deck , this . deckTemplates ) ; if ( deck && deck . DeckList && deck . DeckList . length > 0 ) { console . log ( '[deck-parser] updating active deck 2' , deck , this . currentDeck ) ; this . currentDeck = this . updateDeckFromMemory ( deck , scenarioId , gameType ) ; } else { this . currentDeck = null ; } return this . currentDeck ; } private async init ( ) { this . gameEvents . allEvents . subscribe ( ( event ) => { if ( event . type === GameEvent . SPECTATING ) { console . log ( '[deck-parser] spectating, resetting deck' , event . additionalData ) ; this . spectating = event . additionalData . spectating ; this . currentDeck = { } as DeckInfo ; } else if ( event . type === GameEvent . GAME_END ) { if ( this . currentDeck ?. gameType !== GameType . GT_VS_AI || SCENARIO_WITHOUT_RESTART . includes ( this . currentDeck ?. scenarioId ) ) { console . log ( '[deck-parser] game end on a non-restart scenario, resetting deck' , this . currentDeck ?. gameType , this . currentDeck ?. scenarioId ) ; this . currentDeck = { } as DeckInfo ; this . selectedDeckId = null ; } } } ) ; this . events . on ( Events . MEMORY_UPDATE ) . subscribe ( async ( data ) => { if ( this . spectating ) { console . log ( '[deck-parser] spectating, not registering memory update' ) ; return ; } const changes = data . data [ 0 ] ; if ( changes . SelectedDeckId ) { console . log ( '[deck-parser] selected deck id' , changes . SelectedDeckId ) ; this . selectedDeckId = changes . SelectedDeckId ; const activeDeck = await this . memory . getActiveDeck ( this . selectedDeckId , 2 , true ) ; console . log ( '[deck-parser] getting active deck from memory after ID selection' , this . selectedDeckId , activeDeck ) ; if ( activeDeck && activeDeck . DeckList && activeDeck . DeckList . length > 0 ) { console . log ( '[deck-parser] updating active deck after ID selection' , activeDeck , this . currentDeck ) ; this . currentDeck = this . updateDeckFromMemory ( activeDeck , null , null ) ; } } else if ( changes . CurrentScene && changes . CurrentScene !== SceneMode . GAMEPLAY ) { this . selectedDeckId = null ; } if ( changes . CurrentScene ) { this . currentNonGamePlayScene = ! changes . CurrentScene || changes . CurrentScene === SceneMode . GAMEPLAY ? this . currentNonGamePlayScene : changes . CurrentScene ; this . currentScene = changes . CurrentScene ; } } ) ; const templatesFromRemote = await this . api . callGetApi ( DECK_TEMPLATES_URL ) ; this . deckTemplates = ( templatesFromRemote ?? [ ] ) . filter ( ( template ) => template . DeckList ?. length ) . map ( ( template ) => ( { ... template , DeckList : template . DeckList . map ( ( dbfId ) => + dbfId ) , } as DeckInfoFromMemory ) ) ; console . debug ( '[deck-parser] loaded deck templates' , this . deckTemplates ?. length ) ; this . currentNonGamePlayScene = this . currentNonGamePlayScene ?? ( await this . memory . getCurrentSceneFromMindVision ( ) ) ; console . log ( '[deck-parser] initial scene' , this . currentNonGamePlayScene ) ; } private isDuelsInfo ( activeDeck ) { return ( activeDeck as DuelsInfo ) ?. Wins !== undefined ; } private async getDuelsInfo ( ) { let result = await this . memory . getDuelsInfo ( false , 3 ) ; if ( ! result ) { result = await this . memory . getDuelsInfo ( true , 3 ) ; } return result ; } private updateDeckFromMemory ( deckFromMemory , scenarioId , gameType ) { console . log ( '[deck-parser] updating deck from memory' , deckFromMemory ) ; if ( ! deckFromMemory ) { console . error ( '[deck-parser] no deck to update' ) ; return ; } const decklist = this . normalizeWithDbfIds ( deckFromMemory . DeckList ) ; console . log ( '[deck-parser] normalized decklist with dbf ids' , decklist , deckFromMemory . HeroCardId ) ; const deckDefinition = { format : deckFromMemory . FormatType || GameFormat . FT_WILD , cards : this . explodeDecklist ( decklist ) , heroes : deckFromMemory . HeroCardId ? [ this . handler . normalizeHero ( this . allCards . getCard ( deckFromMemory . HeroCardId ) ?. dbfId ) || 7 ] : deckFromMemory . HeroClass ? [ getDefaultHeroDbfIdForClass ( CardClass [ deckFromMemory . HeroClass ] ) || 7 ] : [ 7 ] , } ; console . log ( '[deck-parser] built deck definition' , deckDefinition ) ; const deckString = deckDefinition . cards . some ( ( pair ) => pair [ 0 ] == null ) ? null : encode ( deckDefinition ) ; console . log ( '[deck-parser] built deckstring' , deckString ) ; const currentDeck = { deck : deckDefinition , name : deckFromMemory . Name , deckstring : deckString , scenarioId : scenarioId , gameType : gameType , } ; console . log ( '[deck-parser] built deck info' , currentDeck ) ; return currentDeck ; } private normalizeWithDbfIds ( decklist ) { return decklist . map ( ( cardId ) => { const isDbfId = ! isNaN ( + cardId ) ; const card = isDbfId ? this . allCards . getCardFromDbfId ( + cardId ) : this . allCards . getCard ( cardId as string ) ; if ( ! card ?. dbfId ) { console . warn ( '[deck-parser] could not find card for dbfId' , cardId , isDbfId , card , this . allCards . getCards ( ) ?. length ) ; } return card ?. dbfId ; } ) ; } private isDeckLogged ( scenarioId ) { const loggingSCenarios = [ ... PRACTICE_ALL , ... ARENAS , ScenarioId . STANDARD_1_VS_1_GAME , ScenarioId . WIZARD_DUELS , ScenarioId . WIZARD_DUELS___ALTERAC_VALLEY , ] ; return loggingSCenarios . includes ( scenarioId ) ; } private async readDeckFromLogFile ( scenarioId , gameType , fileName = 'Decks.log' ) { const gameInfo = await this . ow . getRunningGameInfo ( ) ; if ( ! this . ow . gameRunning ( gameInfo ) ) { return ; } const logsLocation = gameInfo . executionPath . split ( 'Hearthstone.exe' ) [ 0 ] + 'Logs\\' + fileName ; const logsContents = await this . ow . readTextFile ( logsLocation ) ; if ( ! logsContents ) { return ; } const lines = logsContents . split ( '\n' ) . filter ( ( line ) => line && line . length > 0 ) . map ( ( line ) => line . trim ( ) ) ; if ( lines . length >= 4 ) { console . log ( '[deck-parser] lets go' , lines [ lines . length - 4 ] , 'hop' , lines [ lines . length - 3 ] ) ; const deckNameLogLine = ( await this . isDuelsDeck ( lines [ lines . length - 4 ] ) ) ? lines [ lines . length - 4 ] : lines [ lines . length - 3 ] ; const deckstringLogLine = ( await this . isDuelsDeck ( lines [ lines . length - 4 ] ) ) ? lines [ lines . length - 2 ] : lines [ lines . length - 1 ] ; let match ; const deckName = ( match = this . deckNameRegex . exec ( deckNameLogLine ) ) ? match [ 1 ] : undefined ; const deckstring = ( match = this . deckstringRegex . exec ( deckstringLogLine ) ) ? this . handler . normalizeDeckstring ( match [ 1 ] ) : undefined ; if ( ! deckstring ) { return null ; } return { deckstring : deckstring , name : deckName , scenarioId : scenarioId , gameType : gameType , deck : ! ! deckstring ? decode ( deckstring ) : undefined , } as DeckInfo ; console . log ( '[deck-parser] finished reading previous deck from logs' ) ; } } private async isDuelsDeck ( logLine ) { if ( ! logLine ?. length ) { return false ; } if ( ! logLine . includes ( '###' ) ) { return false ; } console . log ( '[deck-parser] current scene' , this . currentNonGamePlayScene ) ; return this . currentNonGamePlayScene === SceneMode . PVP_DUNGEON_RUN ; } private explodeDecklist ( initialDecklist ) { console . log ( '[deck-parser] decklist with dbfids' , initialDecklist ) ; const groupedById = groupByFunction ( ( cardId ) => '' + cardId ) ( initialDecklist ) ; const result = Object . keys ( groupedById ) . map ( ( id ) => [ + id , groupedById [ id ] . length ] ) ; console . log ( '[deck-parser] exploding decklist result' , result ) ; return result ; } } export interface DeckInfo { scenarioId : number ; name : string ; deckstring : string ; deck : DeckDefinition ; gameType : GameType ; }