Last active
November 20, 2024 13:45
-
-
Save lukepolo/dac28b1d323a42d885092f8b51187cd7 to your computer and use it in GitHub Desktop.
record-kills-cs2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { exec } from 'child_process'; | |
import { OBSWebSocket } from 'obs-websocket-js'; | |
import { parseEvent, parseTicks } from '@laihoe/demoparser2'; | |
// set DefensiveConCommands to 0 in game/csgo_core/gameinfo.gi | |
async function execCommand(command) { | |
return new Promise((resolve, reject) => { | |
console.info(`executing command: ${command}`); | |
exec(command, (error) => { | |
if (error) { | |
// TODO - figure out what todo , probably restart | |
reject(error); | |
} | |
resolve(); | |
}); | |
}); | |
} | |
const obs = new OBSWebSocket() | |
void execCommand('flatpak run com.obsproject.Studio'); | |
// Wait until CS2 window is found | |
let startTime = Date.now(); | |
while (Date.now() - startTime < 30000) { | |
try { | |
await execCommand('wmctrl -a "Obs"'); | |
break; | |
} catch (e) { | |
console.info('waiting for OBS'); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
} | |
} | |
await obs.connect(); | |
const outputList = await obs.call('GetOutputList'); | |
// await execCommand('steamcmd +login anonymous +app_update 730 validate +quit'); | |
await execCommand('steam steam://rungameid/730'); | |
// Wait until CS2 window is found | |
startTime = Date.now(); | |
while (Date.now() - startTime < 30000) { | |
try { | |
await execCommand('wmctrl -a "Counter-Strike 2"'); | |
break; | |
} catch (e) { | |
console.info('waiting for CS2 window'); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
} | |
} | |
//TODO - wait till were able to start dmeo | |
await new Promise(resolve => setTimeout(resolve, 15 * 1000)); | |
const { scenes } = await obs.call('GetSceneList'); | |
if (!scenes.find((scene) => { | |
return scene.sceneName === 'GameStream'; | |
})) { | |
await obs.call('CreateScene', { sceneName: 'GameStream' }); | |
} | |
await obs.call('SetCurrentProgramScene', { sceneName: 'GameStream' }); | |
// Create a browser source for the CS2 HUD | |
const { sceneItems } = await obs.call('GetSceneItemList', { sceneName: 'GameStream' }); | |
if (!sceneItems.find((item) => { | |
return item.sourceName === 'CS2 HUD'; | |
})) { | |
await obs.call('CreateInput', { | |
inputName: 'CS2 HUD', | |
inputKind: 'browser_source', | |
sceneName: 'GameStream', | |
inputSettings: { | |
url: 'http://localhost:31982/hud?transparent', | |
width: 1920, | |
height: 1080, | |
reroute_audio: false, | |
restart_when_active: true | |
} | |
}); | |
} | |
// Create a game capture source for CS2 | |
const { sceneItems: gameSceneItems } = await obs.call('GetSceneItemList', { sceneName: 'GameStream' }); | |
if (!gameSceneItems.find((item) => { | |
return item.sourceName === 'CS2 Game'; | |
})) { | |
await obs.call('CreateInput', { | |
inputName: 'CS2 Game', | |
inputKind: 'xcomposite_input', | |
sceneName: 'GameStream', | |
inputSettings: { | |
capture_window: '\r\nCounter-Strike 2\r\ncs2', | |
} | |
}); | |
} | |
async function getTickRanges(demoPath, player) { | |
let kills = parseEvent(demoPath, "player_death", ["last_place_name", "team_name"], ["total_rounds_played", "is_warmup_period"]) | |
let killsNoWarmup = kills.filter(kill => kill.is_warmup_period == false) | |
let filteredKills = killsNoWarmup.filter(kill => kill.attacker_team_name != kill.user_team_name) | |
let maxRound = Math.max(...kills.map(o => o.total_rounds_played)) | |
const killsPerRound = {}; | |
for (let round = 0; round <= maxRound; round++){ | |
let killsThisRound = filteredKills.filter(kill => kill.total_rounds_played == round) | |
for (const item of killsThisRound) { | |
if(item.attacker_name !== player) { | |
continue; | |
} | |
if(!killsPerRound[round]) { | |
killsPerRound[round] = []; | |
} | |
killsPerRound[round].push({ | |
player: item.attacker_name, | |
tick: item.tick, | |
}); | |
} | |
} | |
return killsPerRound; | |
} | |
async function startDemo() { | |
console.info('starting demo'); | |
const commands = [ | |
'wmctrl -a "Counter-Strike 2"', | |
'xdotool key "grave"', | |
'xdotool key "ctrl+a"', | |
'xdotool key "Delete"', | |
'xdotool type "playdemo test"', | |
'xdotool key Return', | |
'xdotool key "grave"' | |
]; | |
for (let i = 0; i < commands.length; i++) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
await execCommand(commands[i]); | |
} | |
} | |
async function clipDemo(demoPath, tick, player) { | |
let players = parseTicks(demoPath, ["user_id", "team_name"], [tick]) | |
let ctPlayers = players.filter(x => x.team_name == "CT") | |
let tPlayers = players.filter(x => x.team_name == "TERRORIST") | |
ctPlayers.sort((a,b) => a.user_id - b.user_id) | |
tPlayers.sort((a,b) => a.user_id - b.user_id) | |
let slots = [] | |
slots.push(...ctPlayers) | |
slots.push(...tPlayers) | |
for (let i = 1; i < slots.length; i++){ | |
slots[i]["slot_num"] = i | |
} | |
const slot = slots.find((slot) => { | |
return slot.name === player; | |
}); | |
if(!slot) { | |
console.info(`player ${player} not found`); | |
return; | |
} | |
// a tick is 64 frames, lets jump back 5 seconds | |
const tickToJumpTo = tick - (5 * 64); | |
const commands = [ | |
'wmctrl -a "Counter-Strike 2"', | |
'xdotool key "grave"', | |
'xdotool key "ctrl+a"', | |
'xdotool key "Delete"', | |
`xdotool type "demo_gototick ${tickToJumpTo}"`, | |
'xdotool key Return', | |
`xdotool type "spec_player ${slot.user_id + 1}"`, | |
'xdotool key Return', | |
'xdotool key "grave"' | |
]; | |
for (let i = 0; i < commands.length; i++) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
await execCommand(commands[i]); | |
} | |
await obs.call('SetOutputSettings', { | |
outputName: "simple_file_output", | |
outputSettings: { | |
record_folder: "/home/default/Videos", | |
record_filename: `${player}-${tick}.mp4` | |
}, | |
}); | |
console.info("START RECORD"); | |
await obs.call('StartRecord'); | |
await new Promise(resolve => setTimeout(resolve, 10 * 1000)); | |
console.info("STOP RECORD"); | |
await obs.call('StopRecord'); | |
} | |
await startDemo(); | |
// TODO - not sure how long it takes to boot the demo | |
// wait for 10 seconds | |
await new Promise(resolve => setTimeout(resolve, 10 * 1000)); | |
const player = process.argv[2]; | |
const demoPath = "/home/default/.steam/steam/steamapps/common/Counter-Strike Global Offensive/game/csgo/test.dem"; | |
for(const [round, ticks] of Object.entries(await getTickRanges(demoPath, player))) { | |
console.info(`round ${round}`, ticks); | |
for(const { tick } of ticks) { | |
try { | |
await clipDemo(demoPath, tick, player); | |
} catch (err) { | |
console.error(`Failed to clip demo at tick ${tick}:`, err.message); | |
throw err; | |
} | |
} | |
} | |
console.info("done.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment