Skip to content
1 change: 1 addition & 0 deletions example.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ var farseer = new Farseer();
farseer.on('game-start', console.log.bind(console, 'game-start'));
farseer.on('game-over', console.log.bind(console, 'game-over:'));
farseer.on('zone-change', console.log.bind(console, 'zone-change:'));
farseer.on('pack-opened', console.log.bind(console, 'pack-opened:'));
farseer.start();
2 changes: 2 additions & 0 deletions src/default-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export default function (log) {
}
defaultOptions.logFile = path.join('C:', programFiles, 'Hearthstone', 'Hearthstone_Data', 'output_log.txt');
defaultOptions.configFile = path.join(process.env.LOCALAPPDATA, 'Blizzard', 'Hearthstone', 'log.config');
defaultOptions.logFileAchievements = path.join('C:', programFiles, 'Hearthstone', 'Logs', 'Achievements.log');
} else {
log.main('OS X platform detected.');
defaultOptions.logFile = path.join(process.env.HOME, 'Library', 'Logs', 'Unity', 'Player.log');
defaultOptions.configFile = path.join(process.env.HOME, 'Library', 'Preferences', 'Blizzard', 'Hearthstone', 'log.config');
defaultOptions.logFileAchievements = path.join('Applications', 'Hearthstone', 'Logs', 'Achievements.log');
}

return defaultOptions;
Expand Down
34 changes: 34 additions & 0 deletions src/file-watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from 'fs';

export default class FileWatcher {
constructor(filePath) {
this.filePath = filePath;
}

start(listener) {
var self = this;
var fileSize = fs.statSync(this.filePath).size;
fs.watchFile(this.filePath, function (current, previous) {
if (current.mtime <= previous.mtime) { return; }

// We're only going to read the portion of the file that we have not read so far.
var newFileSize = fs.statSync(self.filePath).size;
var sizeDiff = newFileSize - fileSize;
if (sizeDiff < 0) {
fileSize = 0;
sizeDiff = newFileSize;
}
var buffer = Buffer.alloc(sizeDiff);
var fileDescriptor = fs.openSync(self.filePath, 'r');
fs.readSync(fileDescriptor, buffer, 0, sizeDiff, fileSize);
fs.closeSync(fileDescriptor);
fileSize = newFileSize;

listener(buffer);
});
}

stop() {
fs.unwatchFile(this.filePath);
}
}
31 changes: 31 additions & 0 deletions src/handle-card-gained.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default function (line, parserState, emit, log) {
var cardRegex = /D (.*) NotifyOfCardGained: \[name=(.*) cardId=(.*) type=(.*)\] (NORMAL|GOLDEN) (.*)/;
var packState = parserState.pack || { cards: [] };

if (cardRegex.test(line)) {
var parts = cardRegex.exec(line);
var cardData = {
cardName: parts[2],
cardId: parts[3],
cardType: parts[4],
golden: parts[5],
qty_owned: parts[6]
};

if (packState.cards.length === 0) {
packState.firstLogTime = parts[1];
}
if (packState.cards.length < 5) {
packState.cards.push(cardData);
}
if (packState.cards.length === 5) {
const finalState = JSON.parse(JSON.stringify(packState));
emit('pack-opened', finalState);
packState.cards = [];
packState.firstLogTime = null;
}
parserState.pack = packState;
}

return parserState;
}
30 changes: 12 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import handleZoneChanges from './handle-zone-changes';
import handleGameOver from './handle-game-over';
import setUpLogger from './set-up-debugger';
import getDefaultOptions from './default-options';
import FileWatcher from './file-watcher';
import handleCardGained from './handle-card-gained';

const log = setUpLogger();

