Standard for hooking into the client -> server connection in arras.io
此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/483575/1304377/APMU.js
/* ==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 }; })();