├── .gitignore ├── LICENSE ├── README.md ├── cfgs ├── gotv │ └── replay_live.cfg └── live │ └── replay_live.cfg ├── config.js ├── package-lock.json ├── package.json ├── src ├── app.ts └── mirvpgl.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | dist/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2019 Shugo Kawamura 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [WIP]CS-GO-AutoReplayRecorder 2 | 3 | ### Author 4 | Shugo "FlowingSPDG" Kawamura 5 | 6 | ### About 7 | CSGO Auto Replay Recorder/Player for live-production,WIP! 8 | You should setup two CSGO-Client(Live-Kill collector/Delayed-ClipRecorder). and insecured CSGO server with GOTV. 9 | You may need vmix that support "Instant Replay" feature(vmix-4K or vmix-PRO). other softweres are not supported yet. 10 | also I'm planning to implement this on OBS-Studio and NewTek 3Play(only if I had chance to test it). 11 | -------------------------------------------------------------------------------- /cfgs/gotv/replay_live.cfg: -------------------------------------------------------------------------------- 1 | mirv_pgl url "ws://localhost:31338/replay_gotv"; 2 | mirv_pgl events whitelist add player_death; 3 | mirv_pgl start; -------------------------------------------------------------------------------- /cfgs/live/replay_live.cfg: -------------------------------------------------------------------------------- 1 | mirv_pgl url "ws://localhost:31337/replay_live"; 2 | mirv_pgl events whitelist add player_death; 3 | mirv_pgl start; -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gsi_auth : "SuperAuth", // GSI Auth token 3 | tv_delay : 20 , // Delay between live-server and GOTV match,tv_delay - ライブ試合鯖からのGOTV遅延 tv_delay 4 | hlae_server_port_live: 3500, // mirv_pgl server port - mirv_pglサーバーのポート番号 5 | hlae_server_path_live: "/replay_live", // mirv_pgl server path for live - mirv_pglサーバーのパス 6 | hlae_server_port_gotv: 3501, // mirv_pgl server port - mirv_pglサーバーのポート番号 7 | hlae_server_path_gotv: "/replay_gotv", // mirv_pgl server path for gotv - mirv_pglサーバーのパス 8 | replay_rec_start_before_kill: 2000, // Time to MarkIn clip before kill event(ms) - キル発生何秒前かリプレイを保存するか 9 | replay_rec_end_after_kill : 1500, // Time to MarkOut clip after kill event(ms) - キル発生何秒後までリプレイを保存するか 10 | vmix_ip : "localhost" // Vmix Web API host IP 11 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cs-go-autoreplayrecorder", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/events": { 8 | "version": "3.0.0", 9 | "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", 10 | "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" 11 | }, 12 | "@types/node": { 13 | "version": "12.12.55", 14 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.55.tgz", 15 | "integrity": "sha512-Vd6xQUVvPCTm7Nx1N7XHcpX6t047ltm7TgcsOr4gFHjeYgwZevo+V7I1lfzHnj5BT5frztZ42+RTG4MwYw63dw==" 16 | }, 17 | "@types/ws": { 18 | "version": "6.0.1", 19 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", 20 | "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", 21 | "requires": { 22 | "@types/events": "*", 23 | "@types/node": "*" 24 | } 25 | }, 26 | "async-limiter": { 27 | "version": "1.0.0", 28 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 29 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 30 | }, 31 | "axios": { 32 | "version": "0.19.0", 33 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", 34 | "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", 35 | "requires": { 36 | "follow-redirects": "1.5.10", 37 | "is-buffer": "^2.0.2" 38 | } 39 | }, 40 | "big-integer": { 41 | "version": "1.6.44", 42 | "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.44.tgz", 43 | "integrity": "sha512-7MzElZPTyJ2fNvBkPxtFQ2fWIkVmuzw41+BZHSzpEq3ymB2MfeKp1+yXl/tS75xCx+WnyV+yb0kp+K1C3UNwmQ==" 44 | }, 45 | "debug": { 46 | "version": "3.1.0", 47 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 48 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 49 | "requires": { 50 | "ms": "2.0.0" 51 | } 52 | }, 53 | "follow-redirects": { 54 | "version": "1.5.10", 55 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 56 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 57 | "requires": { 58 | "debug": "=3.1.0" 59 | } 60 | }, 61 | "is-buffer": { 62 | "version": "2.0.4", 63 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", 64 | "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==" 65 | }, 66 | "ms": { 67 | "version": "2.0.0", 68 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 69 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 70 | }, 71 | "node-csgo-gsi": { 72 | "version": "0.0.4", 73 | "resolved": "https://registry.npmjs.org/node-csgo-gsi/-/node-csgo-gsi-0.0.4.tgz", 74 | "integrity": "sha512-1mkc8WtTDS663z/pBElbce14ge8zGgFkIfMGDJLQpJmzfHnshoIPiRG6Ow6kNEJ/ENY+SmRo5DCZ5wEn94gfNA==" 75 | }, 76 | "querystring": { 77 | "version": "0.2.0", 78 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 79 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 80 | }, 81 | "vmix-js-utils": { 82 | "version": "2.0.1", 83 | "resolved": "https://registry.npmjs.org/vmix-js-utils/-/vmix-js-utils-2.0.1.tgz", 84 | "integrity": "sha512-jOUM66JuDFLJzaLz1ODod+kV/2+avch7FHqJ4OlY14w+tgkrJNAwRvvb9tnCF9cJy4KkCPlcetmsic+2QsBHTA==", 85 | "requires": { 86 | "axios": "^0.19.0", 87 | "querystring": "^0.2.0", 88 | "xmldom": "^0.1.27", 89 | "xpath": "^0.0.27" 90 | } 91 | }, 92 | "ws": { 93 | "version": "7.1.0", 94 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.1.0.tgz", 95 | "integrity": "sha512-Swie2C4fs7CkwlHu1glMePLYJJsWjzhl1vm3ZaLplD0h7OMkZyZ6kLTB/OagiU923bZrPFXuDTeEqaEN4NWG4g==", 96 | "requires": { 97 | "async-limiter": "^1.0.0" 98 | } 99 | }, 100 | "xmldom": { 101 | "version": "0.1.27", 102 | "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", 103 | "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" 104 | }, 105 | "xpath": { 106 | "version": "0.0.27", 107 | "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz", 108 | "integrity": "sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==" 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cs-go-autoreplayrecorder", 3 | "version": "0.0.1", 4 | "description": "CS:GO Auto Highlight-replay record system", 5 | "main": "app.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "node ./dist/app.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/FlowingSPDG/CS-GO-AutoReplayRecorder.git" 13 | }, 14 | "author": "Shugo \"FlowingSPDG\" Kawamura", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/FlowingSPDG/CS-GO-AutoReplayRecorder/issues" 18 | }, 19 | "homepage": "https://github.com/FlowingSPDG/CS-GO-AutoReplayRecorder#readme", 20 | "devDependencies": { 21 | "@types/node": "^12.12.55" 22 | }, 23 | "dependencies": { 24 | "@types/ws": "^6.0.1", 25 | "big-integer": "^1.6.44", 26 | "node-csgo-gsi": "0.0.4", 27 | "vmix-js-utils": "^2.0.1", 28 | "ws": "^7.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import mirvpgl from './mirvpgl'; 2 | const { Connection } = require('vmix-js-utils') 3 | 4 | const config = require("../config") 5 | const client_live = new mirvpgl(config.hlae_server_port_live, config.hlae_server_path_live); 6 | const client_gotv = new mirvpgl(config.hlae_server_port_gotv, config.hlae_server_path_gotv); 7 | const vmix = new Connection(config.vmix_ip) 8 | 9 | const kill_delay = (config.tv_delay *1000) - config.replay_rec_start_before_kill; 10 | 11 | client_live.emitter.on('gameEvent', (data) => { 12 | var obj = JSON.parse(data) 13 | if (obj.name == "player_death") { 14 | setTimeout(() => { 15 | var cmd = `spec_player_by_accountid ${obj.keys.attacker.xuid};spec_mode 1` 16 | console.log(cmd) 17 | client_gotv.sendcommand(cmd) 18 | record_clip(config.replay_rec_end_after_kill) 19 | //},kill_delay) 20 | },2000) //2sec 21 | } 22 | }) 23 | 24 | client_live.emitter.on('error', (err) => { 25 | console.error(err) 26 | }) 27 | 28 | let onSuccess = function (response:any) { 29 | console.log('Performed command', response) 30 | } 31 | let onError = function (error:any) { 32 | console.log('Could not perform command', error) 33 | } 34 | 35 | 36 | function record_clip(markout:number) { 37 | vmix.send({ Function: 'ReplayLive' }, onSuccess, onError) 38 | vmix.send({ Function: 'ReplayMarkIn' }, onSuccess, onError) 39 | setTimeout(() => { 40 | vmix.send({ Function: 'ReplayMarkOut' }, onSuccess, onError) 41 | },markout) 42 | } -------------------------------------------------------------------------------- /src/mirvpgl.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | import * as readline from 'readline'; 4 | import * as events from 'events'; 5 | import * as util from 'util'; 6 | import * as WebSocket from 'ws'; 7 | const WebSocketServer = WebSocket.Server; 8 | import * as http from 'http'; 9 | const bigInt = require("big-integer"); 10 | // MIRV PROCESS 11 | //const readline = require('readline'), 12 | //const events = require('events'), 13 | //const util = require('util'), 14 | //const WebSocketServer = require('ws').Server, 15 | // const http = require('http'), 16 | 17 | 18 | /* 19 | Prerequisites: 20 | 21 | 1. Install node.js and npm ( I used node-v10.15.3-x64.msi ) 22 | 2. npm install 23 | 24 | See also, 25 | 26 | http://einaros.github.com/ws/ 27 | 28 | To run, 29 | (npm update if you haven't in a long time) 30 | node server.js 31 | 32 | Hints: 33 | 34 | - Text entered (with enter) is sent to client as exec. 35 | - You might want to whitelist / blacklist events from being transmitted if you need to reduce the data transmitted. 36 | */ 37 | 38 | //"use strict"; // http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ 39 | 40 | 41 | //////////////////////////////////////////////////////////////////////////////// 42 | 43 | class BufferReader { 44 | buffer: Buffer; 45 | index: number; 46 | constructor(buffer:Buffer) { 47 | this.buffer = buffer 48 | this.index = 0; 49 | } 50 | 51 | readBigUInt64LE() { 52 | var lo = this.readUInt32LE() 53 | var hi = this.readUInt32LE(); 54 | 55 | return bigInt(lo).or(bigInt(hi).shiftLeft(32)); 56 | } 57 | 58 | readUInt32LE() { 59 | var result = this.buffer.readUInt32LE(this.index); 60 | this.index += 4; 61 | 62 | return result; 63 | } 64 | 65 | readInt32LE() { 66 | var result = this.buffer.readInt32LE(this.index); 67 | this.index += 4; 68 | 69 | return result; 70 | }; 71 | 72 | readInt16LE() { 73 | var result = this.buffer.readInt16LE(this.index); 74 | this.index += 2; 75 | 76 | return result; 77 | }; 78 | 79 | readInt8() { 80 | var result = this.buffer.readInt8(this.index); 81 | this.index += 1; 82 | 83 | return result; 84 | }; 85 | 86 | readUInt8() { 87 | var result = this.buffer.readUInt8(this.index); 88 | this.index += 1; 89 | 90 | return result; 91 | }; 92 | 93 | readBoolean() { 94 | return 0 != this.readUInt8(); 95 | }; 96 | 97 | readFloatLE() { 98 | var result = this.buffer.readFloatLE(this.index); 99 | this.index += 4; 100 | 101 | return result; 102 | }; 103 | 104 | readCString() { 105 | var delim = this.findDelim(this.buffer, this.index); 106 | if (this.index <= delim) { 107 | var result = this.buffer.toString('utf8', this.index, delim); 108 | this.index = delim + 1; 109 | return result; 110 | } 111 | 112 | this.readCString(); 113 | } 114 | 115 | eof() { 116 | return this.index >= this.buffer.length; 117 | } 118 | 119 | private findDelim(buffer:Buffer, idx:number): number { 120 | var delim = -1; 121 | for (var i = idx; i < buffer.length; ++i) { 122 | if (0 == buffer[i]) { 123 | delim = i; 124 | break; 125 | } 126 | } 127 | return delim; 128 | } 129 | } 130 | 131 | // GameEventUnserializer /////////////////////////////////////////////////////// 132 | 133 | class GameEventDescription { 134 | eventId: number; 135 | eventName: string | undefined; 136 | keys: any[] 137 | enrichments: any; 138 | 139 | constructor(bufferReader:BufferReader) { 140 | this.eventId = bufferReader.readInt32LE(); 141 | this.eventName = bufferReader.readCString(); 142 | this.keys = []; 143 | this.enrichments = null; 144 | 145 | while (bufferReader.readBoolean()) { 146 | var keyName = bufferReader.readCString(); 147 | var keyType = bufferReader.readInt32LE(); 148 | this.keys.push({ 149 | name: keyName, 150 | type: keyType 151 | }); 152 | } 153 | } 154 | 155 | unserialize(bufferReader:BufferReader) { 156 | var clientTime = bufferReader.readFloatLE(); 157 | 158 | var result:any = { 159 | name: this.eventName, 160 | clientTime: clientTime, 161 | keys: {} 162 | }; 163 | 164 | for (var i = 0; i < this.keys.length; ++i) { 165 | var key = this.keys[i]; 166 | 167 | var keyName = key.name; 168 | 169 | var keyValue:any; 170 | 171 | switch (key.type) { 172 | case 1: 173 | keyValue = bufferReader.readCString(); 174 | break; 175 | case 2: 176 | keyValue = bufferReader.readFloatLE(); 177 | break; 178 | case 3: 179 | keyValue = bufferReader.readInt32LE(); 180 | break; 181 | case 4: 182 | keyValue = bufferReader.readInt16LE(); 183 | break; 184 | case 5: 185 | keyValue = bufferReader.readInt8(); 186 | break; 187 | case 6: 188 | keyValue = bufferReader.readBoolean(); 189 | break; 190 | case 7: 191 | keyValue = bufferReader.readBigUInt64LE(); 192 | break; 193 | default: 194 | GameEventDescription.prototype.unserialize(bufferReader); 195 | } 196 | 197 | if (this.enrichments && this.enrichments[keyName]) { 198 | keyValue = this.enrichments[keyName].unserialize(bufferReader, keyValue); 199 | } 200 | 201 | result.keys[key.name] = keyValue; 202 | } 203 | 204 | return result; 205 | } 206 | } 207 | 208 | class UseridEnrichment{ 209 | enrichments:any; 210 | constructor(){ 211 | this.enrichments = [ 212 | 'useridWithSteamId' 213 | , 'useridWithEyePosition' 214 | , 'useridWithEyeAngles' 215 | ]; 216 | } 217 | unserialize(bufferReader:BufferReader, keyValue:any) { 218 | var xuid = bufferReader.readBigUInt64LE().toString(); 219 | var eyeOrigin = [bufferReader.readFloatLE(), bufferReader.readFloatLE(), bufferReader.readFloatLE()]; 220 | var eyeAngles = [bufferReader.readFloatLE(), bufferReader.readFloatLE(), bufferReader.readFloatLE()]; 221 | 222 | return { 223 | value: keyValue, 224 | xuid: xuid, 225 | eyeOrigin: eyeOrigin, 226 | eyeAngles: eyeAngles, 227 | }; 228 | } 229 | } 230 | 231 | class EntitynumEnrichment{ 232 | enrichments:any; 233 | constructor(){ 234 | this.enrichments = [ 235 | 'entnumWithOrigin' 236 | , 'entnumWithAngles' 237 | ]; 238 | } 239 | unserialize(bufferReader:any, keyValue:any) { 240 | var origin:number[] = [bufferReader.readFloatLE(), bufferReader.readFloatLE(), bufferReader.readFloatLE()]; 241 | var angles:number[] = [bufferReader.readFloatLE(), bufferReader.readFloatLE(), bufferReader.readFloatLE()]; 242 | 243 | return { 244 | value: keyValue, 245 | origin: origin, 246 | angles: angles, 247 | }; 248 | } 249 | } 250 | 251 | class GameEventUnserializer{ 252 | enrichments:any; 253 | knownEvents:any; 254 | constructor(enrichments:any){ 255 | this.enrichments = enrichments; 256 | this.knownEvents = {}; // id -> description 257 | } 258 | unserialize(bufferReader:any) { 259 | var eventId = bufferReader.readInt32LE(); 260 | var gameEvent:any; 261 | if (0 == eventId) { 262 | gameEvent = new GameEventDescription(bufferReader); 263 | this.knownEvents[gameEvent.eventId] = gameEvent; 264 | 265 | if (this.enrichments[gameEvent.eventName]) gameEvent.enrichments = this.enrichments[gameEvent.eventName]; 266 | } 267 | else gameEvent = this.knownEvents[eventId]; 268 | 269 | if (undefined === gameEvent) this.unserialize(bufferReader); 270 | 271 | return gameEvent.unserialize(bufferReader); 272 | } 273 | } 274 | 275 | //////////////////////////////////////////////////////////////////////////////// 276 | 277 | class Console extends EventEmitter { 278 | /* 279 | if (!(this instanceof console)){ 280 | return new console(); 281 | } 282 | */ 283 | 284 | stdin = process.stdin; 285 | stdout = process.stdout; 286 | readlineInterface: any; 287 | 288 | constructor() { 289 | super(); 290 | var self:any = this; 291 | this.readlineInterface = readline.createInterface(this.stdin, this.stdout) 292 | this.stdin = process.stdin; 293 | this.stdout = process.stdout; 294 | this.readlineInterface.on('line', function line(data:any) { 295 | self.emit('line', data); 296 | }) 297 | .on('close', function close() { 298 | self.emit('close'); 299 | }); 300 | } 301 | public print(msg: string): void { 302 | this.stdout.write(msg + '\n'); 303 | } 304 | //util.inherits(Console, events.EventEmitter); 305 | } 306 | 307 | export default class mirvpgl { 308 | emitter: EventEmitter; 309 | private ws:any = null; 310 | private wsConsole:any; 311 | private server:http.Server; 312 | private wss:any; 313 | 314 | public sendcommand(cmd:string){ 315 | if (this.ws) { 316 | this.ws.send(new Uint8Array(Buffer.from('exec\0' + cmd.trim() + '\0', 'utf8')), { binary: true }); 317 | } 318 | else{ 319 | this.wsConsole.print("ws is not active"); 320 | } 321 | } 322 | 323 | constructor(port:number,path_in:string){ 324 | var path:string; 325 | if(!(path_in.indexOf('\/') == 0)){ 326 | path = "\/" + path_in; 327 | } 328 | else { 329 | path = path_in; 330 | } 331 | //console.log(path); 332 | var self:any = this; 333 | this.emitter = new EventEmitter(); 334 | this.ws = null; 335 | this.wsConsole = new Console(); 336 | this.wsConsole.print(`Listening on port ${port}, path ${path} ...`); 337 | this.server = http.createServer(); 338 | this.server.listen(port); 339 | this.wss = new WebSocketServer({ server: this.server, path: path }); 340 | 341 | this.wsConsole.on('close', function close() { 342 | if (self.ws) self.ws.close(); 343 | process.exit(0); 344 | }); 345 | 346 | this.wsConsole.on('line',(data:string)=> { 347 | this.sendcommand(data); 348 | }); 349 | 350 | this.wss.on('connection', function (newWs:any) { 351 | if (self.ws) { 352 | self.ws.close(); 353 | self.ws = newWs; 354 | } 355 | 356 | self.ws = newWs; 357 | 358 | self.wsConsole.print(`${path} connected`); 359 | 360 | var gameEventUnserializer = new GameEventUnserializer(enrichments); 361 | 362 | self.ws.on('message', function (data:any) { 363 | if (data instanceof Buffer) { 364 | var bufferReader:any = new BufferReader(Buffer.from(data)); 365 | 366 | try { 367 | while (!bufferReader.eof()) { 368 | var cmd = bufferReader.readCString(); 369 | //self.wsConsole.print(cmd); 370 | self.emitter.emit("cmd",cmd) 371 | 372 | switch (cmd) { 373 | case 'hello': 374 | { 375 | var version = bufferReader.readUInt32LE(); 376 | //self.wsConsole.print('version = ' + version); 377 | self.emitter.emit("version",version); 378 | if (2 != version) throw "Error: version mismatch"; 379 | 380 | self.ws.send(new Uint8Array(Buffer.from( 381 | 'transBegin\0' 382 | , 'utf8')), { binary: true }); 383 | 384 | self.ws.send(new Uint8Array(Buffer.from( 385 | 'exec\0mirv_pgl events enrich clientTime 1\0', 'utf8' 386 | )), { binary: true }); 387 | 388 | for (var eventName in enrichments) { 389 | for (var keyName in enrichments[eventName]) { 390 | var arrEnrich = enrichments[eventName][keyName].enrichments; 391 | 392 | for (var i = 0; i < arrEnrich.length; ++i) { 393 | self.ws.send(new Uint8Array(Buffer.from( 394 | `exec\0mirv_pgl events enrich eventProperty "${arrEnrich[i]}" "${eventName}" "${keyName}"\0` 395 | , 'utf8')), { binary: true }); 396 | } 397 | } 398 | } 399 | 400 | self.ws.send(new Uint8Array(Buffer.from( 401 | 'exec\0mirv_pgl events enabled 1\0' // enable event 402 | , 'utf8')), { binary: true }); 403 | 404 | self.ws.send(new Uint8Array(Buffer.from( 405 | 'transEnd\0' 406 | , 'utf8')), { binary: true }); 407 | } 408 | break; 409 | case 'dataStart': 410 | break; 411 | case 'dataStop': 412 | break; 413 | case 'levelInit': 414 | { 415 | var map = bufferReader.readCString(); 416 | //self.wsConsole.print('map = ' + map); 417 | self.emitter.emit("map",map) 418 | } 419 | break; 420 | case 'levelShutdown': 421 | break; 422 | case 'cam': 423 | { 424 | var camdata:any = {} 425 | camdata.time = bufferReader.readFloatLE(); 426 | camdata.xPosition = bufferReader.readFloatLE(); 427 | camdata.yPosition = bufferReader.readFloatLE(); 428 | camdata.zPosition = bufferReader.readFloatLE(); 429 | camdata.xRotation = bufferReader.readFloatLE(); 430 | camdata.yRotation = bufferReader.readFloatLE(); 431 | camdata.zRotation = bufferReader.readFloatLE(); 432 | camdata.fov = bufferReader.readFloatLE(); 433 | 434 | self.emitter.emit("cam",camdata); 435 | } 436 | break; 437 | case 'gameEvent': 438 | { 439 | var gameEvent = gameEventUnserializer.unserialize(bufferReader); 440 | //self.wsConsole.print(JSON.stringify(gameEvent)); 441 | self.emitter.emit("gameEvent",JSON.stringify(gameEvent)) 442 | } 443 | break; 444 | default: 445 | throw "Error: unknown message"; 446 | } 447 | } 448 | } 449 | catch (err) { 450 | self.wsConsole.print('Error: ' + err.toString() + ' at ' + bufferReader.index + '.'); 451 | self.emitter.emit('error','Error: ' + err.toString() + ' at ' + bufferReader.index + '.') 452 | } 453 | } 454 | }); 455 | self.ws.on('close', function () { 456 | //self.wsConsole.print('Connection closed!'); 457 | self.emitter.emit('close') 458 | }); 459 | self.ws.on('error', function (e:any) { 460 | self.emitter.emit('error',e) 461 | }); 462 | }); 463 | } 464 | } 465 | 466 | 467 | var useridEnrichment = new UseridEnrichment(); 468 | var entitynumEnrichment = new EntitynumEnrichment(); 469 | 470 | // ( see https://wiki.alliedmods.net/Counter-Strike:_Global_Offensive_Events ) 471 | 472 | interface Ievents{ 473 | [userid:string]:UseridEnrichment | EntitynumEnrichment, 474 | } 475 | interface Ienrichments{ 476 | [key:string]:Ievents; 477 | } 478 | 479 | var enrichments:Ienrichments = { 480 | 'player_death': { 481 | 'userid': useridEnrichment, 482 | 'attacker': useridEnrichment, 483 | 'assister': useridEnrichment, 484 | }, 485 | 'other_death': { 486 | 'attacker': useridEnrichment, 487 | }, 488 | 'player_hurt': { 489 | 'userid': useridEnrichment, 490 | 'attacker': useridEnrichment, 491 | }, 492 | 'item_purchase': { 493 | 'userid': useridEnrichment, 494 | }, 495 | 'bomb_beginplant': { 496 | 'userid': useridEnrichment, 497 | }, 498 | 'bomb_abortplant': { 499 | 'userid': useridEnrichment, 500 | }, 501 | 'bomb_planted': { 502 | 'userid': useridEnrichment, 503 | }, 504 | 'bomb_defused': { 505 | 'userid': useridEnrichment, 506 | }, 507 | 'bomb_exploded': { 508 | 'userid': useridEnrichment, 509 | }, 510 | 'bomb_pickup': { 511 | 'userid': useridEnrichment, 512 | }, 513 | 'bomb_dropped': { 514 | 'userid': useridEnrichment, 515 | 'entindex': entitynumEnrichment, 516 | }, 517 | 'defuser_dropped': { 518 | 'entityid': entitynumEnrichment, 519 | }, 520 | 'defuser_pickup': { 521 | 'entityid': entitynumEnrichment, 522 | 'userid': useridEnrichment, 523 | }, 524 | 'bomb_begindefuse': { 525 | 'userid': useridEnrichment, 526 | }, 527 | 'bomb_abortdefuse': { 528 | 'userid': useridEnrichment, 529 | }, 530 | 'hostage_follows': { 531 | 'userid': useridEnrichment, 532 | 'hostage': entitynumEnrichment, 533 | }, 534 | 'hostage_hurt': { 535 | 'userid': useridEnrichment, 536 | 'hostage': entitynumEnrichment, 537 | }, 538 | 'hostage_killed': { 539 | 'userid': useridEnrichment, 540 | 'hostage': entitynumEnrichment, 541 | }, 542 | 'hostage_rescued': { 543 | 'userid': useridEnrichment, 544 | 'hostage': entitynumEnrichment, 545 | }, 546 | 'hostage_stops_following': { 547 | 'userid': useridEnrichment, 548 | 'hostage': entitynumEnrichment, 549 | }, 550 | 'hostage_call_for_help': { 551 | 'hostage': entitynumEnrichment, 552 | }, 553 | 'vip_escaped': { 554 | 'userid': useridEnrichment, 555 | }, 556 | 'player_radio': { 557 | 'userid': useridEnrichment, 558 | }, 559 | 'bomb_beep': { 560 | 'entindex': entitynumEnrichment, 561 | }, 562 | 'weapon_fire': { 563 | 'userid': useridEnrichment, 564 | }, 565 | 'weapon_fire_on_empty': { 566 | 'userid': useridEnrichment, 567 | }, 568 | 'grenade_thrown': { 569 | 'userid': useridEnrichment, 570 | }, 571 | 'weapon_outofammo': { 572 | 'userid': useridEnrichment, 573 | }, 574 | 'weapon_reload': { 575 | 'userid': useridEnrichment, 576 | }, 577 | 'weapon_zoom': { 578 | 'userid': useridEnrichment, 579 | }, 580 | 'silencer_detach': { 581 | 'userid': useridEnrichment, 582 | }, 583 | 'inspect_weapon': { 584 | 'userid': useridEnrichment, 585 | }, 586 | 'weapon_zoom_rifle': { 587 | 'userid': useridEnrichment, 588 | }, 589 | 'player_spawned': { 590 | 'userid': useridEnrichment, 591 | }, 592 | 'item_pickup': { 593 | 'userid': useridEnrichment, 594 | }, 595 | 'item_pickup_failed': { 596 | 'userid': useridEnrichment, 597 | }, 598 | 'item_remove': { 599 | 'userid': useridEnrichment, 600 | }, 601 | 'ammo_pickup': { 602 | 'userid': useridEnrichment, 603 | 'index': entitynumEnrichment, 604 | }, 605 | 'item_equip': { 606 | 'userid': useridEnrichment, 607 | }, 608 | 'enter_buyzone': { 609 | 'userid': useridEnrichment, 610 | }, 611 | 'exit_buyzone': { 612 | 'userid': useridEnrichment, 613 | }, 614 | 'enter_bombzone': { 615 | 'userid': useridEnrichment, 616 | }, 617 | 'exit_bombzone': { 618 | 'userid': useridEnrichment, 619 | }, 620 | 'enter_rescue_zone': { 621 | 'userid': useridEnrichment, 622 | }, 623 | 'exit_rescue_zone': { 624 | 'userid': useridEnrichment, 625 | }, 626 | 'silencer_off': { 627 | 'userid': useridEnrichment, 628 | }, 629 | 'silencer_on': { 630 | 'userid': useridEnrichment, 631 | }, 632 | 'buymenu_open': { 633 | 'userid': useridEnrichment, 634 | }, 635 | 'buymenu_close': { 636 | 'userid': useridEnrichment, 637 | }, 638 | 'round_end': { 639 | 'winner': useridEnrichment, 640 | }, 641 | 'grenade_bounce': { 642 | 'userid': useridEnrichment, 643 | }, 644 | 'hegrenade_detonate': { 645 | 'userid': useridEnrichment, 646 | }, 647 | 'flashbang_detonate': { 648 | 'userid': useridEnrichment, 649 | }, 650 | 'smokegrenade_detonate': { 651 | 'userid': useridEnrichment, 652 | }, 653 | 'smokegrenade_expired': { 654 | 'userid': useridEnrichment, 655 | }, 656 | 'molotov_detonate': { 657 | 'userid': useridEnrichment, 658 | }, 659 | 'decoy_detonate': { 660 | 'userid': useridEnrichment, 661 | }, 662 | 'decoy_started': { 663 | 'userid': useridEnrichment, 664 | }, 665 | 'tagrenade_detonate': { 666 | 'userid': useridEnrichment, 667 | }, 668 | 'decoy_firing': { 669 | 'userid': useridEnrichment, 670 | }, 671 | 'bullet_impact': { 672 | 'userid': useridEnrichment, 673 | }, 674 | 'player_footstep': { 675 | 'userid': useridEnrichment, 676 | }, 677 | 'player_jump': { 678 | 'userid': useridEnrichment, 679 | }, 680 | 'player_blind': { 681 | 'userid': useridEnrichment, 682 | 'entityid': entitynumEnrichment, 683 | }, 684 | 'player_falldamage': { 685 | 'userid': useridEnrichment, 686 | }, 687 | 'door_moving': { 688 | 'entityid': entitynumEnrichment, 689 | 'userid': useridEnrichment, 690 | }, 691 | 'spec_target_updated': { 692 | 'userid': useridEnrichment, 693 | }, 694 | 'player_avenged_teammate': { 695 | 'avenger_id': useridEnrichment, 696 | 'avenged_player_id': useridEnrichment, 697 | }, 698 | 'round_mvp': { 699 | 'userid': useridEnrichment, 700 | }, 701 | 'player_decal': { 702 | 'userid': useridEnrichment, 703 | }, 704 | 705 | // ... left out the gg / gungame shit, feel free to add it ... 706 | 707 | 'player_reset_vote': { 708 | 'userid': useridEnrichment, 709 | }, 710 | 'start_vote': { 711 | 'userid': useridEnrichment, 712 | }, 713 | 'player_given_c4': { 714 | 'userid': useridEnrichment, 715 | }, 716 | 'player_become_ghost': { 717 | 'userid': useridEnrichment, 718 | }, 719 | 720 | // ... left out the tr shit, feel free to add it ... 721 | 722 | 'jointeam_failed': { 723 | 'userid': useridEnrichment, 724 | }, 725 | 'teamchange_pending': { 726 | 'userid': useridEnrichment, 727 | }, 728 | 'ammo_refill': { 729 | 'userid': useridEnrichment, 730 | }, 731 | 732 | // ... left out the dangerzone shit, feel free to add it ... 733 | 734 | // others: 735 | 736 | 'weaponhud_selection': { 737 | 'userid': useridEnrichment, 738 | }, 739 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "incremental": true, /* Enable incremental compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | "types": ["node"], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | }, 62 | "include": [ 63 | "src/**/*" 64 | ] 65 | } 66 | --------------------------------------------------------------------------------