Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
BeepIsla committed Nov 23, 2018
0 parents commit f754e5f
Show file tree
Hide file tree
Showing 30 changed files with 7,650 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.vscode
config.json
GC2ClientHelloResponse*.json
demofile.dem
33 changes: 33 additions & 0 deletions GameCoordinator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const Protos = require("./protos.js");
const Events = require("events");
const Steam = require("steam");

module.exports = class GameCoordinator extends Events {
constructor(steamUser) {
super();

this._SteamUser = steamUser;
this._GC = new Steam.SteamGameCoordinator(steamUser.client, 730);
this.Protos = Protos;

this._GC.on("message", (header, buffer, callback) => {
this.emit("debug", { header: header, buffer: buffer });

if (header.msg == Protos.EGCBaseClientMsg.k_EMsgGCClientWelcome) {
// Hello GC! 👋
// Stop ClientHello interval
if (this._GCHelloInterval) clearInterval(this._GCHelloInterval);
}
});
};

start() {
this._GCHelloInterval = setInterval(() => {
// Client Hello
this._GC.send({
msg: Protos.EGCBaseClientMsg.k_EMsgGCClientHello,
proto: {}
}, new Protos.CMsgClientHello({}).toBuffer());
}, (2 * 1000));
};
}
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# CSGO Overwatch Bot

Automatically solves Overwatch cases. Although **the detection algorythm for Aimbotting is really bad and there is no Wallhack/Griefing/etc detection**.

When running this you will get some unhandled cases which we simply don't care about.

# Aimbot detection

Log the past X ticks, when the suspect gets a kill check all angles within the past X ticks. If the difference is above the threshold add an infraction.

**This does NOT account for changes from -179 to 179 or similar and will count as an infraction**

# Config

- `account`
- - `username`: The account name you use to log into that account
- - `password`: The password for the account
- `parsing`
- - `maxTicks`: The maximum amount of ticks to check when the suspect gets a kill
- - `threshold`: The maximum threshold between angles before adding an infraction
- `verdict`
- - `maxAimbot`: The maximum amount of infractions allowed before setting user as aimbotter

# GameCoordinator

How Overwatch data is exchanged between GC and Client. The data shown below has been collected from only a single Overwatch case, results may vary depending on the case we get.

1. Send `CMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate`
- `reason` (Ex: `1`)
- - I assume this tells the GC we are waiting for a new case
2. Wait for `CMsgGCCStrike15_v2_PlayerOverwatchCaseAssignment`
- `caseid` (Ex: `331349...`)
- - Case identifier
- `caseurl` (Ex: `http://replay192.valve.net...`)
- - The download URL for the entire demo, not just the specific Overwatch
- `verdict` (Ex: `0`)
- - Unknown
- `throttleseconds` (Ex: `4800`)
- - I assume this is the time between cases, although 1.5 hours makes little sense
- `suspectid` (Ex: `909243640`)
- - Account ID of the suspect. (`[U:1:<SUSPECTID>]` for SteamID3)
- `fractionid` (Ex: `11`)
- - Unknown
- `numrounds` (Ex: `19`)
- - The round the demo starts at
- `fractionrounds` (Ex: `8`)
- - How many rounds we watch
- `streakconvictions` (Ex: `1`)
- - I assume this is how many correct convictions we got in a row. Although why would it be shown to us?
3. *Download the demo*
4. Send `CMsgGCCStrike15_v2_PlayerOverwatchCaseStatus`
- `caseid` (Ex: `331349...`)
- - Our case identifier
- `statusid` (Ex: `1`)
- - I assume this tells the GC we are currently assigned to a case and have successfully downloaded it
5. *Analyse the demo file*
6. Send `CMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate`
- `caseid` (Ex: `331349...`)
- - Our case identifier
- `suspectid` (Ex: `909243640`)
- - Our suspect
- `fractionid` (Ex: `11`)
- - Unknown
- `rpt_aimbot` (Ex: `1`)
- - Did this user have aimbot?
- `rpt_wallhack` (Ex: `1`)
- - Did this user have wallhack?
- `rpt_speedhack` (Ex: `1`)
- - Did this user have any other external assistance?
- `rpt_teamharm` (Ex: `0`)
- - Did this user grief?
- `reason` (Ex: `3`)
- - I assume this tells the GC we have finished the demo and made our verdict
7. Wait for `CMsgGCCStrike15_v2_PlayerOverwatchCaseAssignment`
- `caseid` (Ex: `331349...`)
- - Our case identifier
- `verdict` (Ex: `2`)
- - I assume this tells our client to remove the Overwatch button from the menu and that we are done with the current case
- `throttleseconds` (Ex: `7`)
- - I assume this tells our client to wait this many seconds before we are allowed a new case
8. *Repeat*

