In this talk we will delve into one of the most played games of the moment: Blizzard Interactive's Hearthstone. We will focus on analysing Hearthstone's implementation of Google Protocol Buffers and proceed to show how the game handles communication by providing some code examples and by showing how to create a Deck Tracking application.
18. Packets used during a Hearthstone match
PowerHistoryEntity
message PowerHistoryEntity {
required int32 entity = 1;
required string name = 2;
repeated Tag tags = 3;
}
19. Packets used during a Hearthstone match
EntityChoices
message EntityChoices {
enum PacketID {
ID = 17;
}
required int32 id = 1;
required int32 choice_type = 2;
required int32 count_min = 4;
required int32 count_max = 5;
repeated int32 entities = 6 [packed = true];
optional int32 source = 7;
required int32 player_id = 8;
}
29. // Modules
var ProtoBuf = require('protobufjs');
var PegasusPacket = require('./pegasuspacket').PegasusPacket;
var net = require('net');
var client = new net.Socket();
var builder = ProtoBuf.loadProtoFile(__dirname + '/proto/game.proto');
var PegasusGame = builder.build();
var serverIp = "127.0.0.1"
var handshake = "qAAAAFkAAAAIpoKkChIGY0RnZ2xvGLmgmQIgggIqBjI4OTYwNjo6CAIQBBoOTWFjQm9va1BybzExLDUqJEJBQTVGNzV
handshake.game_handle = 1;
// Crafting ping packet
var ping = new PegasusGame.PegasusGame.Ping();
var encoded_ping = PegasusGame.PegasusGame.Ping.encode(ping);
var type = PegasusGame.PegasusGame.Ping.PacketID.ID;
var pegasus_ping = new PegasusPacket();
var encoded_pegasus_ping = pegasus_ping.Encode(encoded_ping, type);
// Craft USERUI packet
var userui = new PegasusGame.PegasusGame.UserUI();
userui.mouse_info = null;
userui.emote = 4;
userui.player_id = null;
var encoded_userui = PegasusGame.PegasusGame.UserUI.encode(userui);
var type_userui = PegasusGame.PegasusGame.UserUI.PacketID.ID;
var pegasus_userui = new PegasusPacket();
var encoded_pegasus_userui = pegasus_ping.Encode(encoded_userui, type_userui);
client.connect(3724, serverIp, function() {
client.write(Buffer.from(args.options.handshake, 'base64'));
setInterval(function() {
console.log("[+] ping sent");
client.write(encoded_pegasus_ping);
}, 5000);
setInterval(function() {
client.write(encoded_pegasus_userui);
}, 1000);
30. net.createServer(function(sock) {
console.log('[+] connection from: ' + sock.remoteAddress +':'+ sock.remotePort);
sock.on('data', function(data) {
var pegasuspacket = new PegasusPacket();
var bytes_decoded = pegasuspacket.Decode(data, 0, data.length);
if(bytes_decoded >= 4) {
var decoded = Hearthnode.Decode(pegasuspacket);
if(decoded != null && decoded != "unimplemented") {
// Handling
console.log(pegasuspacket.Type);
switch(pegasuspacket.Type) {
// Handshake
case 168:
console.log("[i] Received handshake from client");
console.log(decoded);
// Reply with GameSetup
var GameSetup = new PegasusGame.PegasusGame.GameSetup();
GameSetup.board = 6;
GameSetup.max_secrets_per_player = 5;
GameSetup.max_friendly_minions_per_player = 7;
GameSetup.keep_alive_frequency_seconds = 5;
GameSetup.disconnect_when_stuck_seconds = 25;
var encoded_GameSetup = PegasusGame.PegasusGame.GameSetup.encode(GameSetup);
var type = PegasusGame.PegasusGame.GameSetup.PacketID.ID;
var pegasus_GameSetup = new PegasusPacket();
var encoded_pegasus_GameSetup = pegasus_GameSetup.Encode(encoded_GameSetup, type);
sock.write(encoded_pegasus_GameSetup);
break;
case 115:
// Crafting ping packet
console.log("[i] Received ping from client");
console.log(decoded)
var Pong = new PegasusGame.PegasusGame.Pong();
var encoded_Pong = PegasusGame.PegasusGame.Pong.encode(Pong);
var type = PegasusGame.PegasusGame.Pong.PacketID.ID;
var pegasus_Pong = new PegasusPacket();
var encoded_pegasus_Pong = pegasus_ping.Encode(encoded_Pong, type);
sock.write(encoded_pegasus_Pong);
break;
case 15:
// Received UserUI
console.log("[i] Received UserUI");
console.log(decoded);