
Writing a Server Starter CLI for CS:GO With JavaScript
CS:GO is one of the most popular games out there and recently it has been made completely free. How many of you have tried out the game at least once? How many of you have been actively playing with the game? I guess we can all relate.
Iāve been one of those few people who donāt really dig the world of FPS, especially when it comes to Counter-Strike. But when Valve announced that CS:GO is going free, my friends have tried to persuade me harder than ever before.
Since then, we often play and so the idea came to write a script that starts a server so we donāt have to do it manually each time we want to go for a round. Can you imagine how many hours of time we were able to save by investing in this tool? Well probably not hours but maybe minutes.
Anyway, I managed to do it and this is the story about the challenges and the final success that follows.

The Concept
The main concept was that the user starts a batch file from which they can select the game mode and a map and click on run. The game starts with the defined params and everybody is happy. ā This was the concept. It was easier to conceptualize than to implement. And I havenāt even know by then what have I gotten into.
It seems it is much harder to start a game and automate keyboard input at the same time than I first thought. The biggest challenge was to know when the game finally booted up. I had a couple of ideas to go with.
- The first and most obvious was to see if thereās an API that can handle all of that. There wasnāt.
- What if there is a process event emitted when the game starts? ā I couldnāt rely on processes since the start of the process doesnāt mean the game is already in the main menu.
- What if I create a screenshot about the main menu and compare pixels to check if the main menu is displayed? ā That wonāt work in case they change the main menu. Not to mention itās unnecessarily complex.
Seemed like every solution I came up with had a flaw. I was even thinking about trying out image recognition to spot fixed parts of the game that rarely change and do a match check against them. This would have been an overkill.
The solution was much more simpler but probably the hackiest of all.

The Dependencies
Since Iām a web developer, it was obvious for me to go with JavaScript. And since I only wanted a CLI, node was the obvious choice. We all know node is all about dependencies. To make my concept work, I needed 4.
First I needed a package that hides the underlying mystical layers of interacting with the command line. I initially wanted to go withĀ inquirer, but from the docs, it seemed like it doesnāt support tables. Displaying the options in one single column was less user friendly than doing the same in a table, especially if the player has thousands of workshop maps. SoĀ terminal-kitĀ was the better choice.
I also neededĀ robotjsĀ which exposes some nice API to interact with user input devices such as a keyboard or a mouse. It is required to eventually write out the command inside the game.
Since we are working from node, we donāt have the same API we have inside the browser. To copy the params into the clipboard I neededĀ copy-pasteĀ therefore, which adds the functionality for that.
Last but not least, to ease the process of reading maps,Ā pathĀ is also among the list of dependencies
This is the finalĀ package.json
Ā file Iām using:
{
"name": "cs-server",
"version": "2.0.0",
"description": "",
"main": "CSScript.js",
"author": "",
"license": "ISC",
"dependencies": {
"copy-paste": "1.3.0",
"path": "0.12.7",
"robotjs": "0.5.1",
"terminal-kit": "1.27.0"
}
}

Creating The Configuration File
To declutter the starting script, I moved out some options into aĀ config.js
Ā file which holds all changeable parameters. Such as the path to the CS root folder, the shortcut key for opening the console in game, and an array holding all parameters that are usually set by the players. These will be copied into the clipboard to later be pasted into the console.
/**
* Modify the rootPath and consoleShortcut to match your settings.
* If you wish to add additional params to the game, you can do so in optionalParams, these will be copied to your clipboard
*/
module.exports = {
rootPath: 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Counter-Strike Global Offensive', // Your CS:GO root path
consoleShortcut: 'f2', // Set this to the button that you use to open the console in CS:GO
// params to set in CS:GO
optionalParams: [
'mp_friendlyfire 1',
'mp_solid_teammates 2',
'mp_freezetime 5',
'mp_autoteambalance 0'
]
};
Creating The Menu File
I also moved some constants into another file calledĀ menu.js
, again for removing some noise from the main script file. This file holds the menu options the user can choose from as well as information on the selected game mode and map:
const selectedMenuItem = {
gameMode: {
name: 'Competitive',
type: 0,
mode: 0
},
map: {
mapName: '',
mapPath: ''
}
};
const menuOptions = [
'Let\'s teach these dogs a lesson',
'Game Mode',
'Classic maps',
'Workshop maps',
'Exit'
];
module.exports = {
selectedMenuItem,
menuOptions
};

