Skip to content

Commit

Permalink
feat(Battlegrounds): hero picking for Duos
Browse files Browse the repository at this point in the history
  • Loading branch information
fmoraes74 committed Jun 7, 2024
1 parent 78e6324 commit f6eb03a
Show file tree
Hide file tree
Showing 21 changed files with 311 additions and 88 deletions.
32 changes: 22 additions & 10 deletions HSTracker/BobsBuddy/BobsBuddyInvoker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ class BobsBuddyInvoker {
func maybeRunDuosPartialCombat() -> Promise<Bool> {
return Promise<Bool> { seal in
if input != nil && !(duosInputPlayerTeammate == nil || duosInputOpponentTeammate == nil) {
logger.debug("No need to run patial combat, all teammates found. Exiting.")
logger.debug("No need to run partial combat, all teammates found. Exiting.")
seal.fulfill(false)
return
}
Expand Down Expand Up @@ -250,8 +250,18 @@ class BobsBuddyInvoker {
BobsBuddyInvoker.bobsBuddyDisplay.setErrorState(error: .notEnoughData)
} else if self.state == .combatPartial {
logger.debug("Displaying partial simulation results")
let winRate = top.winRate
let tieRate = top.tieRate
let lossRate = top.lossRate
let myDeathRate = top.myDeathRate
let theirDeathRate = top.theirDeathRate
let possibleResults = top.getResultDamage()
let friendlyWon = self.duosInputPlayerTeammate == nil
let playerCanDie = input.player.health <= input.damageCap
let opponentCanDie = input.opponent.health <= input.damageCap

DispatchQueue.main.async {
BobsBuddyInvoker.bobsBuddyDisplay.showPartialDuosSimulation(winRate: top.winRate, tieRate: top.tieRate, lossRate: top.lossRate, playerLethal: top.theirDeathRate, opponentLethal: top.myDeathRate, possibleResults: top.getResultDamage(), friendlyWon: self.duosInputPlayerTeammate == nil, playerCanDie: input.player.health <= input.damageCap, opponentCanDie: input.opponent.health <= input.damageCap)
BobsBuddyInvoker.bobsBuddyDisplay.showPartialDuosSimulation(winRate: winRate, tieRate: tieRate, lossRate: lossRate, playerLethal: theirDeathRate, opponentLethal: myDeathRate, possibleResults: possibleResults, friendlyWon: friendlyWon, playerCanDie: playerCanDie, opponentCanDie: opponentCanDie)
}
} else {
logger.debug("Displaying simulation results")
Expand Down Expand Up @@ -410,18 +420,20 @@ class BobsBuddyInvoker {
logger.debug("\(_instanceKey) already in shopping state. Exiting")
return
}
let wasPreviousStateParcial = state == .combatPartial
state = wasPreviousStateParcial ? .shoppingAfterPartial : .shopping
if hasErrorState() {
return
}
let wasPreviousStatePartial = state == .combatPartial
if isGameOver {
BobsBuddyInvoker.bobsBuddyDisplay.setState(st: wasPreviousStateParcial ? .gameOverAfterPartial : .gameOver)
logger.debug("Setting UI state to GameOver")
if state != .initial {
logger.debug("Setting UI state to GameOver")
state = wasPreviousStatePartial ? .gameOverAfterPartial : .gameOver
}
} else {
BobsBuddyInvoker.bobsBuddyDisplay.setState(st: wasPreviousStateParcial ? .shoppingAfterPartial : .shopping)
logger.debug("Setting UI state to shopping")
state = wasPreviousStatePartial ? .shoppingAfterPartial : .shopping
}
if hasErrorState() {
return
}
BobsBuddyInvoker.bobsBuddyDisplay.setState(st: wasPreviousStatePartial ? .gameOverAfterPartial : .gameOver)
validateSimulationResult()
}

Expand Down
2 changes: 1 addition & 1 deletion HSTracker/HSReplay/Data/BattlegroundsHeroPickStats.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct BattlegroundsHeroPickStats: Decodable {
var pick_rate: Double?
var avg_placement: Double?
var placement_distribution: [Double]?
var first_place_comp_popularity: [BattlegroundsComposition]
var first_place_comp_popularity: [BattlegroundsComposition]?
}

struct BattlegroundsHeroPickToast: Decodable {
Expand Down
1 change: 1 addition & 0 deletions HSTracker/HSReplay/HSReplay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ struct HSReplay {
static let accountUrl = "\(baseApiUrl)\(accountApi)"

static let tier7HeroPickStatsUrl = "\(baseApiUrl)/battlegrounds/hero_pick/"
static let tier7DuosHeroPickStatsUrl = "\(baseApiUrl)/battlegrounds/duos/hero_pick/"
static let tier7QuestStatsUrl = "\(baseApiUrl)/battlegrounds/quest_stats/"
static let tier7AllTimeMMR = "\(baseApiUrl)/battlegrounds/alltime/"
static let playerTrial = "\(baseApiUrl)/playertrials/"
Expand Down
65 changes: 65 additions & 0 deletions HSTracker/HSReplay/HSReplayAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,71 @@ class HSReplayAPI {
}
}

@available(macOS 10.15.0, *)
static func getTier7DuosHeroPickStats(parameters: BattlegroundsHeroPickStatsParams) async -> BattlegroundsHeroPickStats? {
return await withCheckedContinuation { continuation in
let encoder = JSONEncoder()
var body: Data?
do {
body = try encoder.encode(parameters)
if let body = body {
logger.debug("Sending hero picks request: \(String(data: body, encoding: .utf8) ?? "ERROR")")
}
} catch {
logger.error(error)
}
oauthswift.client.request("\(HSReplay.tier7DuosHeroPickStatsUrl)", method: .POST, headers: ["Content-Type": "application/json"], body: body, completionHandler: { result in
switch result {
case .success(let response):
logger.debug("Response: \(String(data: response.data, encoding: .utf8) ?? "FAILED")")
let bqs: BattlegroundsHeroPickStats? = parseResponse(data: response.data, defaultValue: nil)
continuation.resume(returning: bqs)
return
case .failure(let error):
logger.error(error)
continuation.resume(returning: nil)
return
}
})
}
}

@available(macOS 10.15.0, *)
static func getTier7DuosHeroPickStats(token: String?, parameters: BattlegroundsHeroPickStatsParams) async -> BattlegroundsHeroPickStats? {
guard let token = token else {
return nil
}
return await withCheckedContinuation { continuation in
let encoder = JSONEncoder()
var body: Data?
do {
body = try encoder.encode(parameters)
if let body = body {
logger.debug("Sending hero picks request: \(String(data: body, encoding: .utf8) ?? "ERROR")")
}
} catch {
logger.error(error)
}
guard let body = body else {
continuation.resume(returning: nil)
return
}
let http = Http(url: "\(HSReplay.tier7DuosHeroPickStatsUrl)")
_ = http.uploadPromise(method: .post, headers: ["Content-Type": "application/json", "X-Trial-Token": token], data: body).done { response in
guard let data = response as? Data else {
continuation.resume(returning: nil)
return
}
logger.debug("Response: \(String(data: data, encoding: .utf8) ?? "FAILED")")
let bqs: BattlegroundsHeroPickStats? = parseResponse(data: data, defaultValue: nil)
continuation.resume(returning: bqs)
}.catch { error in
logger.error(error)
continuation.resume(returning: nil)
}
}
}

@available(macOS 10.15.0, *)
static func getTier7QuestStats(parameters: BattlegroundsQuestStatsParams) async -> [BattlegroundsQuestStats]? {
return await withCheckedContinuation { continuation in
Expand Down
71 changes: 71 additions & 0 deletions HSTracker/HearthMirror/DeckWatcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -787,3 +787,74 @@ class BattlegroundsLeaderboardWatcher: Watcher {
}
}

struct BattlegroundsTeammateBoardStateEntity: Equatable {
var cardId: String
var tags: [Int: Int]

init(entity: MirrorBattlegroundsTeammateBoardStateEntity) {
cardId = entity.cardId
tags = Dictionary(uniqueKeysWithValues: entity.tags.compactMap { x in (x.key.intValue, x.value.intValue) })
}
}

struct BattlegroundsTeammateBoardStateArgs: Equatable {
var isViewingTeammate: Bool
var mulliganHeroes: [String]
var entities: [BattlegroundsTeammateBoardStateEntity]

init(boardState: MirrorBattlegroundsTeammateBoardState?) {
isViewingTeammate = boardState?.viewingTeammate ?? false
mulliganHeroes = boardState?.mulliganHeroes ?? [String]()
entities = boardState?.entities.compactMap { be in BattlegroundsTeammateBoardStateEntity(entity: be) } ?? [BattlegroundsTeammateBoardStateEntity]()
}
}

class BattlegroundsTeammateBoardStateWatcher: Watcher {
static var change: ((_ sender: BattlegroundsTeammateBoardStateWatcher, _ args: BattlegroundsTeammateBoardStateArgs) -> Void)?

var _watch: Bool = false
var _prev: BattlegroundsTeammateBoardStateArgs?

static let _instance = BattlegroundsTeammateBoardStateWatcher()

init(delay: TimeInterval = 0.200) {
super.init()

refreshInterval = delay
}

override func run() {
_watch = true
update()
}

static func start() {
_instance.startWatching()
}

static func stop() {
_instance._watch = false
_instance.stopWatching()
}

private func update() {
isRunning = true
while _watch {
Thread.sleep(forTimeInterval: refreshInterval)
if !_watch {
break
}

let value = MirrorHelper.getBattlegroundsTeammateBoardState()
let curr = BattlegroundsTeammateBoardStateArgs(boardState: value)
if curr == _prev {
continue
}
BattlegroundsTeammateBoardStateWatcher.change?(self, curr)
_prev = curr
}
_prev = nil
isRunning = false
}
}

8 changes: 8 additions & 0 deletions HSTracker/HearthMirror/MirrorHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,5 +496,13 @@ struct MirrorHelper {
}
return result
}

static func getBattlegroundsTeammateBoardState() -> MirrorBattlegroundsTeammateBoardState? {
var result: MirrorBattlegroundsTeammateBoardState?
MirrorHelper.accessQueue.sync {
result = mirror?.getBattlegroundsTeammateBoardState()
}
return result
}
}

6 changes: 6 additions & 0 deletions HSTracker/Logging/CoreManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ final class CoreManager: NSObject {
SceneHandler.onSceneUpdate(prevMode: Mode.allCases[args.prevMode], mode: Mode.allCases[args.mode], sceneLoaded: args.sceneLoaded, transitioning: args.transitioning)
}

BattlegroundsTeammateBoardStateWatcher.change = { _, args in
self.game.windowManager.battlegroundsHeroPicking.viewModel.isViewingTeammate = args.isViewingTeammate
// rest is not used
}

ExperienceWatcher.newExperienceHandler = { args in
AppDelegate.instance().coreManager.game.experienceChangedAsync(experience: args.experience, experienceNeeded: args.experienceNeeded, level: args.level, levelChange: args.levelChange, animate: args.animate)
}
Expand Down Expand Up @@ -312,6 +317,7 @@ final class CoreManager: NSObject {
BaconWatcher.stop()
SceneWatcher.stop()
BattlegroundsLeaderboardWatcher.stop()
BattlegroundsTeammateBoardStateWatcher.stop()
DeckPickerWatcher.stop()
MirrorHelper.destroy()
let wm = game.windowManager
Expand Down
68 changes: 38 additions & 30 deletions HSTracker/Logging/Game.swift
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ class Game: NSObject, PowerEventHandler {
@available(macOS 10.15, *)
@MainActor
func updateTier7PreLobbyVisibility() {
let show = isRunning && isInMenu && !queueEvents.isInQueue && SceneHandler.scene == .bacon && Settings.enableTier7Overlay && Settings.showBattlegroundsTier7PreLobby && windowManager.tier7PreLobby.viewModel.battlegroundsGameMode != .duos && windowManager.tier7PreLobby.viewModel.visibility
let show = isRunning && isInMenu && !queueEvents.isInQueue && SceneHandler.scene == .bacon && Settings.enableTier7Overlay && Settings.showBattlegroundsTier7PreLobby && (windowManager.tier7PreLobby.viewModel.battlegroundsGameMode == .solo || windowManager.tier7PreLobby.viewModel.battlegroundsGameMode == .duos) && windowManager.tier7PreLobby.viewModel.visibility
if show {
Task.init {
_ = await windowManager.tier7PreLobby.viewModel.update()
Expand Down Expand Up @@ -1706,6 +1706,9 @@ class Game: NSObject, PowerEventHandler {
if (self.gameEntity?[.step] ?? 0) > Step.begin_mulligan.rawValue {
self.windowManager.battlegroundsSession.update()
BattlegroundsLeaderboardWatcher.start()
if self.isBattlegroundsDuosMatch() {
BattlegroundsTeammateBoardStateWatcher.start()
}
self.updateBattlegroundsOverlays()
}
}
Expand Down Expand Up @@ -2519,9 +2522,13 @@ class Game: NSObject, PowerEventHandler {

// At this point the user either owns tier7 or has an active trial!

let isDuos = isBattlegroundsDuosMatch()

let stats = (token != nil && !userOwnsTier7) ?
await HSReplayAPI.getTier7HeroPickStats(token: token, parameters: parameters) :
await HSReplayAPI.getTier7HeroPickStats(parameters: parameters)
// trial
isDuos ? await HSReplayAPI.getTier7DuosHeroPickStats(token: token, parameters: parameters) : await HSReplayAPI.getTier7HeroPickStats(token: token, parameters: parameters) :
// tier 7
isDuos ? await HSReplayAPI.getTier7DuosHeroPickStats(parameters: parameters) : await HSReplayAPI.getTier7HeroPickStats(parameters: parameters)

if stats == nil {
// FIXME: add
Expand Down Expand Up @@ -2614,43 +2621,43 @@ class Game: NSObject, PowerEventHandler {

await windowManager.battlegroundsSession.update()

if isBattlegroundsDuosMatch() {
BattlegroundsTeammateBoardStateWatcher.start()
}

if gameEntity?[.step] != Step.begin_mulligan.rawValue {
return
}

snapshotBattlegroundsOfferedHeroes(heroes)
cacheBattlegroundsHeroPickParams()

if isBattlegroundsSoloMatch() {
let heroIds = heroes.sorted(by: { (a, b) -> Bool in a.zonePosition < b.zonePosition }).compactMap { x in x.card.dbfId }

async let statsTask = getBattlegroundsHeroPickStats()

// Wait for the mulligan to be ready
await waitForMulliganStart()
let heroIds = heroes.sorted(by: { (a, b) -> Bool in a.zonePosition < b.zonePosition }).compactMap { x in x.card.dbfId }

async let waitAndAppear: () = Task.sleep(seconds: 500)
async let statsTask = getBattlegroundsHeroPickStats()

// Wait for the mulligan to be ready
await waitForMulliganStart()

async let waitAndAppear: () = Task.sleep(seconds: 500)

var battlegroundsHeroPickStats: BattlegroundsHeroPickStats?

let (finalResults, _) = await (statsTask, waitAndAppear)
battlegroundsHeroPickStats = finalResults

var battlegroundsHeroPickStats: BattlegroundsHeroPickStats?
var toastParams: [String: String]?

let (finalResults, _) = await (statsTask, waitAndAppear)
battlegroundsHeroPickStats = finalResults

var toastParams: [String: String]?

if let stats = battlegroundsHeroPickStats {
toastParams = stats.toast.parameters
DispatchQueue.main.async {
self.showBattlegroundsHeroPickingStats(heroIds.compactMap { dbfId in stats.data.first { x in x.hero_dbf_id == dbfId }}, stats.toast.parameters, stats.toast.min_mmr, stats.toast.anomaly_adjusted ?? false)
}
}
// TODO: handle errors, exceptions

if Settings.showHeroToast {
showBattlegroundsHeroPanel(heroIds, toastParams)
if let stats = battlegroundsHeroPickStats {
toastParams = stats.toast.parameters
DispatchQueue.main.async {
self.showBattlegroundsHeroPickingStats(heroIds.compactMap { dbfId in stats.data.first { x in x.hero_dbf_id == dbfId }}, stats.toast.parameters, stats.toast.min_mmr, stats.toast.anomaly_adjusted ?? false)
}
} else {
// Duos...
}
// TODO: handle errors, exceptions

if Settings.showHeroToast {
showBattlegroundsHeroPanel(heroIds, isBattlegroundsDuosMatch(), toastParams)
}
}

Expand Down Expand Up @@ -3429,10 +3436,11 @@ class Game: NSObject, PowerEventHandler {
}
}

func showBattlegroundsHeroPanel(_ heroIds: [Int], _ parameters: [String: String]?) {
func showBattlegroundsHeroPanel(_ heroIds: [Int], _ duos: Bool, _ parameters: [String: String]?) {
DispatchQueue.main.async {
let toast = BgHeroesToastView(frame: NSRect.zero)
toast.heroIds = heroIds
toast.duos = duos
toast.anomalyDbfId = BattlegroundsUtils.getBattlegroundsAnomalyDbfId(game: self.gameEntity)
toast.parameters = parameters

Expand Down
Loading

0 comments on commit f6eb03a

Please sign in to comment.