Skip to content

Commit

Permalink
feat: detect tome of retraining uses (#26)
Browse files Browse the repository at this point in the history
* refactor: differ between numeric and alphanumeric itemids in parser

* test: detailed assertions on one replays' actions and units

* test: adjust tests to reflect newly added uint8 action parsing

* improvement: implement basic retraining detection

* style: correct linter errors

* improvement: simple tome of retraining implementation
  • Loading branch information
PBug90 authored Aug 22, 2019
1 parent 24426f9 commit 7e96e9d
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 73 deletions.
Binary file added replays/standard_tomeofretraining_1.w3g
Binary file not shown.
119 changes: 69 additions & 50 deletions src/Player.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,40 @@
import convert from './convert'
import { items, units, buildings, upgrades, abilityToHero } from './mappings'
import { Races } from './types'
import { Races, ItemID } from './types'

/**
* Helpers
*/
const reverseString = (input: string) => input.split('').reverse().join('')
const isObjectId = (input: string) => ['u', 'e', 'h', 'o'].includes(input[0])
const isRightclickAction = (input: number[]) => input[0] === 0x03 && input[1] === 0
const isBasicAction = (input: number[]) => input[0] <= 0x19 && input[1] === 0

interface HeroInfo {
export const reduceHeroes = (heroCollector: {[key: string]: HeroInfo}) => {
return Object.values(heroCollector).sort((h1, h2) => h1.order - h2.order).reduce((aggregator, hero) => {
hero.level = Object.values(hero.abilities).reduce((prev, curr) => prev + curr, 0)
delete hero['order']
aggregator.push(hero)
return aggregator
}, <HeroInfo[]>[])
}

interface Ability {
type: 'ability';
time: number;
value: string;
}

interface Retraining{
type: 'retraining';
time: number;
}

export interface HeroInfo {
level: number;
abilities: { [key: string]: number };
order: number;
id: string;
retrainingHistory: {time: number; abilities: {[key: string]: number}}[];
abilityOrder: (Ability | Retraining) [];
}

class Player {
Expand Down Expand Up @@ -54,8 +74,6 @@ class Player {

heroCollector: {[key: string]: HeroInfo}

heroSkills: { [key: string]: number }

heroCount: number

actions: {
Expand All @@ -75,6 +93,10 @@ class Player {

_currentlyTrackedAPM: number

_retrainingMetadata: {[key: string]: {start: number; end: number}}

_lastRetrainingTime: number

_lastActionWasDeselect: boolean

currentTimePlayed: number
Expand All @@ -94,7 +116,6 @@ class Player {
this.buildings = { summary: {}, order: [] }
this.heroes = []
this.heroCollector = {}
this.heroSkills = {}
this.heroCount = 0
this.actions = {
timed: [],
Expand All @@ -112,6 +133,8 @@ class Player {
}
this._currentlyTrackedAPM = 0
this._lastActionWasDeselect = false
this._retrainingMetadata = {}
this._lastRetrainingTime = 0
this.currentTimePlayed = 0
this.apm = 0
return this
Expand Down Expand Up @@ -139,7 +162,7 @@ class Player {
}
}

handleActionId (actionId: string, gametime: number): void {
handleStringencodedItemID (actionId: string, gametime: number): void {
if (units[actionId]) {
this.units.summary[actionId] = this.units.summary[actionId] + 1 || 1
this.units.order.push({ id: actionId, ms: gametime })
Expand All @@ -155,88 +178,89 @@ class Player {
}
}

handleHeroSkill (actionId: string): void {
if (this.heroCollector[abilityToHero[actionId]] === undefined) {
handleHeroSkill (actionId: string, gametime: number): void {
const heroId = abilityToHero[actionId]
if (this.heroCollector[heroId] === undefined) {
this.heroCount += 1
this.heroCollector[abilityToHero[actionId]] = { level: 0, abilities: {}, order: this.heroCount, id: abilityToHero[actionId] }
this.heroCollector[heroId] = { level: 0, abilities: {}, order: this.heroCount, id: heroId, abilityOrder: [], retrainingHistory: [] }
}
this.heroCollector[abilityToHero[actionId]].level += 1
this.heroCollector[abilityToHero[actionId]].abilities[actionId] = this.heroCollector[abilityToHero[actionId]].abilities[actionId] || 0
this.heroCollector[abilityToHero[actionId]].abilities[actionId] += 1
}

handle0x10 (actionId: string, gametime: number): void {
if (typeof actionId === 'string') {
actionId = reverseString(actionId)
if (this._lastRetrainingTime > 0) {
this.heroCollector[heroId].retrainingHistory.push({ time: this._lastRetrainingTime, abilities: this.heroCollector[heroId].abilities })
this.heroCollector[heroId].abilities = {}
this.heroCollector[heroId].abilityOrder.push({ type: 'retraining', time: this._lastRetrainingTime })
this._lastRetrainingTime = 0
}
this.heroCollector[heroId].abilities[actionId] = this.heroCollector[heroId].abilities[actionId] || 0
this.heroCollector[heroId].abilities[actionId] += 1
this.heroCollector[heroId].abilityOrder.push({ type: 'ability', time: gametime, value: actionId })
}

switch (actionId[0]) {
handleRetraining (gametime: number) {
this._lastRetrainingTime = gametime
}

handle0x10 (itemid: ItemID, gametime: number): void {
switch (itemid.value[0]) {
case 'A':
this.heroSkills[actionId] = this.heroSkills[actionId] + 1 || 1
this.handleHeroSkill(actionId)
this.handleHeroSkill(itemid.value, gametime)
break
case 'R':
this.handleActionId(actionId, gametime)
this.handleStringencodedItemID(itemid.value, gametime)
break
case 'u':
case 'e':
case 'h':
case 'o':
if (!this.raceDetected) {
this.detectRaceByActionId(actionId)
this.detectRaceByActionId(itemid.value)
}
this.handleActionId(actionId, gametime)
this.handleStringencodedItemID(itemid.value, gametime)
break
default:
this.handleActionId(actionId, gametime)
this.handleStringencodedItemID(itemid.value, gametime)
}

actionId[0] !== '0'
itemid.value[0] !== '0'
? this.actions['buildtrain'] = this.actions['buildtrain'] + 1 || 1
: this.actions['ability'] = this.actions['ability'] + 1 || 1

this._currentlyTrackedAPM++
}

handle0x11 (actionId: string | number[], gametime: number): void {
handle0x11 (itemid: ItemID, gametime: number): void {
this._currentlyTrackedAPM++

if (Array.isArray(actionId)) {
if (actionId[0] <= 0x19 && actionId[1] === 0) {
if (itemid.type === 'alphanumeric') {
if (itemid.value[0] <= 0x19 && itemid.value[1] === 0) {
this.actions['basic'] = this.actions['basic'] + 1 || 1
} else {
this.actions['ability'] = this.actions['ability'] + 1 || 1
}

return
}

actionId = reverseString(actionId)
if (isObjectId(actionId)) {
this.handleActionId(actionId, gametime)
} else {
this.handleStringencodedItemID(itemid.value, gametime)
}
}

handle0x12 (actionId: number[]): void {
if (isRightclickAction(actionId)) {
handle0x12 (itemid: ItemID): void {
if (isRightclickAction(itemid.value)) {
this.actions['rightclick'] = this.actions['rightclick'] + 1 || 1
} else if (isBasicAction(actionId)) {
} else if (isBasicAction(itemid.value)) {
this.actions['basic'] = this.actions['basic'] + 1 || 1
} else {
this.actions['ability'] = this.actions['ability'] + 1 || 1
}
this._currentlyTrackedAPM++
}

handle0x13 (actionId: string): void {
handle0x13 (itemid: string): void {
this.actions['item'] = this.actions['item'] + 1 || 1
this._currentlyTrackedAPM++
}

handle0x14 (actionId: number[]): void {
if (isRightclickAction(actionId)) {
handle0x14 (itemid: ItemID): void {
if (isRightclickAction(itemid.value)) {
this.actions['rightclick'] = this.actions['rightclick'] + 1 || 1
} else if (isBasicAction(actionId)) {
} else if (isBasicAction(itemid.value)) {
this.actions['basic'] = this.actions['basic'] + 1 || 1
} else {
this.actions['ability'] = this.actions['ability'] + 1 || 1
Expand Down Expand Up @@ -281,12 +305,7 @@ class Player {
cleanup (): void {
const apmSum = this.actions.timed.reduce((a: number, b: number): number => a + b)
this.apm = Math.round(apmSum / this.actions.timed.length)
this.heroes = Object.values(this.heroCollector).sort((h1, h2) => h1.order - h2.order).reduce((aggregator, hero) => {
delete hero['order']
aggregator.push(hero)
return aggregator
}, <HeroInfo[]>[])

this.heroes = reduceHeroes(this.heroCollector)
delete this._currentlyTrackedAPM
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/W3GReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class W3GReplay extends ReplayParser {

chatlog: any

playerActionTracker: {[key: string]: any[]} = {}

id: string = ''

leaveEvents: any[]
Expand Down Expand Up @@ -148,6 +150,12 @@ class W3GReplay extends ReplayParser {
}

handleActionBlock (action: ActionBlock, currentPlayer: Player) {
this.playerActionTracker[currentPlayer.id] = this.playerActionTracker[currentPlayer.id] || []
this.playerActionTracker[currentPlayer.id].push(action)

if (action.itemId && (action.itemId.value === 'tert' || action.itemId.value === 'tret')) {
currentPlayer.handleRetraining(this.totalTimeTracker)
}
switch (action.actionId) {
case 0x10:
currentPlayer.handle0x10(action.itemId, this.totalTimeTracker)
Expand Down
3 changes: 3 additions & 0 deletions src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const playerColor = (color: number): string => {
const gameVersion = (version: number): string => {
if (version === 10030) {
return `1.30.2+`
} else if (version > 10030) {
const str = String(version)
return `1.${str.substring(str.length - 2, str.length)}`
}
return `1.${version}`
}
Expand Down
24 changes: 12 additions & 12 deletions src/parsers/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const UnitBuildingAbilityActionTargetPosition = new Parser()
const UnitBuildingAbilityActionTargetPositionTargetObjectId = new Parser()
.int16le('abilityFlags')
.array('itemId', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
Expand All @@ -63,7 +63,7 @@ const UnitBuildingAbilityActionTargetPositionTargetObjectId = new Parser()
const GiveItemToUnitAction = new Parser()
.int16le('abilityFlags')
.array('itemId', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
Expand All @@ -80,7 +80,7 @@ const GiveItemToUnitAction = new Parser()
const UnitBuildingAbilityActionTwoTargetPositions = new Parser()
.int16le('abilityFlags')
.array('itemId1', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
Expand All @@ -90,7 +90,7 @@ const UnitBuildingAbilityActionTwoTargetPositions = new Parser()
.floatle('targetAX')
.floatle('targetAY')
.array('itemId2', {
type: 'int8',
type: 'uint8',
length: 4,
formatter: objectIdFormatter
})
Expand All @@ -100,13 +100,13 @@ const UnitBuildingAbilityActionTwoTargetPositions = new Parser()

const SelectionUnit = new Parser()
.array('itemId1', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
})
.array('itemId2', {
type: 'int8',
type: 'uint8',
length: 4,
formatter: objectIdFormatter
})
Expand All @@ -133,7 +133,7 @@ const SelectGroupHotkeyAction = new Parser()

const SelectSubgroupAction = new Parser()
.array('itemId', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
Expand All @@ -149,34 +149,34 @@ const UnknownAction1B = new Parser()
const SelectGroundItemAction = new Parser()
.skip(1)
.array('itemId1', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
})
.array('itemId2', {
type: 'int8',
type: 'uint8',
length: 4,
formatter: objectIdFormatter
})

const CancelHeroRevivalAction = new Parser()
.array('itemId1', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
})
.array('itemId2', {
type: 'int8',
type: 'uint8',
length: 4,
formatter: objectIdFormatter
})

const RemoveUnitFromBuildingQueueAction = new Parser()
.int8('slotNumber')
.array('itemId', {
type: 'int8',
type: 'uint8',
length: 4,
// @ts-ignore
formatter: objectIdFormatter
Expand Down
9 changes: 4 additions & 5 deletions src/parsers/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Parser } from 'binary-parser'
import { ItemID } from '../types'

const objectIdFormatter = (arr: any[]): any => {
let copy = arr
const objectIdFormatter = (arr: any[]): ItemID => {
if (arr[3] >= 0x41 && arr[3] <= 0x7A) {
copy = arr.slice()
return arr.map(e => String.fromCharCode(parseInt(e, 10))).join('')
return { type: 'stringencoded', value: arr.map(e => String.fromCharCode(parseInt(e, 10))).reverse().join('') }
}
return copy
return { type: 'alphanumeric', value: arr.map(e => parseInt(e, 16)) }
}

const raceFlagFormatter = (flag: Parser.Data): string | Parser.Data => {
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ export interface GameDataBlock {
[key: string]: any;
}

export interface ItemID {
type: 'alphanumeric' | 'stringencoded';
value: any;
}

export interface ParserOutput {
id: string;
gamename: GameMetaDataDecoded['gameName'];
Expand Down
Loading

0 comments on commit 7e96e9d

Please sign in to comment.