Expand All @@ -22,6 +24,7 @@ export default class extends EventEmitter {

log.main('config file path: %s', this.options.configFile);
log.main('log file path: %s', this.options.logFile);
log.main('achievements log file path: %s', this.options.logFileAchievements);

// Copy local config file to the correct location. Unless already exists.
// Don't want to break other trackers
Expand All @@ -44,28 +47,18 @@ export default class extends EventEmitter {

log.main('Log watcher started.');
// Begin watching the Hearthstone log file.
var fileSize = fs.statSync(self.options.logFile).size;
fs.watchFile(self.options.logFile, function (current, previous) {
if (current.mtime <= previous.mtime) { return; }

// We're only going to read the portion of the file that we have not read so far.
var newFileSize = fs.statSync(self.options.logFile).size;
var sizeDiff = newFileSize - fileSize;
if (sizeDiff < 0) {
fileSize = 0;
sizeDiff = newFileSize;
}
var buffer = new Buffer(sizeDiff);
var fileDescriptor = fs.openSync(self.options.logFile, 'r');
fs.readSync(fileDescriptor, buffer, 0, sizeDiff, fileSize);
fs.closeSync(fileDescriptor);
fileSize = newFileSize;

var logWatcher = new FileWatcher(self.options.logFile);
logWatcher.start(function(buffer) {
self.parseBuffer(buffer, parserState);
});
var achievementsLogWatcher = new FileWatcher(self.options.logFileAchievements);
achievementsLogWatcher.start(function(buffer) {
self.parseBuffer(buffer, parserState);
});

self.stop = function () {
fs.unwatchFile(self.options.logFile);
logWatcher.stop();
achievementsLogWatcher.stop();
delete self.stop;
};
}
Expand All @@ -79,6 +72,7 @@ export default class extends EventEmitter {
state.players = newPlayerIds(line, state.players);
state.players = findPlayerName(line, state.players);
state = handleGameOver(line, state, self.emit.bind(self), log);
state = handleCardGained(line, state, self.emit.bind(self), log);

return state;
}
Expand Down
7 changes: 7 additions & 0 deletions src/log.config
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ FilePrinting=false
ConsolePrinting=true
ScreenPrinting=false

[Achievements]
LogLevel=1
FilePrinting=true
ConsolePrinting=true
ScreenPrinting=false
Verbose=true

[Asset]
LogLevel=1
ConsolePrinting=true
Expand Down
4 changes: 4 additions & 0 deletions src/parser-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ export default class {
this.players = [];
this.playerCount = 0;
this.gameOverCount = 0;
this.pack = {
cards: [],
firstLogTime: null
};
}
}
Empty file.
7 changes: 7 additions & 0 deletions test/artifacts/dummy.config
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ FilePrinting=false
ConsolePrinting=true
ScreenPrinting=false

[Achievements]
LogLevel=1
FilePrinting=true
ConsolePrinting=true
ScreenPrinting=false
Verbose=true

[Asset]
LogLevel=1
ConsolePrinting=true
Expand Down
50 changes: 50 additions & 0 deletions test/file-watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import chai from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
const expect = chai.expect;
chai.should();
chai.use(sinonChai);

import FileWatcher from '../src/file-watcher';

import fs from 'fs';
import readline from 'readline';

describe('file-watcher', function () {
let sandbox, log, emit, fileWatcher, logFile;

beforeEach(function () {
sandbox = sinon.sandbox.create();
log = { zoneChange: sandbox.spy(), gameStart: sandbox.spy(), gameOver: sandbox.spy() };
emit = sandbox.spy();

logFile = __dirname + '/artifacts/dummy-achievements.log';
fileWatcher = new FileWatcher(logFile);
});

afterEach(function () {
sandbox.restore();
fs.truncateSync(logFile);
});

describe('start', function () {
it('logs get detected', function (done) {
this.timeout(25000);
fileWatcher.start(function(buffer) {
var newLogs = buffer.toString();
expect(newLogs).to.include('NotifyOfCardGained');
done();
});

var lineReader = readline.createInterface({
input: fs.createReadStream(__dirname + '/fixture/Achievements.log')
});
lineReader.on('line', function (line) {
var fileDescriptor = fs.openSync(logFile, 'a');
fs.writeSync(fileDescriptor, line);
fs.writeSync(fileDescriptor, '\n');
fs.closeSync(fileDescriptor);
});
});
});
});
26 changes: 26 additions & 0 deletions test/fixture/Achievements.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
D 15:07:59.7666234 NetCache.OnProfileNotices(): sending notices to DialogManager::OnNewNotices
D 15:07:59.7696236 NetCache.OnProfileNotices(): sending notices to AdventureProgressMgr::OnNewNotices
D 15:07:59.7696236 NetCache.OnProfileNotices(): sending notices to AchieveManager::OnNewNotices
D 15:07:59.7706235 NetCache.OnProfileNotices(): sending notices to GenericRewardChestNoticeManager::OnNewNotices
D 15:07:59.7706235 NetCache.OnProfileNotices(): sending notices to AccountLicenseMgr::OnNewNotices
D 15:07:59.7716244 NetCache.OnProfileNotices(): sending notices to FixedRewardsMgr::OnNewNotices
D 15:07:59.7776244 NetCache.OnProfileNotices(): sending notices to PopupDisplayManager::OnNewNotices
D 15:07:59.7786245 NetCache.OnProfileNotices(): sending notices to StoreManager::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to GeneralStorePacksPane::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to DialogManager::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to AdventureProgressMgr::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to AchieveManager::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to GenericRewardChestNoticeManager::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to AccountLicenseMgr::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to FixedRewardsMgr::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to PopupDisplayManager::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to StoreManager::OnNewNotices
D 15:07:59.7796235 NetCache.OnProfileNotices(): sending notices to GeneralStorePacksPane::OnNewNotices
D 15:08:07.2131614 PopupDisplayManager: Calling AllAchievesShownListeners callbacks
D 15:08:21.3775598 PopupDisplayManager: adding 0 rewards to load total=0
D 15:08:54.0669559 NotifyOfCardGained: [name=Booty Bay Bookie cardId=TRL_504 type=MINION] NORMAL 1
D 15:08:54.0679811 NotifyOfCardGained: [name=Ticket Scalper cardId=TRL_015 type=MINION] NORMAL 2
D 15:08:54.0709580 NotifyOfCardGained: [name=Dragon Roar cardId=TRL_362 type=SPELL] NORMAL 1
D 15:08:54.0709580 NotifyOfCardGained: [name=Sharkfin Fan cardId=TRL_507 type=MINION] GOLDEN 1
D 15:08:54.0758987 NotifyOfCardGained: [name=Amani War Bear cardId=TRL_550 type=MINION] NORMAL 1
D 15:09:42.8429143 PopupDisplayManager: adding 0 rewards to load total=0
103 changes: 103 additions & 0 deletions test/handle-card-gained.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import chai from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
const expect = chai.expect;
chai.should();
chai.use(sinonChai);

import handleCardGained from '../src/handle-card-gained';

describe('handle-card-gained', function () {
let sandbox, log, emit;

beforeEach(function () {
sandbox = sinon.sandbox.create();
log = { zoneChange: sandbox.spy(), gameStart: sandbox.spy(), gameOver: sandbox.spy() };
emit = sandbox.spy();
});

afterEach(function () {
sandbox.restore();
});

describe('parsing packs', function() {
it('ignores random lines', function() {
const parserState = {};
const logLine = 'D 15:08:21.3775598 PopupDisplayManager: adding 0 rewards to load total=0';
const newState = handleCardGained(logLine, parserState, emit, log);
expect(newState).to.deep.equal(parserState);
expect(emit).not.to.have.been.called;
});

it('stores first card', function() {
const parserState = {};
const logLine = 'D 15:08:54.0669559 NotifyOfCardGained: [name=Booty Bay Bookie cardId=TRL_504 type=MINION] NORMAL 1';
const newState = handleCardGained(logLine, parserState, emit, log);
expect(newState.pack.cards).to.have.lengthOf(1);
expect(newState.pack.firstLogTime).to.equal('15:08:54.0669559');
expect(emit).not.to.have.been.called;
});

it('stores second card, keeping original timestamp', function() {
const parserState = {};
const logLine1 = 'D 15:08:54.0669559 NotifyOfCardGained: [name=Booty Bay Bookie cardId=TRL_504 type=MINION] NORMAL 1';
const logLine2 = 'D 15:08:54.0679811 NotifyOfCardGained: [name=Ticket Scalper cardId=TRL_015 type=MINION] NORMAL 2';
const newState1 = handleCardGained(logLine1, parserState, emit, log);
const newState2 = handleCardGained(logLine2, newState1, emit, log);
expect(newState2.pack.cards).to.have.lengthOf(2);
expect(newState2.pack.firstLogTime).to.equal('15:08:54.0669559');
expect(emit).not.to.have.been.called;
});

it('detects 5th card, emits event, and clears state', function() {
const parserState = {};
const expectedEventData = {
cards: [{
cardId: "TRL_504",
cardName: "Booty Bay Bookie",
cardType: "MINION",
golden: "NORMAL",
qty_owned: "1"
}, {
cardId: "TRL_015",
cardName: "Ticket Scalper",
cardType: "MINION",
golden: "NORMAL",
qty_owned: "2"
}, {
cardId: "TRL_362",
cardName: "Dragon Roar",
cardType: "SPELL",
golden: "NORMAL",
qty_owned: "1"
}, {
cardId: "TRL_507",
cardName: "Sharkfin Fan",
cardType: "MINION",
golden: "GOLDEN",
qty_owned: "1"
}, {
cardId: "TRL_550",
cardName: "Amani War Bear",
cardType: "MINION",
golden: "NORMAL",
qty_owned: "1"
}],
firstLogTime: "15:08:54.0669559"
};
const logLine1 = 'D 15:08:54.0669559 NotifyOfCardGained: [name=Booty Bay Bookie cardId=TRL_504 type=MINION] NORMAL 1';
const logLine2 = 'D 15:08:54.0679811 NotifyOfCardGained: [name=Ticket Scalper cardId=TRL_015 type=MINION] NORMAL 2';
const logLine3 = 'D 15:08:54.0709580 NotifyOfCardGained: [name=Dragon Roar cardId=TRL_362 type=SPELL] NORMAL 1';
const logLine4 = 'D 15:08:54.0709580 NotifyOfCardGained: [name=Sharkfin Fan cardId=TRL_507 type=MINION] GOLDEN 1';
const logLine5 = 'D 15:08:54.0758987 NotifyOfCardGained: [name=Amani War Bear cardId=TRL_550 type=MINION] NORMAL 1';
const newState1 = handleCardGained(logLine1, parserState, emit, log);
const newState2 = handleCardGained(logLine2, newState1, emit, log);
const newState3 = handleCardGained(logLine3, newState2, emit, log);
const newState4 = handleCardGained(logLine4, newState3, emit, log);
const newState5 = handleCardGained(logLine5, newState4, emit, log);
expect(newState5.pack.cards).to.have.lengthOf(0);
expect(newState5.pack.firstLogTime).to.be.null;
expect(emit).to.have.been.calledWith('pack-opened', expectedEventData);
});
});
});
Loading