APM

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

  1. // ==UserScript==
  2. // @name APM
  3. // @version 1.0.2
  4. // @author ABC & Ray Adams
  5. // @namespace github.com/ABCxFF
  6. // @description Standard for hooking into the client -> server connection in arras.io
  7. // @match *://arras.io/
  8. // @match *://arras.netlify.app/
  9. // @grant none
  10. // @run-at document-start
  11. // @license GPL-3.0
  12. // ==/UserScript==
  13. /*
  14. * Copyright (C) 2021 ABC & Ray Adams
  15. * Licensed under GNU General Public License v3.0
  16. */
  17. const arras = (() => {
  18. const u32 = new Uint32Array(1);
  19. const u16 = new Uint16Array(1);
  20. const c16 = new Uint8Array(u16.buffer);
  21. const c32 = new Uint8Array(u32.buffer);
  22. const f32 = new Float32Array(u32.buffer);
  23. Array.prototype.remove = function (index) {
  24. if (index === this.length - 1) return this.pop();
  25. this[index] = this.pop();
  26. };
  27. function encode(message) {
  28. let headers = []
  29. let headerCodes = []
  30. let contentSize = 0
  31. let lastTypeCode = 0b1111
  32. let repeatTypeCount = 0
  33. for (let block of message) {
  34. let typeCode = 0
  35. if (block === 0 || block === false) {
  36. typeCode = 0b0000
  37. } else if (block === 1 || block === true) {
  38. typeCode = 0b0001
  39. } else if (typeof block === 'number') {
  40. if (!Number.isInteger(block) || block < -0x100000000 || block >= 0x100000000) {
  41. typeCode = 0b1000
  42. contentSize += 4
  43. } else if (block >= 0) {
  44. if (block < 0x100) {
  45. typeCode = 0b0010
  46. contentSize++
  47. } else if (block < 0x10000) {
  48. typeCode = 0b0100
  49. contentSize += 2
  50. } else if (block < 0x100000000) {
  51. typeCode = 0b0110
  52. contentSize += 4
  53. }
  54. } else {
  55. if (block >= -0x100) {
  56. typeCode = 0b0011
  57. contentSize++
  58. } else if (block >= -0x10000) {
  59. typeCode = 0b0101
  60. contentSize += 2
  61. } else if (block >= -0x100000000) {
  62. typeCode = 0b0111
  63. contentSize += 4
  64. }
  65. }
  66. } else if (typeof block === 'string') {
  67. let hasUnicode = false
  68. for (let i = 0; i < block.length; i++) {
  69. if (block.charAt(i) > '\xff') {
  70. hasUnicode = true
  71. } else if (block.charAt(i) === '\x00') {
  72. console.error('Null containing string', block)
  73. throw new Error('Null containing string')
  74. }
  75. }
  76. if (!hasUnicode && block.length <= 1) {
  77. typeCode = 0b1001
  78. contentSize++
  79. } else if (hasUnicode) {
  80. typeCode = 0b1011
  81. contentSize += block.length * 2 + 2
  82. } else {
  83. typeCode = 0b1010
  84. contentSize += block.length + 1
  85. }
  86. } else {
  87. console.error('Unencodable data type', block)
  88. throw new Error('Unencodable data type')
  89. }
  90. headers.push(typeCode)
  91. if (typeCode === lastTypeCode) {
  92. repeatTypeCount++
  93. } else {
  94. headerCodes.push(lastTypeCode)
  95. if (repeatTypeCount >= 1) {
  96. while (repeatTypeCount > 19) {
  97. headerCodes.push(0b1110)
  98. headerCodes.push(15)
  99. repeatTypeCount -= 19
  100. }
  101. if (repeatTypeCount === 1)
  102. headerCodes.push(lastTypeCode)
  103. else if (repeatTypeCount === 2)
  104. headerCodes.push(0b1100)
  105. else if (repeatTypeCount === 3)
  106. headerCodes.push(0b1101)
  107. else if (repeatTypeCount < 20) {
  108. headerCodes.push(0b1110)
  109. headerCodes.push(repeatTypeCount - 4)
  110. }
  111. }
  112. repeatTypeCount = 0
  113. lastTypeCode = typeCode
  114. }
  115. }
  116. headerCodes.push(lastTypeCode)
  117. if (repeatTypeCount >= 1) {
  118. while (repeatTypeCount > 19) {
  119. headerCodes.push(0b1110)
  120. headerCodes.push(15)
  121. repeatTypeCount -= 19
  122. }
  123. if (repeatTypeCount === 1)
  124. headerCodes.push(lastTypeCode)
  125. else if (repeatTypeCount === 2)
  126. headerCodes.push(0b1100)
  127. else if (repeatTypeCount === 3)
  128. headerCodes.push(0b1101)
  129. else if (repeatTypeCount < 20) {
  130. headerCodes.push(0b1110)
  131. headerCodes.push(repeatTypeCount - 4)
  132. }
  133. }
  134. headerCodes.push(0b1111)
  135. if (headerCodes.length % 2 === 1)
  136. headerCodes.push(0b1111)
  137. let output = new Uint8Array((headerCodes.length >> 1) + contentSize)
  138. for (let i = 0; i < headerCodes.length; i += 2) {
  139. let upper = headerCodes[i]
  140. let lower = headerCodes[i + 1]
  141. output[i >> 1] = (upper << 4) | lower
  142. }
  143. let index = headerCodes.length >> 1
  144. for (let i = 0; i < headers.length; i++) {
  145. let block = message[i]
  146. switch (headers[i]) {
  147. case 0b0000:
  148. case 0b0001:
  149. break
  150. case 0b0010:
  151. case 0b0011:
  152. output[index++] = block
  153. break
  154. case 0b0100:
  155. case 0b0101:
  156. u16[0] = block
  157. output.set(c16, index)
  158. index += 2
  159. break
  160. case 0b0110:
  161. case 0b0111:
  162. u32[0] = block
  163. output.set(c32, index)
  164. index += 4
  165. break
  166. case 0b1000:
  167. f32[0] = block
  168. output.set(c32, index)
  169. index += 4
  170. break
  171. case 0b1001:
  172. {
  173. let byte = block.length === 0 ? 0 : block.charCodeAt(0)
  174. output[index++] = byte
  175. }
  176. break
  177. case 0b1010:
  178. for (let i = 0; i < block.length; i++) {
  179. output[index++] = block.charCodeAt(i)
  180. }
  181. output[index++] = 0
  182. break
  183. case 0b1011:
  184. for (let i = 0; i < block.length; i++) {
  185. let charCode = block.charCodeAt(i)
  186. output[index++] = charCode & 0xff
  187. output[index++] = charCode >> 8
  188. }
  189. output[index++] = 0
  190. output[index++] = 0
  191. break
  192. }
  193. }
  194. return output
  195. };
  196. function decode(packet) {
  197. let data = new Uint8Array(packet)
  198. if (data[0] >> 4 !== 0b1111)
  199. return null
  200. let headers = []
  201. let lastTypeCode = 0b1111
  202. let index = 0
  203. let consumedHalf = true
  204. while (true) {
  205. if (index >= data.length)
  206. return null
  207. let typeCode = data[index]
  208. if (consumedHalf) {
  209. typeCode &= 0b1111
  210. index++
  211. } else {
  212. typeCode >>= 4
  213. }
  214. consumedHalf = !consumedHalf
  215. if ((typeCode & 0b1100) === 0b1100) {
  216. if (typeCode === 0b1111) {
  217. if (consumedHalf)
  218. index++
  219. break
  220. }
  221. let repeat = typeCode - 10 // 0b1100 - 2
  222. if (typeCode === 0b1110) {
  223. if (index >= data.length)
  224. return null
  225. let repeatCode = data[index]
  226. if (consumedHalf) {
  227. repeatCode &= 0b1111
  228. index++
  229. } else {
  230. repeatCode >>= 4
  231. }
  232. consumedHalf = !consumedHalf
  233. repeat += repeatCode
  234. }
  235. for (let i = 0; i < repeat; i++)
  236. headers.push(lastTypeCode)
  237. } else {
  238. headers.push(typeCode)
  239. lastTypeCode = typeCode
  240. }
  241. }
  242. let output = []
  243. for (let header of headers) {
  244. switch (header) {
  245. case 0b0000:
  246. output.push(0)
  247. break
  248. case 0b0001:
  249. output.push(1)
  250. break
  251. case 0b0010:
  252. output.push(data[index++])
  253. break
  254. case 0b0011:
  255. output.push(data[index++] - 0x100)
  256. break
  257. case 0b0100:
  258. c16[0] = data[index++]
  259. c16[1] = data[index++]
  260. output.push(u16[0])
  261. break
  262. case 0b0101:
  263. c16[0] = data[index++]
  264. c16[1] = data[index++]
  265. output.push(u16[0] - 0x10000)
  266. break
  267. case 0b0110:
  268. c32[0] = data[index++]
  269. c32[1] = data[index++]
  270. c32[2] = data[index++]
  271. c32[3] = data[index++]
  272. output.push(u32[0])
  273. break
  274. case 0b0111:
  275. c32[0] = data[index++]
  276. c32[1] = data[index++]
  277. c32[2] = data[index++]
  278. c32[3] = data[index++]
  279. output.push(u32[0] - 0x100000000)
  280. break
  281. case 0b1000:
  282. c32[0] = data[index++]
  283. c32[1] = data[index++]
  284. c32[2] = data[index++]
  285. c32[3] = data[index++]
  286. output.push(f32[0])
  287. break
  288. case 0b1001:
  289. {
  290. let byte = data[index++]
  291. output.push(byte === 0 ? '' : String.fromCharCode(byte))
  292. }
  293. break
  294. case 0b1010:
  295. {
  296. let string = ''
  297. let byte = 0
  298. while (byte = data[index++]) {
  299. string += String.fromCharCode(byte)
  300. }
  301. output.push(string)
  302. }
  303. break
  304. case 0b1011:
  305. {
  306. let string = ''
  307. let byte = 0
  308. while (byte = data[index++] | (data[index++] << 8)) {
  309. string += String.fromCharCode(byte)
  310. }
  311. output.push(string)
  312. }
  313. break
  314. }
  315. }
  316. return output
  317. };
  318. function rotator(packet) {
  319. return {
  320. i: 0,
  321. arr: packet,
  322. get(index) {
  323. return packet[index];
  324. },
  325. set(index, value) {
  326. return (packet[index] = value);
  327. },
  328. nex() {
  329. if (this.i === this.arr.length) {
  330. console.error(new Error('End reached'), this.arr)
  331. return -1;
  332. }
  333. return packet[this.i++];
  334. }
  335. }
  336. };
  337. class BroadcastParser {
  338. constructor() {
  339. this.leaderboard = [];
  340. this.teamMinimap = [];
  341. this.globalMinimap = [];
  342. }
  343. parse(packet) {
  344. const rot = rotator(packet);
  345. if (rot.nex() !== 'b') throw new TypeError('Invalid packet header; expected packet `b`');
  346. this._array(rot, () => {
  347. const del = rot.nex();
  348. this.globalMinimap.remove(this.globalMinimap.findIndex(({ id }) => id === del));
  349. });
  350. this._array(rot, () => {
  351. const dot = {
  352. id: rot.nex(),
  353. type: rot.nex(),
  354. x: rot.nex(),
  355. y: rot.nex(),
  356. color: rot.nex(),
  357. size: rot.nex()
  358. };
  359. let index = this.globalMinimap.findIndex(({ id }) => id === dot.id);
  360. if (index === -1) index = this.globalMinimap.length;
  361. this.globalMinimap[index] = dot;
  362. });
  363. this._array(rot, () => {
  364. const del = rot.nex();
  365. this.teamMinimap.remove(this.teamMinimap.findIndex(({ id }) => id === del));
  366. });
  367. this._array(rot, () => {
  368. const dot = {
  369. id: rot.nex(),
  370. x: rot.nex(),
  371. y: rot.nex(),
  372. size: rot.nex()
  373. };
  374. let index = this.teamMinimap.findIndex(({ id }) => id === dot.id);
  375. if (index === -1) index = this.teamMinimap.length;
  376. this.teamMinimap[index] = dot;
  377. });
  378. this._array(rot, () => {
  379. const del = rot.nex();
  380. this.leaderboard.remove(this.leaderboard.findIndex(({ id }) => id === del));
  381. });
  382. this._array(rot, () => {
  383. const champ = {
  384. id: rot.nex(),
  385. score: rot.nex(),
  386. index: rot.nex(),
  387. name: rot.nex(),
  388. color: rot.nex(),
  389. barColor: rot.nex()
  390. };
  391. let index = this.leaderboard.findIndex(({ id }) => id === champ.id);
  392. if (index === -1) index = this.leaderboard.length;
  393. this.leaderboard[index] = champ;
  394. });
  395. this.leaderboard.sort((c1, c2) => c2.score - c1.score);
  396. return this;
  397. }
  398. _array(rot, read, length = rot.nex()) {
  399. const out = Array(Math.max(0, length));
  400. for (let i = 0; i < length; ++i) out[i] = read.call(this, i, rot);
  401. return out;
  402. }
  403. };
  404. class RecordParser {
  405. constructor() {
  406. this.score = null;
  407. this.seconds = null;
  408. this.killCount = {
  409. players: null,
  410. assists: null,
  411. bosses: null
  412. };
  413. this.killersLength = null;
  414. this.killers = [];
  415. }
  416. parse(packet) {
  417. if (packet.shift() !== 'F') throw new TypeError('Invalid packet header; expected packet `F`');
  418. this.score = packet.shift();
  419. this.seconds = packet.shift();
  420. this.killCount.players = packet.shift();
  421. this.killCount.assists = packet.shift();
  422. this.killCount.bosses = packet.shift();
  423. this.killersLength = packet.shift();
  424. this.killers = packet.slice(0, this.killersLength);
  425. return this;
  426. }
  427. };
  428. class UpdateParser {
  429. constructor(doEntities = true) {
  430. this.camera = { x: null, y: null, vx: null, vy: null, fov: null };
  431. this.now = 0;
  432. this.player = {
  433. fps: 1,
  434. body: {
  435. type: null,
  436. color: null,
  437. id: null,
  438. },
  439. score: null,
  440. points: null,
  441. upgrades: [],
  442. stats: [],
  443. skills: null,
  444. accel: null,
  445. top: null,
  446. party: null
  447. }
  448. this.entities = doEntities ? [] : false;
  449. }
  450. parse(packet) {
  451. const rot = rotator(packet);
  452. if (rot.nex() !== 'u') throw new TypeError('Invalid packet header; expected packet `u`');
  453. this.now = rot.nex();
  454. const version = this.now === 0 ? 2 : 1;
  455. this.camera.x = rot.nex();
  456. this.camera.y = rot.nex();
  457. this.camera.fov = rot.nex();
  458. this.camera.vx = rot.nex();
  459. this.camera.vy = rot.nex();
  460. const flags = rot.nex();
  461. if (flags & 0x0001) this.player.fps = rot.nex();
  462. if (flags & 0x0002) {
  463. this.player.body.type = rot.nex();
  464. this.player.body.color = rot.nex();
  465. this.player.body.id = rot.nex();
  466. }
  467. if (flags & 0x0004) this.player.score = rot.nex();
  468. if (flags & 0x0008) this.player.points = rot.nex();
  469. if (flags & 0x0010) this.player.upgrades = Array(Math.max(0, rot.nex())).fill(-1).map(() => rot.nex());
  470. if (flags & 0x0020) this.player.stats = Array(30).fill(0).map(() => rot.nex());
  471. if (flags & 0x0040) {
  472. const result = rot.nex();
  473. this.player.skills = [
  474. (result / 0x1000000000 & 15),
  475. (result / 0x0100000000 & 15),
  476. (result / 0x0010000000 & 15),
  477. (result / 0x0001000000 & 15),
  478. (result / 0x0000100000 & 15),
  479. (result / 0x0000010000 & 15),
  480. (result / 0x0000001000 & 15),
  481. (result / 0x0000000100 & 15),
  482. (result / 0x0000000010 & 15),
  483. (result / 0x0000000001 & 15)
  484. ]
  485. }
  486. if (flags & 0x0080) this.player.accel = rot.nex();
  487. if (flags & 0x0100) this.player.top = rot.nex();
  488. if (flags & 0x0200) this.player.party = rot.nex();
  489. if (flags & 0x0400) this.player.speed = rot.nex();
  490. if (version === 2 && this.entities !== false) {
  491. this._parseEnts(rot)
  492. } else if (version !== 2 && this.entities !== false) {
  493. this.entities = false;
  494. console.error('Invalid version, expected version 2. Disabling entities');
  495. }
  496. return this;
  497. }
  498. _table(rot, read) {
  499. const out = [];
  500. for (let id = rot.nex(); id !== -1; id = rot.nex()) {
  501. out[out.length] = read.call(this, id, rot)
  502. }
  503. return out
  504. }
  505. _parseEnts(rot) {
  506. if (rot.nex() !== -1) return console.warn('uhhhh-cancelling', rot.arr);
  507. this._table(rot, (id) => {
  508. const index = this.entities.findIndex(ent => ent.id === id);
  509. if (index === -1) {
  510. return console.warn('Possible desync, deletion of non existant entity ' + id, this.entities.findIndex(ent => ent.id2 === id), JSON.stringify(this.entities));
  511. }
  512. this.entities[index] = this.entities[this.entities.length - 1]
  513. --this.entities.length;
  514. });
  515. this._table(rot, (id) => {
  516. let index = this.entities.findIndex(ent => ent.id === id)
  517. if (index === -1) this.entities[index = this.entities.length] = { id };
  518. const ent = this.entities[index];
  519. this._parseEnt(ent, rot)
  520. });
  521. }
  522. _parseEnt(ent, rot) {
  523. const flags = rot.nex();
  524. if (!ent) console.log(this.entities.length, rot.get(rot.i - 1));
  525. if (flags & 0x0001) {
  526. let { x: lastX, y: lastY } = ent;
  527. ent.x = rot.nex() * 0.0625;
  528. ent.y = rot.nex() * 0.0625;
  529. if (typeof lastX !== 'undefined') {
  530. ent.vx = (ent.x - lastX);
  531. ent.vy = (ent.y - lastY);
  532. } else ent.vx = ent.vy = 0;
  533. }
  534. if (flags & 0x0002) ent.facing = rot.nex() * (360 / 256);
  535. if (flags & 0x0004) ent.flags = rot.nex();
  536. if (flags & 0x0008) ent.health = rot.nex() / 255;
  537. if (flags & 0x0010) ent.shield = Math.max(0, rot.nex() / 255);
  538. if (flags & 0x0020) ent.alpha = rot.nex() / 255;
  539. if (flags & 0x0040) ent.size = rot.nex() * 0.0625;
  540. if (flags & 0x0080) ent.score = rot.nex();
  541. if (flags & 0x0100) ent.name = rot.nex();
  542. if (flags & 0x0200) ent.id2 = rot.nex();
  543. if (flags & 0x0400) ent.color = rot.nex();
  544. if (flags & 0x0800) ent.layer = rot.nex();
  545. if (flags & 0x1000) {
  546. if (!ent.guns) ent.guns = []
  547. this._table(rot, (index) => {
  548. const flag = rot.nex();
  549. if (!ent.guns[index]) ent.guns[index] = {};
  550. if (flag & 1) ent.guns[index].time = rot.nex();
  551. if (flag & 2) ent.guns[index].power = Math.sqrt(rot.nex()) / 20;
  552. });
  553. }
  554. if (flags & 0x2000) {
  555. if (!ent.turrets) ent.turrets = [];
  556. ent.turrets = this._table(rot, (index) => {
  557. let i = ent.turrets.findIndex(ent => ent.index === index)
  558. if (i === -1) ent.turrets[i = ent.turrets.length] = { index };
  559. const turret = ent.turrets[i];
  560. return this._parseEnt(turret, rot);
  561. });
  562. }
  563. return ent;
  564. }
  565. };
  566. const coder = { encode, decode };
  567. const hijack = () => {
  568. if (window['%arras']) return window['%arras'];
  569. window['%arras'] = new Promise(r => {
  570. const _send = WebSocket.prototype.send;
  571. window.WebSocket = class ArrasSocket extends WebSocket {
  572. constructor(...args) {
  573. super(...args);
  574. this.isntArras = true;
  575. if (Array.isArray(args[1])) {
  576. this.isntArras = false;
  577. this._hook();
  578. this.onopen = () => r(this);
  579. this.sendHooks = [];
  580. this.msgHooks = [];
  581. }
  582. }
  583. _hook() {
  584. if (this.isntArras) throw 'sus';
  585. let send = this.send;
  586. this.send = function(buf) {
  587. return send.call(this, coder.encode(this.sendHooks.reduce((data, hook) => hook(data) || data, coder.decode(buf))));
  588. }
  589. let adv = this.addEventListener;
  590. this.addEventListener = function(type, cb, pro=false) {
  591. if (pro) return adv.call(this, type, cb, pro);
  592. if (type === 'message') {
  593. adv.call(this, 'message', (event) => {
  594. cb(new MessageEvent('message', {
  595. data: coder.encode(this.msgHooks.reduce((data, hook) => hook(data) || data, coder.decode(new Uint8Array(event.data)))).buffer
  596. }));
  597. });
  598. } else return adv.call(this, type, cb, pro);
  599. }
  600. }
  601. hookSend(...funcs) {
  602. this.sendHooks.push.apply(this.sendHooks, funcs)
  603. }
  604. hookMsg(...funcs) {
  605. this.msgHooks.push.apply(this.msgHooks, funcs)
  606. }
  607. directTalk(...data) {
  608. _send.call(this, coder.encode(data));
  609. }
  610. talk(...data) {
  611. this.send(coder.encode(data));
  612. }
  613. }
  614. });
  615. return window['%arras']
  616. };
  617. return { encode, decode, BroadcastParser, RecordParser, UpdateParser, hijack };
  618. })()