Writing The CS-Script
The main script file is responsible for creating the CLI, collecting available maps, and starting the game. To create an executable version for it, I also created aĀ .bat
Ā file which holds one single line:
node CSScript
Tells node to runĀ CSScript.js
.
Setting everything up
To set everything up, I pulled in all the dependencies as well as two additional built in packages:
spawn
Ā which will be responsible for starting the process for CSfs
Ā is the file system which is used to read in maps
const spawn = require('child_process').spawn;
const robot = require('robotjs');
const ncp = require('copy-paste');
const fs = require('fs');
const path = require('path');
const terminal = require('terminal-kit').terminal;
const settings = require('./config.js');
const {
selectedMenuItem,
menuOptions
} = require('./menu.js');
const gameModes = [
'Casual',
'Competitive',
'Arms Race',
'DeathMatch',
'Demolition',
'Wingman'
];
const maps = {
classic: [],
workshop: []
};
const defaults = {};
const isMap = (file) => path.extname(file) === '.bsp';
let gameStartedBooting = false;
let gameInterval;
Apart from importing theĀ config
Ā andĀ menu
Ā files ā
- I also defined the list of available game modes from which the player can choose from
- A constant forĀ
maps
Ā that will hold all classic and workshop maps - AĀ
defaults
Ā object which will hold paths to game files and the size of our screenĀ (this will be needed for checking if the game booted) - And I also added a helper function for checking if a file is a map.
I also added a flag to check if the game has started booting as well as an interval that will set up aĀ setInterval
Ā which will do the hackiest checks Iāve ever created.
Setting up the defaults
First things first: we need to collect some information from the player, such as the route for the maps, the executable fileās path, and the screensize. This is what this function is supposed to do:
function setDefaults() {
defaults.mapPath = `${settings.rootPath}\\csgo\\maps`;
defaults.workshopMapPath = `${settings.rootPath}\\csgo\\maps\\workshop\\`;
defaults.executablePath = `${settings.rootPath}\\csgo.exe`;
defaults.screenSize = robot.getScreenSize();
}
Iām assigning it toĀ defaults
Ā since I will make use of them later on. Getting the screen size is easy with robotjs, I just had to callĀ robot.getScreenSize
.
Getting the maps
To let the user select a map, we first need to know what maps they have. To do so, I defined two functions after setting the defaults:Ā getMaps
Ā andĀ getWorkshopMaps
.
function getMaps() {
fs.readdirSync(defaults.mapPath).forEach(file => {
if (isMap(file)) {
const mapName = path.basename(file, '.bsp');
maps.classic.push({
mapName,
mapPath: mapName
});
}
});
}
For getting the maps, the function reads the directory which is set by theĀ setDefaults
Ā function. To be on the safe side, it checks if the file in the loop is a map and populatesĀ maps.classic
Ā with it. TheĀ mapName
Ā property will be displayed in the CLI while theĀ mapPath
Ā will be inserted into the console. For classic maps, thereās no difference between the map name and map path, but for workshop maps, there is:
function getWorkshopMaps() {
fs.readdirSync(defaults.workshopMapPath).forEach(file => {
fs.readdirSync(defaults.workshopMapPath + file).forEach(internal => {
if (isMap(internal)) {
const mapName = path.basename(internal, '.bsp');
const mapPath = `workshop\/${file}\/${mapName}`;
maps.workshop.push({
mapName,
mapPath
});
maps.workshop.sort((a, b) => a.mapName > b.mapName ? 1 : -1);
}
});
});
}
The same goes for the workshop maps, but here, each map is inside a sub-folder. This is the reason we need nested loops and thatās why we have different values for the map name and the path.

