// ==UserScript==
// @name agar-mini-map
// @namespace http://github.com/dimotsai/
// @version 0.46
// @description This script will show a mini map and your location on agar.io
// @author dimotsai
// @license MIT
// @match http://agar.io/*
// @require http://cdn.jsdelivr.net/msgpack/1.05/msgpack.js
// @grant none
// @run-at document-body
// ==/UserScript==
window.msgpack = this.msgpack;
(function() {
var _WebSocket = window._WebSocket = window.WebSocket;
var $ = window.jQuery;
var msgpack = window.msgpack;
var options = {
enableMultiCells: true,
enablePosition: true,
enableCross: true
};
// game states
var agar_server = null;
var map_server = null;
var player_name = [];
var players = [];
var id_players = [];
var cells = [];
var current_cell_ids = [];
var start_x = -7000,
start_y = -7000,
end_x = 7000,
end_y = 7000,
length_x = 14000,
length_y = 14000;
var render_timer = null;
function miniMapSendRawData(data) {
if (map_server !== null && map_server.readyState === window._WebSocket.OPEN) {
var array = new Uint8Array(data);
map_server.send(array.buffer);
}
}
function miniMapConnectToServer(address, onOpen, onClose) {
try {
var ws = new window._WebSocket(address);
} catch (ex) {
onClose();
console.error(ex);
return false;
}
ws.binaryType = "arraybuffer";
ws.onopen = function() {
onOpen();
console.log(address + ' connected');
}
ws.onmessage = function(event) {
var buffer = new Uint8Array(event.data);
var packet = msgpack.unpack(buffer);
switch(packet.type) {
case 128:
for (var i=0; i < packet.data.addition.length; ++i) {
var cell = packet.data.addition[i];
if (! miniMapIsRegisteredToken(cell.id))
{
miniMapRegisterToken(
cell.id,
miniMapCreateToken(cell.id, cell.color)
);
}
var size_n = cell.size/length_x;
miniMapUpdateToken(cell.id, (cell.x - start_x)/length_x, (cell.y - start_y)/length_y, size_n);
}
for (var i=0; i < packet.data.deletion.length; ++i) {
var id = packet.data.deletion[i];
miniMapUnregisterToken(id);
}
break;
case 129:
players = packet.data;
for (var p in players) {
var player = players[p];
var ids = player.ids;
for (var i in ids) {
id_players[ids[i]] = player.no;
}
}
mini_map_party.trigger('update-list');
break;
case 130:
if (agar_server != packet.data.url) {
var region_name = $('#region > option[value="' + packet.data.region + '"]').text();
var gamemode_name = $('#gamemode > option[value="' + packet.data.gamemode + '"]').text();
var title = 'Agar Server Mismatched';
var content = ('You are now at: <strong>' + agar_server
+ '</strong><br>Your team members are all at: <strong>' + packet.data.url + ', ' + region_name + ':' + gamemode_name + packet.data.party
+ '</strong>.<br>The minimap server has disconnected automatically.');
$('#mini-map-connect-btn').popover('destroy').popover({
animation: false,
placement: 'top',
title: title,
content: content,
container: document.body,
html: true
}).popover('show');
} else {
$('#mini-map-content-btn').popover('hide');
}
break;
}
}
ws.onerror = function() {
onClose();
console.error('failed to connect to map server');
}
ws.onclose = function() {
onClose();
map_server = null;
console.log('map server disconnected');
}
map_server = ws;
}
function miniMapRender() {
var canvas = window.mini_map;
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var id in window.mini_map_tokens) {
var token = window.mini_map_tokens[id];
var x = token.x * canvas.width;
var y = token.y * canvas.height;
var size = token.size * canvas.width;
ctx.beginPath();
ctx.arc(
x,
y,
size,
0,
2 * Math.PI,
false
);
ctx.closePath();
ctx.fillStyle = token.color;
ctx.fill();
if (options.enableCross && -1 != current_cell_ids.indexOf(token.id))
miniMapDrawCross(token.x, token.y);
if (id_players[id] !== undefined) {
ctx.font = size * 2 + 'px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'white';
ctx.fillText(id_players[id] + 1, x, y);
}
};
}
function miniMapDrawCross(x, y) {
var canvas = window.mini_map;
var ctx = canvas.getContext('2d');
ctx.lineWidth = 0.5;
ctx.beginPath();
ctx.moveTo(0, y * canvas.height);
ctx.lineTo(canvas.width, y * canvas.height);
ctx.moveTo(x * canvas.width, 0);
ctx.lineTo(x * canvas.width, canvas.height);
ctx.closePath();
ctx.strokeStyle = '#FFFFFF';
ctx.stroke();
}
function miniMapCreateToken(id, color) {
var mini_map_token = {
id: id,
color: color,
x: 0,
y: 0,
size: 0
};
return mini_map_token;
}
function miniMapRegisterToken(id, token) {
if (window.mini_map_tokens[id] === undefined) {
// window.mini_map.append(token);
window.mini_map_tokens[id] = token;
}
}
function miniMapUnregisterToken(id) {
if (window.mini_map_tokens[id] !== undefined) {
// window.mini_map_tokens[id].detach();
delete window.mini_map_tokens[id];
}
}
function miniMapIsRegisteredToken(id) {
return window.mini_map_tokens[id] !== undefined;
}
function miniMapUpdateToken(id, x, y, size) {
if (window.mini_map_tokens[id] !== undefined) {
window.mini_map_tokens[id].x = x;
window.mini_map_tokens[id].y = y;
window.mini_map_tokens[id].size = size;
return true;
} else {
return false;
}
}
function miniMapUpdatePos(x, y) {
window.mini_map_pos.text('x: ' + x.toFixed(0) + ', y: ' + y.toFixed(0));
}
function miniMapReset() {
cells = [];
window.mini_map_tokens = [];
}
function miniMapInit() {
window.mini_map_tokens = [];
cells = [];
current_cell_ids = [];
start_x = -7000;
start_y = -7000;
end_x = 7000;
end_y = 7000;
length_x = 14000;
length_y = 14000;
// minimap dom
if ($('#mini-map-wrapper').length === 0) {
var wrapper = $('<div>').attr('id', 'mini-map-wrapper').css({
position: 'fixed',
bottom: 10,
right: 10,
width: 300,
height: 300,
background: 'rgba(128, 128, 128, 0.58)'
});
var mini_map = $('<canvas>').attr({
id: 'mini-map',
width: 300,
height: 300
}).css({
width: '100%',
height: '100%',
position: 'relative'
});
wrapper.append(mini_map).appendTo(document.body);
window.mini_map = mini_map[0];
}
// minimap renderer
if (render_timer === null)
render_timer = setInterval(miniMapRender, 1000 / 30);
// minimap location
if ($('#mini-map-pos').length === 0) {
window.mini_map_pos = $('<div>').attr('id', 'mini-map-pos').css({
bottom: 10,
right: 10,
color: 'white',
fontSize: 15,
fontWeight: 800,
position: 'fixed'
}).appendTo(document.body);
}
// minimap options
if ($('#mini-map-options').length === 0) {
window.mini_map_options = $('<div>').attr('id', 'mini-map-options').css({
bottom: 315,
right: 10,
color: '#666',
fontSize: 14,
position: 'fixed',
fontWeight: 400,
zIndex: 1000
}).appendTo(document.body);
var container = $('<div>')
.css({
background: 'rgba(200, 200, 200, 0.58)',
padding: 5,
borderRadius: 5
})
.hide();
for (var name in options) {
var label = $('<label>').css({
display: 'block'
});
var checkbox = $('<input>').attr({
type: 'checkbox'
}).prop({
checked: options[name]
});
label.append(checkbox);
label.append(' ' + camel2cap(name));
checkbox.click(function(options, name) { return function(evt) {
options[name] = evt.target.checked;
console.log(name, evt.target.checked);
}}(options, name));
label.appendTo(container);
}
container.appendTo(window.mini_map_options);
var form = $('<div>')
.addClass('form-inline')
.css({
opacity: 0.7,
marginTop: 2
})
.appendTo(window.mini_map_options);
var form_group = $('<div>')
.addClass('form-group')
.appendTo(form);
var setting_btn = $('<button>')
.addClass('btn')
.css({
float: 'right',
fontWeight: 800,
marginLeft: 2
})
.on('click', function() {
container.toggle();
setting_btn.blur();
return false;
})
.append($('<i>').addClass('glyphicon glyphicon-cog'))
.appendTo(form_group);
var help_btn = $('<button>')
.addClass('btn')
.text('?')
.on('click', function(e) {
window.open('https://github.com/dimotsai/agar-mini-map/#minimap-server');
help_btn.blur();
return false;
})
.appendTo(form_group);
var addressInput = $('<input>')
.css({
marginLeft: 2
})
.attr('placeholder', 'ws://127.0.0.1:34343')
.attr('type', 'text')
.addClass('form-control')
.val('ws://127.0.0.1:34343')
.appendTo(form_group);
var connect = function (evt) {
var address = addressInput.val();
connectBtn.popover('destroy');
connectBtn.text('Disconnect');
miniMapConnectToServer(address, function onOpen() {
miniMapSendRawData(msgpack.pack({
type: 0,
data: player_name
}));
for (var i in current_cell_ids) {
miniMapSendRawData(msgpack.pack({
type: 32,
data: current_cell_ids[i]
}));
}
miniMapSendRawData(msgpack.pack({
type: 100,
data: {url: agar_server, region: $('#region').val(), gamemode: $('#gamemode').val(), party: location.hash}
}));
window.mini_map_party.show();
}, function onClose() {
players = [];
id_players = [];
window.mini_map_party.hide();
disconnect();
});
connectBtn.off('click');
connectBtn.on('click', disconnect);
miniMapReset();
connectBtn.blur();
};
var disconnect = function() {
connectBtn.text('Connect');
connectBtn.off('click');
connectBtn.on('click', connect);
connectBtn.blur();
if (map_server)
map_server.close();
miniMapReset();
};
var connectBtn = $('<button>')
.attr('id', 'mini-map-connect-btn')
.css({
marginLeft: 2
})
.text('Connect')
.click(connect)
.addClass('btn')
.appendTo(form_group);
}
// minimap party
if ($('#mini-map-party').length === 0) {
var mini_map_party = window.mini_map_party = $('<div>')
.css({
top: 50,
left: 10,
width: 200,
color: '#FFF',
fontSize: 20,
position: 'fixed',
fontWeight: 600,
background: 'rgba(128, 128, 128, 0.58)',
textAlign: 'center',
padding: 10
})
.attr('id', 'mini-map-party')
.appendTo(window.document.body)
.append(
$('<h3>').css({
margin: 0,
padding: 0
}).text('Party')
);
var mini_map_party_list = $('<ol>')
.attr('id', 'mini-map-party-list')
.css({
listStyle: 'none',
padding: 0,
margin: 0
})
.appendTo(mini_map_party);
mini_map_party.on('update-list', function(e) {
mini_map_party_list.empty();
for (var p in players) {
var player = players[p];
var name = String.fromCharCode.apply(null, player.name);
name = (name == '' ? 'anonymous' : name);
$('<li>')
.text(player.no + 1 + '. ' + name)
.appendTo(mini_map_party_list);
}
});
mini_map_party.hide();
}
}
// cell constructor
function Cell(id, x, y, size, color, name) {
cells[id] = this;
this.id = id;
this.ox = this.x = x;
this.oy = this.y = y;
this.oSize = this.size = size;
this.color = color;
this.points = [];
this.pointsAcc = [];
this.setName(name);
}
Cell.prototype = {
id: 0,
points: null,
pointsAcc: null,
name: null,
nameCache: null,
sizeCache: null,
x: 0,
y: 0,
size: 0,
ox: 0,
oy: 0,
oSize: 0,
nx: 0,
ny: 0,
nSize: 0,
updateTime: 0,
updateCode: 0,
drawTime: 0,
destroyed: false,
isVirus: false,
isAgitated: false,
wasSimpleDrawing: true,
destroy: function() {
delete cells[this.id];
id = current_cell_ids.indexOf(this.id);
-1 != id && current_cell_ids.splice(id, 1);
this.destroyed = true;
if (map_server === null || map_server.readyState !== window._WebSocket.OPEN) {
miniMapUnregisterToken(this.id);
}
},
setName: function(name) {
this.name = name;
},
updatePos: function() {
if (map_server === null || map_server.readyState !== window._WebSocket.OPEN) {
if (options.enableMultiCells || -1 != current_cell_ids.indexOf(this.id)) {
if (! miniMapIsRegisteredToken(this.id))
{
miniMapRegisterToken(
this.id,
miniMapCreateToken(this.id, this.color)
);
}
var size_n = this.nSize/length_x;
miniMapUpdateToken(this.id, (this.nx - start_x)/length_x, (this.ny - start_y)/length_y, size_n);
}
}
if (options.enablePosition && -1 != current_cell_ids.indexOf(this.id)) {
window.mini_map_pos.show();
miniMapUpdatePos(this.nx, this.ny);
} else {
window.mini_map_pos.hide();
}
}
};
String.prototype.capitalize = function() {
return this.charAt(0).toUpperCase() + this.slice(1);
};
function camel2cap(str) {
return str.replace(/([A-Z])/g, function(s){return ' ' + s.toLowerCase();}).capitalize();
};
// create a linked property from slave object
// whenever master[prop] update, slave[prop] update
function refer(master, slave, prop) {
Object.defineProperty(master, prop, {
get: function(){
return slave[prop];
},
set: function(val) {
slave[prop] = val;
},
enumerable: true,
configurable: true
});
};
// extract a websocket packet which contains the information of cells
function extractCellPacket(data, offset) {
////
var dataToSend = {
destroyQueue : [],
nodes : [],
nonVisibleNodes : []
};
////
var I = +new Date;
var qa = false;
var b = Math.random(), c = offset;
var size = data.getUint16(c, true);
c = c + 2;
// Nodes to be destroyed (killed)
for (var e = 0; e < size; ++e) {
var p = cells[data.getUint32(c, true)],
f = cells[data.getUint32(c + 4, true)],
c = c + 8;
p && f && (
f.destroy(),
f.ox = f.x,
f.oy = f.y,
f.oSize = f.size,
f.nx = p.x,
f.ny = p.y,
f.nSize = f.size,
f.updateTime = I,
dataToSend.destroyQueue.push(f.id));
}
// Nodes to be updated
for (e = 0; ; ) {
var d = data.getUint32(c, true);
c += 4;
if (0 == d) {
break;
}
++e;
var p = data.getInt32(c, true),
c = c + 4,
f = data.getInt32(c, true),
c = c + 4;
g = data.getInt16(c, true);
c = c + 2;
for (var h = data.getUint8(c++), m = data.getUint8(c++), q = data.getUint8(c++), h = (h << 16 | m << 8 | q).toString(16); 6 > h.length; )
h = "0" + h;
var h = "#" + h,
k = data.getUint8(c++),
m = !!(k & 1),
q = !!(k & 16);
k & 2 && (c += 4);
k & 4 && (c += 8);
k & 8 && (c += 16);
for (var n, k = ""; ; ) {
n = data.getUint16(c, true);
c += 2;
if (0 == n)
break;
k += String.fromCharCode(n)
}
n = k;
k = null;
var updated = false;
// if d in cells then modify it, otherwise create a new cell
cells.hasOwnProperty(d)
? (k = cells[d],
k.updatePos(),
k.ox = k.x,
k.oy = k.y,
k.oSize = k.size,
k.color = h,
updated = true)
: (k = new Cell(d, p, f, g, h, n),
k.pX = p,
k.pY = f);
k.isVirus = m;
k.isAgitated = q;
k.nx = p;
k.ny = f;
k.nSize = g;
k.updateCode = b;
k.updateTime = I;
n && k.setName(n);
// ignore food creation
if (updated) {
dataToSend.nodes.push({
id: k.id,
x: k.nx,
y: k.ny,
size: k.nSize,
color: k.color
});
}
}
// Destroy queue + nonvisible nodes
b = data.getUint32(c, true);
c += 4;
for (e = 0; e < b; e++) {
d = data.getUint32(c, true);
c += 4, k = cells[d];
null != k && k.destroy();
dataToSend.nonVisibleNodes.push(d);
}
var packet = {
type: 16,
data: dataToSend
}
miniMapSendRawData(msgpack.pack(packet));
}
// extract the type of packet and dispatch it to a corresponding extractor
function extractPacket(event) {
var c = 0;
var data = new DataView(event.data);
240 == data.getUint8(c) && (c += 5);
var opcode = data.getUint8(c);
c++;
switch (opcode) {
case 16: // cells data
extractCellPacket(data, c);
break;
case 20: // cleanup ids
current_cell_ids = [];
break;
case 32: // cell id belongs me
var id = data.getUint32(c, true);
if (current_cell_ids.indexOf(id) === -1)
current_cell_ids.push(id);
miniMapSendRawData(msgpack.pack({
type: 32,
data: id
}));
break;
case 64: // get borders
start_x = data.getFloat64(c, !0), c += 8,
start_y = data.getFloat64(c, !0), c += 8,
end_x = data.getFloat64(c, !0), c += 8,
end_y = data.getFloat64(c, !0), c += 8,
center_x = (start_x + end_x) / 2,
center_y = (start_y + end_y) / 2,
length_x = Math.abs(start_x - end_x),
length_y = Math.abs(start_y - end_y);
}
};
function extractSendPacket(data) {
var view = new DataView(data);
switch (view.getUint8(0, true)) {
case 0:
player_name = [];
for (var i=1; i < data.byteLength; i+=2) {
player_name.push(view.getUint16(i, true));
}
miniMapSendRawData(msgpack.pack({
type: 0,
data: player_name
}));
break;
}
}
// the injected point, overwriting the WebSocket constructor
window.WebSocket = function(url, protocols) {
console.log('Listen');
if (protocols === undefined) {
protocols = [];
}
var ws = new _WebSocket(url, protocols);
refer(this, ws, 'binaryType');
refer(this, ws, 'bufferedAmount');
refer(this, ws, 'extensions');
refer(this, ws, 'protocol');
refer(this, ws, 'readyState');
refer(this, ws, 'url');
this.send = function(data){
extractSendPacket(data);
return ws.send.call(ws, data);
};
this.close = function(){
return ws.close.call(ws);
};
this.onopen = function(event){};
this.onclose = function(event){};
this.onerror = function(event){};
this.onmessage = function(event){};
ws.onopen = function(event) {
miniMapInit();
agar_server = url;
miniMapSendRawData(msgpack.pack({
type: 100,
data: {url: url, region: $('#region').val(), gamemode: $('#gamemode').val(), party: location.hash}
}));
if (this.onopen)
return this.onopen.call(ws, event);
}.bind(this);
ws.onmessage = function(event) {
extractPacket(event);
if (this.onmessage)
return this.onmessage.call(ws, event);
}.bind(this);
ws.onclose = function(event) {
if (this.onclose)
return this.onclose.call(ws, event);
}.bind(this);
ws.onerror = function(event) {
if (this.onerror)
return this.onerror.call(ws, event);
}.bind(this);
};
window.WebSocket.prototype = _WebSocket;
$(window.document).ready(function() {
miniMapInit();
});
$(window).load(function() {
var main_canvas = document.getElementById('canvas');
if (main_canvas && main_canvas.onmousemove) {
document.onmousemove = main_canvas.onmousemove;
main_canvas.onmousemove = null;
}
});
})();