Initial commit — Krunker Civilian Client
Cross-platform Krunker.io game client forked from Krunker Police Client with all KPD/moderator features stripped: no KPD auth, OBS recording, evidence uploads, yt-dlp, bytenode, or code obfuscation. Retained: unlimited FPS (custom Electron 42), ad blocking, resource swapper, matchmaker, userscripts, chat translator, Discord RPC, alt account manager, configurable keybinds, and advanced Chromium flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import { Socket } from 'net';
|
||||
import { electronLog } from './logger';
|
||||
|
||||
const DISCORD_CLIENT_ID = '1474451871694323975';
|
||||
|
||||
// Discord IPC opcodes
|
||||
const OP_HANDSHAKE = 0;
|
||||
const OP_FRAME = 1;
|
||||
const OP_CLOSE = 2;
|
||||
|
||||
// Rate limit: Discord rejects updates faster than 15s
|
||||
const RATE_LIMIT_MS = 5000;
|
||||
const RECONNECT_INTERVAL_MS = 30000;
|
||||
|
||||
export interface ActivityPayload {
|
||||
details?: string;
|
||||
state?: string;
|
||||
startTimestamp?: number;
|
||||
largeImageKey?: string;
|
||||
largeImageText?: string;
|
||||
}
|
||||
|
||||
function getPipePath(id: number): string {
|
||||
if (process.platform === 'win32') {
|
||||
return `\\\\?\\pipe\\discord-ipc-${id}`;
|
||||
}
|
||||
// Linux/macOS: check XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP, /tmp
|
||||
const dir = process.env.XDG_RUNTIME_DIR
|
||||
|| process.env.TMPDIR
|
||||
|| process.env.TMP
|
||||
|| process.env.TEMP
|
||||
|| '/tmp';
|
||||
return `${dir}/discord-ipc-${id}`;
|
||||
}
|
||||
|
||||
function encodeFrame(opcode: number, payload: object): Buffer {
|
||||
const json = JSON.stringify(payload);
|
||||
const jsonBuf = Buffer.from(json);
|
||||
const header = Buffer.alloc(8);
|
||||
header.writeUInt32LE(opcode, 0);
|
||||
header.writeUInt32LE(jsonBuf.length, 4);
|
||||
return Buffer.concat([header, jsonBuf]);
|
||||
}
|
||||
|
||||
export class DiscordRPC {
|
||||
private socket: Socket | null = null;
|
||||
private connected = false;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private lastUpdate = 0;
|
||||
private nonce = 0;
|
||||
private destroyed = false;
|
||||
private recvBuf = Buffer.alloc(0);
|
||||
private pendingActivity: ActivityPayload | null = null;
|
||||
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
get isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (this.destroyed) return;
|
||||
this.tryConnect(0);
|
||||
}
|
||||
|
||||
private tryConnect(pipeIndex: number): void {
|
||||
if (this.destroyed || pipeIndex > 9) {
|
||||
this.scheduleReconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
const pipePath = getPipePath(pipeIndex);
|
||||
const sock = new Socket();
|
||||
let settled = false;
|
||||
|
||||
const onError = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
sock.destroy();
|
||||
// Try next pipe index
|
||||
this.tryConnect(pipeIndex + 1);
|
||||
};
|
||||
|
||||
sock.once('error', onError);
|
||||
|
||||
sock.connect(pipePath, () => {
|
||||
if (settled || this.destroyed) {
|
||||
sock.destroy();
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
this.socket = sock;
|
||||
this.recvBuf = Buffer.alloc(0);
|
||||
|
||||
// Remove the initial error handler and set up persistent ones
|
||||
sock.removeListener('error', onError);
|
||||
sock.on('error', (err) => {
|
||||
electronLog.warn('[KCC-Discord] Socket error:', err.message);
|
||||
this.handleDisconnect();
|
||||
});
|
||||
sock.on('close', () => {
|
||||
this.handleDisconnect();
|
||||
});
|
||||
sock.on('data', (data) => {
|
||||
this.onData(data);
|
||||
});
|
||||
|
||||
// Send handshake
|
||||
const handshake = encodeFrame(OP_HANDSHAKE, {
|
||||
v: 1,
|
||||
client_id: DISCORD_CLIENT_ID,
|
||||
});
|
||||
sock.write(handshake);
|
||||
});
|
||||
|
||||
// Connection timeout — 5s
|
||||
sock.setTimeout(5000, onError);
|
||||
}
|
||||
|
||||
private onData(data: Buffer): void {
|
||||
this.recvBuf = Buffer.concat([this.recvBuf, data]);
|
||||
|
||||
while (this.recvBuf.length >= 8) {
|
||||
const opcode = this.recvBuf.readUInt32LE(0);
|
||||
const length = this.recvBuf.readUInt32LE(4);
|
||||
|
||||
if (this.recvBuf.length < 8 + length) break;
|
||||
|
||||
const jsonBuf = this.recvBuf.slice(8, 8 + length);
|
||||
this.recvBuf = this.recvBuf.slice(8 + length);
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(jsonBuf.toString());
|
||||
this.handleMessage(opcode, payload);
|
||||
} catch {
|
||||
// Malformed JSON — ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(opcode: number, payload: any): void {
|
||||
if (opcode === OP_FRAME) {
|
||||
if (payload.cmd === 'DISPATCH' && payload.evt === 'READY') {
|
||||
this.connected = true;
|
||||
electronLog.log('[KCC-Discord] Connected to Discord');
|
||||
// Flush any activity that was set before connection completed
|
||||
if (this.pendingActivity) {
|
||||
this.sendActivity(this.pendingActivity);
|
||||
this.pendingActivity = null;
|
||||
}
|
||||
}
|
||||
} else if (opcode === OP_CLOSE) {
|
||||
electronLog.warn('[KCC-Discord] Discord closed connection:', payload.message || '');
|
||||
this.handleDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private handleDisconnect(): void {
|
||||
if (!this.connected && !this.socket) return;
|
||||
this.connected = false;
|
||||
if (this.flushTimer) {
|
||||
clearTimeout(this.flushTimer);
|
||||
this.flushTimer = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
this.recvBuf = Buffer.alloc(0);
|
||||
electronLog.log('[KCC-Discord] Disconnected');
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.destroyed || this.reconnectTimer) return;
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
if (!this.destroyed && !this.connected) {
|
||||
this.tryConnect(0);
|
||||
}
|
||||
}, RECONNECT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
setActivity(activity: ActivityPayload): void {
|
||||
if (this.destroyed) return;
|
||||
|
||||
// Always store latest activity so it can be sent on (re)connect
|
||||
this.pendingActivity = activity;
|
||||
|
||||
if (!this.connected || !this.socket) return;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = now - this.lastUpdate;
|
||||
if (elapsed < RATE_LIMIT_MS) {
|
||||
// Schedule a flush after the rate limit window expires
|
||||
if (!this.flushTimer) {
|
||||
this.flushTimer = setTimeout(() => {
|
||||
this.flushTimer = null;
|
||||
if (this.pendingActivity && this.connected && this.socket) {
|
||||
this.sendActivity(this.pendingActivity);
|
||||
this.pendingActivity = null;
|
||||
}
|
||||
}, RATE_LIMIT_MS - elapsed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendActivity(activity);
|
||||
this.pendingActivity = null;
|
||||
}
|
||||
|
||||
private sendActivity(activity: ActivityPayload): void {
|
||||
if (!this.socket || this.destroyed) return;
|
||||
this.lastUpdate = Date.now();
|
||||
|
||||
const activityObj: any = {};
|
||||
if (activity.details) activityObj.details = activity.details;
|
||||
if (activity.state) activityObj.state = activity.state;
|
||||
if (activity.startTimestamp) {
|
||||
activityObj.timestamps = { start: activity.startTimestamp };
|
||||
}
|
||||
if (activity.largeImageKey) {
|
||||
activityObj.assets = {
|
||||
large_image: activity.largeImageKey,
|
||||
large_text: activity.largeImageText || 'Krunker Civilian Client',
|
||||
};
|
||||
}
|
||||
|
||||
const frame = encodeFrame(OP_FRAME, {
|
||||
cmd: 'SET_ACTIVITY',
|
||||
args: {
|
||||
pid: process.pid,
|
||||
activity: activityObj,
|
||||
},
|
||||
nonce: String(++this.nonce),
|
||||
});
|
||||
|
||||
try {
|
||||
this.socket.write(frame);
|
||||
} catch (err) {
|
||||
electronLog.warn('[KCC-Discord] Write error:', (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
clearActivity(): void {
|
||||
if (!this.connected || !this.socket || this.destroyed) return;
|
||||
|
||||
const frame = encodeFrame(OP_FRAME, {
|
||||
cmd: 'SET_ACTIVITY',
|
||||
args: {
|
||||
pid: process.pid,
|
||||
activity: null,
|
||||
},
|
||||
nonce: String(++this.nonce),
|
||||
});
|
||||
|
||||
try {
|
||||
this.socket.write(frame);
|
||||
} catch {
|
||||
// Silent
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this.destroyed = true;
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
if (this.flushTimer) {
|
||||
clearTimeout(this.flushTimer);
|
||||
this.flushTimer = null;
|
||||
}
|
||||
if (this.socket) {
|
||||
try {
|
||||
this.clearActivity();
|
||||
} catch {
|
||||
// Silent
|
||||
}
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
this.connected = false;
|
||||
this.recvBuf = Buffer.alloc(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user