Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Emit events for pack openings #39

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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