APMU

Standard for hooking into the client -> server connection in arras.io

Αυτός ο κώδικας δεν πρέπει να εγκατασταθεί άμεσα. Είναι μια βιβλιοθήκη για άλλους κώδικες που περιλαμβάνεται μέσω της οδηγίας meta // @require https://update.greasyfork.org/scripts/483575/1304377/APMU.js

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

/* ==UserScript==
// @name         APMU
// @version      1.2.1
// @author       ABC & Ray Adams
// @namespace    https://github.com/ABCxFF
// @description  Standard for hooking into the client -> server connection in arras.io
// @match        *://arras.io/*
// @match        *://arras.netlify.app/*
// @homepageURL  https://github.com/Ray-Adams/Arras-Archive
// @grant        none
// @run-at       document-start
// @license      GPL-3.0
*/

/****************************************************
 * 
 * Copyright (C) 2021  ABC & Ray Adams
 * Licensed under GNU General Public License v3.0
 * 
 ***************************************************/

const arras = (() => {

	// API

	const gamemodeTable = [
		[{
			id: 'x',
			u: 'Private'
		}],
		[{
			id: 'e',
			Hb: 'word'
		}],
		[{
			id: 'w',
			Hb: 'words'
		}],
		[{
			id: 'p',
			u: 'Portal'
		}],
		[{
			id: 'o',
			u: 'Open'
		}],
		[{
			id: 'm',
			u: 'Maze',
			delay: !0,
			remove: 'f'
		}],
		[{
			id: 'f',
			u: 'FFA'
		},
		{
			id: 'd',
			u: 'Duos'
		},
		{
			id: 's',
			u: 'Squads'
		},
		{
			id: '1',
			u: '1 Team',
			advance: !0
		},
		{
			id: '2',
			u: '2 Team',
			advance: !0,
			end: '2TDM'
		},
		{
			id: '3',
			u: '3 Team',
			advance: !0,
			end: '3TDM'
		},
		{
			id: '4',
			u: '4 Team',
			advance: !0,
			end: '4TDM'
		}
		],
		[{
			id: 'd',
			u: 'Domination'
		},
		{
			id: 'm',
			u: 'Mothership',
			remove: '2'
		},
		{
			id: 'a',
			u: 'Assault',
			remove: ['2', 'm']
		},
		{
			id: 's',
			u: 'Siege',
			remove: '1'
		},
		{
			id: 't',
			u: 'Tag',
			remove: ['o', '4']
		},
		{
			id: 'p',
			u: 'Pandemic',
			remove: ['o', '2']
		},
		{
			id: 'z',
			u: 'Sandbox'
		}
		]
	];

	const regionTable = {
		xyz: ['Local', 'Localhost', null],
		unk: ['Unknown', 'Unknown', null],
		svx: ['US West', 'Silicon Valley, CA, US', -7],
		lax: ['US West', 'Los Angeles, CA, US', -7],
		dal: ['USA', 'Dallas, TX, US', -5],
		kci: ['USA', 'Kansas City, MO, US', -5],
		vin: ['US East', 'Vint Hill, VA, US', -4],
		mtl: ['US East', 'Montreal, CA', -4],
		lon: ['Europe', 'London, UK', 1],
		fra: ['Europe', 'Frankfurt, DE', 2],
		sgp: ['Asia', 'Singapore', 8]
	};

	const hostTable = {
		z: ['Private', null],
		x: ['Local', null],
		glitch: ['Glitch', 10],
		vultr: ['Vultr', 30],
		buyvm: ['BuyVM', 15],
		extravm: ['ExtraVM', 40],
		ovh: ['OVH', 45],
		wsi: ['WSI', 50]
	};

	class Server {
		static parseGamemode(code) {
			if ('%' === code) return 'Unknown';
			let tags = [];
			let filter = [];
			let at = 0;

			for (const games of gamemodeTable) {
				for (const game of games) {
					if (game.id === code.charAt(at)) {
						if (Array.isArray(game.remove)) {
							filter.push.apply(filter, game.remove);
						} else if (game.remove) {
							filter.push(game.remove);
						}
						tags.push(Object.assign({}, game));
						at++;
						break;
					}
				}
			}
			if (tags.length == 0) return 'Unknown';

			return tags.map((n, i, l) => l[Math.min(i + Math.pow(-1, i), l.length - 1)]).filter(({ id }) => !filter.includes(id)).map(data => data.u).join(' ');
		}
		static parseRegion(code) {
			return regionTable[code][0];
		}
		static parseHost(code) {
			return hostTable[code][0];
		}
		static parseCode(code) {
			const [host, region, gamemode] = code.split('-');

			return [Server.parseHost(host), Server.parseRegion(region), Server.parseGamemode(gamemode)].join(' - ');
		}
	}

	// PROTOCOL

	const u32 = new Uint32Array(1);
	const u16 = new Uint16Array(1);
	const c16 = new Uint8Array(u16.buffer);
	const c32 = new Uint8Array(u32.buffer);
	const f32 = new Float32Array(u32.buffer);

	Array.prototype.remove = function (index) {
		if (index === this.length - 1) return this.pop();
		this[index] = this.pop();
	};

	function encode(message) {
		let headers = [];
		let headerCodes = [];
		let contentSize = 0;
		let lastTypeCode = 0b1111;
		let repeatTypeCount = 0;
		for (let block of message) {
			let typeCode = 0;
			if (block === 0 || block === false) {
				typeCode = 0b0000;
			} else if (block === 1 || block === true) {
				typeCode = 0b0001;
			} else if (typeof block === 'number') {
				if (!Number.isInteger(block) || block < -0x100000000 || block >= 0x100000000) {
					typeCode = 0b1000;
					contentSize += 4;
				} else if (block >= 0) {
					if (block < 0x100) {
						typeCode = 0b0010;
						contentSize++;
					} else if (block < 0x10000) {
						typeCode = 0b0100;
						contentSize += 2;
					} else if (block < 0x100000000) {
						typeCode = 0b0110;
						contentSize += 4;
					}
				} else {
					if (block >= -0x100) {
						typeCode = 0b0011;
						contentSize++;
					} else if (block >= -0x10000) {
						typeCode = 0b0101;
						contentSize += 2;
					} else if (block >= -0x100000000) {
						typeCode = 0b0111;
						contentSize += 4;
					}
				}
			} else if (typeof block === 'string') {
				let hasUnicode = false;
				for (let i = 0; i < block.length; i++) {
					if (block.charAt(i) > '\xff') {
						hasUnicode = true;
					} else if (block.charAt(i) === '\x00') {
						console.error('Null containing string', block);
						throw new Error('Null containing string');
					}
				}
				if (!hasUnicode && block.length <= 1) {
					typeCode = 0b1001;
					contentSize++;
				} else if (hasUnicode) {
					typeCode = 0b1011;
					contentSize += block.length * 2 + 2;
				} else {
					typeCode = 0b1010;
					contentSize += block.length + 1;
				}
			} else {
				console.error('Unencodable data type', block);
				throw new Error('Unencodable data type');
			}
			headers.push(typeCode);
			if (typeCode === lastTypeCode) {
				repeatTypeCount++;
			} else {
				headerCodes.push(lastTypeCode);
				if (repeatTypeCount >= 1) {
					while (repeatTypeCount > 19) {
						headerCodes.push(0b1110);
						headerCodes.push(15);
						repeatTypeCount -= 19;
					}
					if (repeatTypeCount === 1)
						headerCodes.push(lastTypeCode);
					else if (repeatTypeCount === 2)
						headerCodes.push(0b1100);
					else if (repeatTypeCount === 3)
						headerCodes.push(0b1101);
					else if (repeatTypeCount < 20) {
						headerCodes.push(0b1110);
						headerCodes.push(repeatTypeCount - 4);
					}
				}
				repeatTypeCount = 0;
				lastTypeCode = typeCode;
			}
		}
		headerCodes.push(lastTypeCode);
		if (repeatTypeCount >= 1) {
			while (repeatTypeCount > 19) {
				headerCodes.push(0b1110);
				headerCodes.push(15);
				repeatTypeCount -= 19;
			}
			if (repeatTypeCount === 1)
				headerCodes.push(lastTypeCode);
			else if (repeatTypeCount === 2)
				headerCodes.push(0b1100);
			else if (repeatTypeCount === 3)
				headerCodes.push(0b1101);
			else if (repeatTypeCount < 20) {
				headerCodes.push(0b1110);
				headerCodes.push(repeatTypeCount - 4);
			}
		}
		headerCodes.push(0b1111);
		if (headerCodes.length % 2 === 1)
			headerCodes.push(0b1111);

		let output = new Uint8Array((headerCodes.length >> 1) + contentSize);
		for (let i = 0; i < headerCodes.length; i += 2) {
			let upper = headerCodes[i];
			let lower = headerCodes[i + 1];
			output[i >> 1] = (upper << 4) | lower;
		}
		let index = headerCodes.length >> 1;
		for (let i = 0; i < headers.length; i++) {
			let block = message[i];
			switch (headers[i]) {
			case 0b0000:
			case 0b0001:
				break;
			case 0b0010:
			case 0b0011:
				output[index++] = block;
				break;
			case 0b0100:
			case 0b0101:
				u16[0] = block;
				output.set(c16, index);
				index += 2;
				break;
			case 0b0110:
			case 0b0111:
				u32[0] = block;
				output.set(c32, index);
				index += 4;
				break;
			case 0b1000:
				f32[0] = block;
				output.set(c32, index);
				index += 4;
				break;
			case 0b1001:
				{
					let byte = block.length === 0 ? 0 : block.charCodeAt(0);
					output[index++] = byte;
				}
				break;
			case 0b1010:
				for (let i = 0; i < block.length; i++) {
					output[index++] = block.charCodeAt(i);
				}
				output[index++] = 0;
				break;
			case 0b1011:
				for (let i = 0; i < block.length; i++) {
					let charCode = block.charCodeAt(i);
					output[index++] = charCode & 0xff;
					output[index++] = charCode >> 8;
				}
				output[index++] = 0;
				output[index++] = 0;
				break;
			}
		}

		return output;
	}

	function decode(packet) {
		let data = new Uint8Array(packet);
		if (data[0] >> 4 !== 0b1111)
			return null;

		let headers = [];
		let lastTypeCode = 0b1111;
		let index = 0;
		let consumedHalf = true;
		while (true) {
			if (index >= data.length)
				return null;
			let typeCode = data[index];

			if (consumedHalf) {
				typeCode &= 0b1111;
				index++;
			} else {
				typeCode >>= 4;
			}
			consumedHalf = !consumedHalf;

			if ((typeCode & 0b1100) === 0b1100) {
				if (typeCode === 0b1111) {
					if (consumedHalf)
						index++;
					break;
				}

				let repeat = typeCode - 10; // 0b1100 - 2
				if (typeCode === 0b1110) {
					if (index >= data.length)
						return null;
					let repeatCode = data[index];

					if (consumedHalf) {
						repeatCode &= 0b1111;
						index++;
					} else {
						repeatCode >>= 4;
					}
					consumedHalf = !consumedHalf;

					repeat += repeatCode;
				}

				for (let i = 0; i < repeat; i++)
					headers.push(lastTypeCode);
			} else {
				headers.push(typeCode);
				lastTypeCode = typeCode;
			}
		}

		let output = [];
		for (let header of headers) {
			switch (header) {
			case 0b0000:
				output.push(0);
				break;
			case 0b0001:
				output.push(1);
				break;
			case 0b0010:
				output.push(data[index++]);
				break;
			case 0b0011:
				output.push(data[index++] - 0x100);
				break;
			case 0b0100:
				c16[0] = data[index++];
				c16[1] = data[index++];
				output.push(u16[0]);
				break;
			case 0b0101:
				c16[0] = data[index++];
				c16[1] = data[index++];
				output.push(u16[0] - 0x10000);
				break;
			case 0b0110:
				c32[0] = data[index++];
				c32[1] = data[index++];
				c32[2] = data[index++];
				c32[3] = data[index++];
				output.push(u32[0]);
				break;
			case 0b0111:
				c32[0] = data[index++];
				c32[1] = data[index++];
				c32[2] = data[index++];
				c32[3] = data[index++];
				output.push(u32[0] - 0x100000000);
				break;
			case 0b1000:
				c32[0] = data[index++];
				c32[1] = data[index++];
				c32[2] = data[index++];
				c32[3] = data[index++];
				output.push(f32[0]);
				break;
			case 0b1001:
				{
					let byte = data[index++];
					output.push(byte === 0 ? '' : String.fromCharCode(byte));
				}
				break;
			case 0b1010:
				{
					let string = '';
					let byte = 0;
					while (byte = data[index++]) {
						string += String.fromCharCode(byte);
					}
					output.push(string);
				}
				break;
			case 0b1011:
				{
					let string = '';
					let byte = 0;
					while (byte = data[index++] | (data[index++] << 8)) {
						string += String.fromCharCode(byte);
					}
					output.push(string);
				}
				break;
			}
		}

		return output;
	}

	function rotator(packet) {
		return {
			i: 0,
			arr: packet,
			get(index) {
				return packet[index];
			},
			set(index, value) {
				return (packet[index] = value);
			},
			nex() {
				if (this.i === this.arr.length) {
					console.error(new Error('End reached'), this.arr);
					return -1;
				}
				return packet[this.i++];
			}
		};
	}

	class BroadcastParser {
		constructor() {
			this.leaderboard = [];
			this.teamMinimap = [];
			this.globalMinimap = [];
		}

		parse(packet) {
			const rot = rotator(packet);

			if (rot.nex() !== 'b') throw new TypeError('Invalid packet header; expected packet `b`');

			this._array(rot, () => {
				const del = rot.nex();

				this.globalMinimap.remove(this.globalMinimap.findIndex(({ id }) => id === del));
			});

			this._array(rot, () => {
				const dot = {
					id: rot.nex(),
					type: rot.nex(),
					x: rot.nex(),
					y: rot.nex(),
					color: rot.nex(),
					size: rot.nex()
				};

				let index = this.globalMinimap.findIndex(({ id }) => id === dot.id);
				if (index === -1) index = this.globalMinimap.length;

				this.globalMinimap[index] = dot;
			});

			this._array(rot, () => {
				const del = rot.nex();

				this.teamMinimap.remove(this.teamMinimap.findIndex(({ id }) => id === del));
			});

			this._array(rot, () => {
				const dot = {
					id: rot.nex(),
					x: rot.nex(),
					y: rot.nex(),
					color: rot.nex()
				};

				let index = this.teamMinimap.findIndex(({ id }) => id === dot.id);
				if (index === -1) index = this.teamMinimap.length;

				this.teamMinimap[index] = dot;
			});

			this._array(rot, () => {
				const del = rot.nex();

				this.leaderboard.remove(this.leaderboard.findIndex(({ id }) => id === del));
			});

			this._array(rot, () => {
				const champ = {
					id: rot.nex(),
					score: rot.nex(),
					index: rot.nex(),
					name: rot.nex(),
					color: rot.nex(),
					barColor: rot.nex()
				};

				let index = this.leaderboard.findIndex(({ id }) => id === champ.id);
				if (index === -1) index = this.leaderboard.length;

				this.leaderboard[index] = champ;
			});

			this.leaderboard.sort((c1, c2) => c2.score - c1.score);

			return this;
		}

		_array(rot, read, length = rot.nex()) {
			const out = Array(Math.max(0, length));

			for (let i = 0; i < length; ++i) out[i] = read.call(this, i, rot);

			return out;
		}
	}

	class RecordParser {
		constructor() {
			this.score = null;
			this.seconds = null;
			this.killCount = {
				players: null,
				assists: null,
				bosses: null
			};
			this.killersLength = null;
			this.killers = [];
			this.baseCooldown = null;
		}
	
		parse(packet) {
			const rot = rotator(packet);
	
			if (rot.nex() !== 'F') throw new TypeError('Invalid packet header; expected packet `F`');
	
			this.score = rot.nex();
			this.seconds = rot.nex();
	
			this.killCount.players = rot.nex();
			this.killCount.assists = rot.nex();
			this.killCount.bosses = rot.nex();
	
			this.killersLength = rot.nex();
			for (let i = 0; i < this.killersLength; i++) {
				this.killers.push(rot.nex());
			}
	
			this.baseCooldown = rot.nex();
	
			return this;
		}
	}

	class UpdateParser {
		constructor(doEntities = true) {
			this.camera = { x: null, y: null, vx: null, vy: null, fov: null };
			this.now = 0;
			this.player = {
				fps: 1,
				body: {
					type: null,
					color: null,
					id: null,
				},
				score: null,
				points: null,
				upgrades: [],
				stats: [],
				skills: null,
				accel: null,
				top: null,
				party: null
			};
			this.entities = doEntities ? [] : false;
		}
		parse(packet) {
			const rot = rotator(packet);

			if (rot.nex() !== 'u') throw new TypeError('Invalid packet header; expected packet `u`');

			this.now = rot.nex();

			const version = this.now === 0 ? 2 : 1;

			this.camera.x = rot.nex();
			this.camera.y = rot.nex();
			this.camera.fov = rot.nex();
			this.camera.vx = rot.nex();
			this.camera.vy = rot.nex();

			const flags = rot.nex();
			if (flags & 0x0001) this.player.fps = rot.nex();
			if (flags & 0x0002) {
				this.player.body.type = rot.nex();
				this.player.body.color = rot.nex();
				this.player.body.id = rot.nex();
			}
			if (flags & 0x0004) this.player.score = rot.nex();
			if (flags & 0x0008) this.player.points = rot.nex();
			if (flags & 0x0010) this.player.upgrades = Array(Math.max(0, rot.nex())).fill(-1).map(() => rot.nex());
			if (flags & 0x0020) this.player.stats = Array(30).fill(0).map(() => rot.nex());
			if (flags & 0x0040) {
				const result = parseInt(rot.nex(), 36);

				this.player.skills = [
					(result / 0x1000000000 & 15),
					(result / 0x0100000000 & 15),
					(result / 0x0010000000 & 15),
					(result / 0x0001000000 & 15),
					(result / 0x0000100000 & 15),
					(result / 0x0000010000 & 15),
					(result / 0x0000001000 & 15),
					(result / 0x0000000100 & 15),
					(result / 0x0000000010 & 15),
					(result / 0x0000000001 & 15)
				];
			}
			if (flags & 0x0080) this.player.accel = rot.nex();
			if (flags & 0x0100) this.player.top = rot.nex();
			if (flags & 0x0200) this.player.party = rot.nex();
			if (flags & 0x0400) this.player.speed = rot.nex();

			if (version === 2 && this.entities !== false) {
				this._parseEnts(rot);
			} else if (version !== 2 && this.entities !== false) {
				this.entities = false;
				console.error('Invalid version, expected version 2. Disabling entities');
			}
			return this;
		}
		_table(rot, read) {
			const out = [];
			for (let id = rot.nex(); id !== -1; id = rot.nex()) {
				out[out.length] = read.call(this, id, rot);
			}
			return out;
		}
		_parseEnts(rot) {
			if (rot.nex() !== -1) return console.warn('uhhhh-cancelling', rot.arr);

			this._table(rot, (id) => {
				const index = this.entities.findIndex(ent => ent.id === id);
				if (index === -1) {
					return console.warn('Possible desync, deletion of non existent entity ' + id);
				}
				this.entities[index] = this.entities[this.entities.length - 1];
				--this.entities.length;
			});

			this._table(rot, (id) => {
				let index = this.entities.findIndex(ent => ent.id === id);
				if (index === -1) this.entities[index = this.entities.length] = { id };

				const ent = this.entities[index];
				this._parseEnt(ent, rot);
			});
		}

		_parseEnt(ent, rot) {
			const flags = rot.nex();
			if (!ent) console.log(this.entities.length, rot.get(rot.i - 1));
			if (flags & 0x0001) {
				let { x: lastX, y: lastY } = ent;
				ent.x = rot.nex() * 0.0625;
				ent.y = rot.nex() * 0.0625;
				if (typeof lastX !== 'undefined') {
					ent.vx = (ent.x - lastX);
					ent.vy = (ent.y - lastY);
				} else ent.vx = ent.vy = 0;
			}
			if (flags & 0x0002) ent.facing = rot.nex() * (360 / 256);
			if (flags & 0x0004) ent.flags = rot.nex();
			if (flags & 0x0008) ent.health = rot.nex() / 255;
			if (flags & 0x0010) ent.shield = Math.max(0, rot.nex() / 255);
			if (flags & 0x0020) ent.alpha = rot.nex() / 255;
			if (flags & 0x0040) ent.size = rot.nex() * 0.0625;
			if (flags & 0x0080) ent.score = rot.nex();
			if (flags & 0x0100) ent.name = rot.nex();
			if (flags & 0x0200) ent.mockupIndex = rot.nex();
			if (flags & 0x0400) ent.color = rot.nex();
			if (flags & 0x0800) ent.layer = rot.nex();
			if (flags & 0x1000) {
				if (!ent.guns) ent.guns = [];

				this._table(rot, (index) => {
					const flag = rot.nex();
					if (!ent.guns[index]) ent.guns[index] = {};
					if (flag & 1) ent.guns[index].time = rot.nex();
					if (flag & 2) ent.guns[index].power = Math.sqrt(rot.nex()) / 20;
				});
			}
			if (flags & 0x2000) {
				if (!ent.turrets) ent.turrets = [];

				ent.turrets = this._table(rot, (index) => {
					let i = ent.turrets.findIndex(ent => ent.index === index);
					if (i === -1) ent.turrets[i = ent.turrets.length] = { index };
					const turret = ent.turrets[i];

					return this._parseEnt(turret, rot);
				});
			}

			return ent;
		}
	}

	class MockupsParser {
		constructor() {
			this.entries = [];
		}
	
		parse(packet) {
			if (packet[0] !== 'J') throw new TypeError('Invalid packet header; expected packet `J`');
	
			this.entries.push(...packet.slice(1));
			return this;
		}
	
		get(index) {
			const idx = this.entries.indexOf(index) + 1;
	
			if (idx === 0) return console.error(`Index ${index} not present in mockups`, this.entries);
	
			return JSON.parse(this.entries[idx]);
		}
	}

	const coder = { encode, decode };

	// HOOKING

	const hijack = () => {
		if (window['%arras']) return window['%arras'];

		window['%arras'] = new Promise(r => {
			const _send = WebSocket.prototype.send;
			window.WebSocket = class ArrasSocket extends WebSocket {
				constructor(...args) {
					super(...args);
					this.isntArras = true;
					if (Array.isArray(args[1]) && !(args[2] && args[2] === 'apm-ignore')) {
						this.isntArras = false;
						this._hook();

						this.onopen = () => r(this);
						this.sendHooks = [];
						this.msgHooks = [];
					}
				}

				_hook() {
					if (this.isntArras) throw 'sus';

					let send = this.send;
					this.send = function (buf) {
						return send.call(this, coder.encode(this.sendHooks.reduce((data, hook) => hook(data) || data, coder.decode(buf))));
					};

					let adv = this.addEventListener;
					this.addEventListener = function (type, cb, pro = false) {
						if (pro) return adv.call(this, type, cb, pro);

						if (type === 'message') {
							adv.call(this, 'message', (event) => {
								this.msgCallback = cb;
								cb(new MessageEvent('message', {
									data: coder.encode(this.msgHooks.reduce((data, hook) => hook(data) || data, coder.decode(new Uint8Array(event.data)))).buffer
								}));
							});
						} else return adv.call(this, type, cb, pro);
					};
				}

				hookSend(...funcs) {
					this.sendHooks.push.apply(this.sendHooks, funcs);
					return this.sendHooks.length - 1;
				}
				hookMsg(...funcs) {
					this.msgHooks.push.apply(this.msgHooks, funcs);
					return this.msgHooks.length - 1;
				}

				directTalk(...data) {
					_send.call(this, coder.encode(data));
				}

				talk(...data) {
					this.send(coder.encode(data));
				}

				receive(...data) {
					this.msgCallback(new MessageEvent('message', { data: coder.encode(data) }));
				}
			};
		});

		return window['%arras'];
	};

	return { encode, decode, BroadcastParser, RecordParser, UpdateParser, MockupsParser, hijack, Server };
})();