Creating the main menu
The next step was to finally get something on the CLI, just some basic information first: showing the selected game mode and the map. Thanks to terminal-kit, this was fairly easy:
terminal.white('Welcome to the CS Server Starter!\n\n');
terminal.cyan('Selected gamemode: ');
terminal.red(selectedMenuItem.gameMode.name + '\n');
terminal.cyan('Selected map:\t ');
terminal.red(selectedMenuItem.map.mapName + '\n');
It is also time to put the menu in place with the help ofĀ terminal.singleColumnMenu
. I get the menu options from the menu file and based on the selected menuās name, I execute a given function:
terminal.singleColumnMenu(menuOptions, (error, response) => {
const selectedMenuItem = menuOptions[response.selectedIndex];
switch (selectedMenuItem) {
case 'Let\'s teach these dogs a lesson': letsTeachTheseDogsALesson(); break;
case 'Game Mode': selectGameModeMenu(); break;
case 'Classic maps': selectMap(false); break;
case 'Workshop maps': selectMap(true); break;
case 'Exit': process.exit(); break;
default:
terminal.red('Something wrong has happened... oh boy š')
process.exit();
break;
}
});
To keep things separated, Iāve put everything inside aĀ mainMenu
Ā function and added aĀ start
Ā function at the bottom of the file which will call it. I also call theĀ setDefault
,Ā getMaps
Ā andĀ getWorkshopMaps
Ā functions here so we have every variable set up.
function start() {
setDefaults();
getMaps();
getWorkshopMaps();
mainMenu();
}
start();
From here on, we can take a look at each functionās implementation called for the menu items, apart from the exit menu as its only purpose is to kill the running task.
Selecting game mode
Selecting game mode is all about assigning a value toĀ selectedMenuItem.gameMode
. It is made up of three different functions:
function selectGameModeMenu() {
terminal.singleColumnMenu(gameModes, { cancelable: true }, (error, response) => {
selectGameMode(response.selectedText);
mainMenu();
});
};
function selectGameMode(gamemode) {
switch (gamemode) {
case 'Casual': setMenuGameMode('Casual', 0, 0); break;
case 'Competitive': setMenuGameMode('Competitive', 1, 0); break;
case 'Arms Race': setMenuGameMode('Arms Race', 0, 1); break;
case 'DeathMatch': setMenuGameMode('DeathMatch', 2, 1); break;
case 'Demolition': setMenuGameMode('Demolition', 1, 1); break;
case 'Wingman': setMenuGameMode('Wingman', 2, 0); break;
default: setMenuGameMode('Competitive', 1, 0); break;
}
};
function setMenuGameMode(name, mode, type) {
selectedMenuItem.gameMode = {
name,
mode,
type
};
};
selectGameModeMenu
Ā is called from theĀ mainMenu
Ā function when the game mode menu is selected. It then displays a single list created from theĀ gameModes
Ā array defined at the top of the file. When an option is selected, it calls through toĀ selectGameMode
, which holds a switch case to set the correct mode and type. These values will be placed inside the gameās console. You can get the list of game mode commands fromĀ here.
When the game mode is selected, we callĀ mainMenu
Ā to return.
Selecting maps
Selecting a map is also about setting the proper value toĀ selectedMenuItem.map
Ā which can later be used for typing the map into the gameās console. Youāve probably noticed I call theĀ selectMap
Ā function from theĀ mainMenu
Ā with a flag. This is for deciding if the player wants to select classic or workshop maps:
function selectMap(isWorkShopMap) {
const menuItems = [];
if (isWorkShopMap) {
maps.workshop.forEach(map => menuItems.push(map.mapName));
} else {
maps.classic.forEach(map => menuItems.push(map.mapName));
}
terminal.gridMenu(menuItems, {cancelable: true}, (error, response) => {
if (isWorkShopMap) {
selectedMenuItem.map = maps.workshop[response.selectedIndex];
} else {
selectedMenuItem.map = maps.classic[response.selectedIndex];
}
mainMenu();
});
};
I create the menu items from the list of maps generated byĀ getpMaps
Ā andĀ getWorkshopMaps
Ā and display a menu usingĀ terminal.gridMenu
. Once an option is selected, I set the value ofĀ selectedMenuItem.map
Ā to be the selected optionās value and then we can return to the main menu.
Selecting a default game
Before looking into how Iāve managed to launch the game, test if it is booted and put everything inside the console to start it, I added a small function toĀ start
Ā to select a default game mode and type:
function selectMapAndGameMode() {
selectGameMode('Competitive');
selectedMenuItem.map = maps.classic[21];
}
function start() {
setDefaults();
getMaps();
getWorkshopMaps();
selectMapAndGameMode();
mainMenu();
}
Creating The Heart ā Booting The Game
To start the game, we can get it done relatively easily. The hard part will be to tell if we are in the main menu; before that, we canāt open the console in the game.
function letsTeachTheseDogsALesson() {
ncp.copy(settings.optionalParams.join(';'));
spawn(defaults.executablePath, ['-steam']);
gameInterval = setInterval(checkGameState, 1000);
terminal.white('\nUsing additional params:\n');
terminal.cyan(` - ${settings.optionalParams.join('\n - ')}`);
terminal.white('\n\ncopied additional params to clipboard\n\n');
}
This is where I used the copy-paste npm package to copy the parameters defined inĀ config.js
Ā and log it out to the player. The game can be started with theĀ spawn
Ā function, we need to set theĀ -steam
Ā flag, otherwise it will say you ran the game in āinsecure modeā.
And the magic happens inside theĀ checkGameState
Ā function. The interval executes it each second to check if the game has started. This is the code for the function:
function checkGameState() {
const hex = robot.getPixelColor(0, defaults.screenSize.height / 2);
const onLoadingScreen = hex === '000000' && mouse.x === 0;
const onMainMenu = hex !== '000000' && mouse.x === 0;
if (!gameStartedBooting && onLoadingScreen) {
terminal.white('Game booting up\n');
gameStartedBooting = true;
}
if (gameStartedBooting && onMainMenu) {
const mode = selectedMenuItem.gameMode.mode;
const type = selectedMenuItem.gameMode.type;
const map = selectedMenuItem.map.mapPath;
terminal.green('Game loaded, starting server...');
robot.setKeyboardDelay(500);
robot.keyTap(settings.consoleShortcut);
robot.typeString(`game_mode ${mode}`);
robot.keyTap('enter');
robot.typeString(`game_type ${type}`);
robot.keyTap('enter');
robot.typeString(`map ${map}`);
robot.keyTap('enter');
clearInterval(gameInterval);
}
}
The solution:
The solution was the following. Start the game and get the pixel color of the screen from the middle left-hand side. If it is pure blackĀ (#000), the game hasĀ PROBABLYĀ started booting up. If it is no more black then the game has loaded. If weāre in the main menu, we can start typing in commands and press enter then clear the interval.
Iām saying probably because what if your background screen color is exactly black at that point? Probably not a problem as it keeps waiting until it turns into something else. Probably. What if the main menu changes in a way that makes that single pixel black? ā The script will come to a halt and the interval will never be cleared. What if you open the game in windowed mode? Not a bulletproof solution but it gets things done. Most of the time.


Limitations
There are some limitations to it, which you will soon find out if you want to start a local server with it, such as:
- You need to press shift in-game to make sure the console works again.
- If you exit the game, you have to close the console before starting the game again, you canāt restart a new game from the same console.
- If you close the cmd, it will also terminate the game, since it is a child process started from the console.
I havenāt found any other issues with it so this is just a small price to pay for salvation. If youāre interested in the script or want to use it yourself, you can clone it fromĀ GitHub. Now letās go andĀ teach these dogs a lesson. š®

Unlimited access to hundred of tutorials
Access to exclusive interactive lessons
Remove ads to learn without distractions