If we start the game with a Overwatch case already being assigned to us the game sends `CMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate` with `reason` being `0`. The GC wil respond with the same assigned case we got earlier. It might also respond with a new case incase our old case has expired, this is untested though.
14 changes: 14 additions & 0 deletions config.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"account": {
"username": "ExampleUsername",
"password": "ExamplePassword",
"sharedSecret": ""
},
"parsing": {
"maxTicks": 10,
"threshold": 1.0
},
"verdict": {
"maxAimbot": 20
}
}
188 changes: 188 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
const SteamUser = require("steam-user");
const SteamTotp = require("steam-totp");
const fs = require("fs");
const request = require("request");
const demofile = require("demofile");
const bz2 = require("unbzip2-stream");
const SteamID = require("steamid");
const almostEqual = require("almost-equal");
const GameCoordinator = require("./GameCoordinator.js");
const config = require("./config.json");

const steamUser = new SteamUser();
const csgoUser = new GameCoordinator(steamUser);

var logonSettings = {
accountName: config.account.username,
password: config.account.password
};

if (config.account.sharedSecret && config.account.sharedSecret.length > 5) {
logonSettings.twoFactorCode = SteamTotp.getAuthCode(config.account.sharedSecret);
}

steamUser.logOn(logonSettings);

steamUser.once("loggedOn", () => {
console.log("Logged in");
steamUser.setPersona(SteamUser.Steam.EPersonaState.Online);
steamUser.gamesPlayed([ 730 ]);
csgoUser.start();

setTimeout(() => {
console.log("-----------------\nRequested Overwatch case");
csgoUser._GC.send({
msg: csgoUser.Protos.ECsgoGCMsg.k_EMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate,
proto: {}
}, new csgoUser.Protos.CMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate({
reason: 1
}).toBuffer());
}, (4 * 1000));
});

steamUser.on("error", (err) => {
if (csgoUser._GCHelloInterval) clearInterval(csgoUser._GCHelloInterval);

console.error(err);
});

csgoUser.on("debug", (event) => {
if (event.header.msg === csgoUser.Protos.ECsgoGCMsg.k_EMsgGCCStrike15_v2_MatchmakingGC2ClientHello) {
var msg = csgoUser.Protos.CMsgGCCStrike15_v2_MatchmakingGC2ClientHello.decode(event.buffer);
console.log(msg);
return;
}

if (event.header.msg === csgoUser.Protos.EGCBaseClientMsg.k_EMsgGCClientWelcome) {
var msg = csgoUser.Protos.CMsgClientWelcome.decode(event.buffer);
console.log(msg);
return;
}

if (event.header.msg === csgoUser.Protos.ECsgoGCMsg.k_EMsgGCCStrike15_v2_PlayerOverwatchCaseAssignment) {
var msg = csgoUser.Protos.CMsgGCCStrike15_v2_PlayerOverwatchCaseAssignment.decode(event.buffer);
console.log(msg);

if (msg.caseurl) {
// Download demo
if (fs.existsSync("./demofile.dem")) fs.unlinkSync("./demofile.dem");
console.log("Downloading case " + msg.caseid + " from url " + msg.caseurl);

var sid = new SteamID("[U:1:" + msg.suspectid + "]");
if (!sid.isValid()) {
console.log("Got invalid suspect ID " + msg.suspectid);
return;
}

var r = request(msg.caseurl);
r.on("response", (res) => {
res.pipe(fs.createWriteStream("./demofile.bz2")).on("close", () => {
// Successfully downloaded, tell the GC about it!
console.log("Finished downloading " + msg.caseid + ". Unpacking...");

csgoUser._GC.send({
msg: csgoUser.Protos.ECsgoGCMsg.k_EMsgGCCStrike15_v2_PlayerOverwatchCaseStatus,
proto: {}
}, new csgoUser.Protos.CMsgGCCStrike15_v2_PlayerOverwatchCaseStatus({
caseid: msg.caseid,
statusid: 1
}).toBuffer());

// Parse the demo
fs.createReadStream("./demofile.bz2").pipe(bz2()).pipe(fs.createWriteStream("./demofile.dem")).on("close", () => {
fs.unlinkSync("./demofile.bz2");

console.log("Finished unpacking " + msg.caseid + "Parsing as suspect " + sid.getSteamID64() + "...");

// WARNING: Really shitty aimbot detection ahead!
fs.readFile("./demofile.dem", (err, buffer) => {
if (err) return console.error(err);

var aimbot_infractions = [];

const demoFile = new demofile.DemoFile();

// Detect Aimbot
var lastFewAngles = [];
demoFile.on("tickend", (curTick) => {
var ourPlayer = demoFile.players.filter(p => p.steam64Id === sid.getSteamID64());
if (ourPlayer.length <= 0) { // User left
lastFewAngles = [];
return;
}
lastFewAngles.push(ourPlayer[0].eyeAngles);

if (lastFewAngles.length >= config.parsing.maxTicks) {
lastFewAngles.shift();
}
});

demoFile.gameEvents.on("player_death", (event) => {
var attacker = demoFile.entities.getByUserId(event.attacker);
if (!attacker) return; // Attacker no longer available

for (let i = 0; i < lastFewAngles.length; i++) {
// Check pitch
if (typeof lastFewAngles[i] !== "undefined" && typeof lastFewAngles[i + 1] !== "undefined") {
if (!almostEqual(lastFewAngles[i].pitch, lastFewAngles[i + 1].pitch, config.parsing.threshold)) {
aimbot_infractions.push({ prevAngle: lastFewAngles[i], nextAngle: lastFewAngles[i + 1], tick: demoFile.currentTick });
}
}

// Check yaw
if (typeof lastFewAngles[i] !== "undefined" && typeof lastFewAngles[i + 1] !== "undefined") {
if (!almostEqual(lastFewAngles[i].yaw, lastFewAngles[i + 1].yaw, config.parsing.threshold)) {
aimbot_infractions.push({ prevAngle: lastFewAngles[i], nextAngle: lastFewAngles[i + 1], tick: demoFile.currentTick });
}
}
}
});

demoFile.parse(buffer);

demoFile.on("end", async (err) => {
if (err.error) console.error(err);

console.log("Done parsing case " + msg.caseid);
console.log(sid.getSteamID64() + " has " + aimbot_infractions.length + " infraction" + (aimbot_infractions.length === 1 ? "" : "s") + " for aimbotting");

// Once we finished analysing the demo send the results
csgoUser._GC.send({
msg: csgoUser.Protos.ECsgoGCMsg.k_EMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate,
proto: {}
}, new csgoUser.Protos.CMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate({
caseid: msg.caseid,
suspectid: msg.suspectid,
fractionid: msg.fractionid,
rpt_aimbot: (aimbot_infractions.length > config.verdict.maxAimbot) ? 1 : 0,
rpt_wallhack: 0, // TODO: Add detection for wallhacking
rpt_speedhack: 0, // TODO: Add detection for other cheats (Ex BunnyHopping)
rpt_teamharm: 0, // TODO: Add detection of griefing (Ex Afking, Damaging teammates)
reason: 3
}).toBuffer());
});
});
});
});
});
} else {
if (msg.verdict === 2) { // We are done here!
console.log("Successfully submitted verdict for case " + msg.caseid + " throttled for " + msg.throttleseconds + " seconds");

// Request a overwatch case after the time has run out
setTimeout(() => {
console.log("-----------------\nRequested Overwatch case");
csgoUser._GC.send({
msg: csgoUser.Protos.ECsgoGCMsg.k_EMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate,
proto: {}
}, new csgoUser.Protos.CMsgGCCStrike15_v2_PlayerOverwatchCaseUpdate({
reason: 1
}).toBuffer());
}, ((msg.throttleseconds + 1) * 1000));
}
}
return;
}

console.log(event); // Unhandled event
});
Loading

0 comments on commit f754e5f

Please sign in to comment.