A CLI for CSGO, another useless side-project?

To be honest, I don’t think there is any use case for a CLI (Command Line Interface) for the game CSGO (Counter Strike Global Offensive). But who cares? For me, a side-project does not have to have any use case or so. For me, a side-project is about learning and trying new stuff. I learned how easy it is to write a CLI in Python, using typer, and I realized how great Python is for automating stuff.

In this blog post, which is my first blog post ever, I will go through how I created a CLI for CSGO. All the code shown in this blog and the source code for the project is available on github, and the CLI is available PyPi*.

** The code shown here in the blog is functional but simplified, for edge-case support, exception handling, and more robust code, I suggest having a look at the source code on github.*

What should the CLI do?

From the start, the idea was that the CLI should only contain two simple commands.

  • servers: Display live information like the number of players and current map for my favorite marked servers.
  • connect: Automatically start CSGO and connect to a given server.

The workflow I had in mind was that I should quickly get an overview of my favorite servers and connect to one. The goal was that I should not have to interact with CSGO myself before I joined a server.

Getting Started with the Developing

When working with Python, I prefer working in virtual environments.

I start by:

$ python3 -m venv venv
# On Windows
$ ./venv/Scripts/activate
# On Unix
$ source venv/bin/activate

I knew that I would require the typer package, and I installed it by

$ pip install typer
Installing collected packages: click, typer
Successfully installed click-7.1.2 typer-0.3.2

The Typer package is very intuitive and easy to work with. On a high level, you first create a Typer object which you then add commands to. In Typer, commands are to decorated python functions. Below is some boilerplate code for csgo.py.

import typer

app = typer.Typer()

@app.command()
def servers():
    typer.echo("Listing my favorite servers...")

@app.command()
def connect():
    typer.echo("Connecting to a server...")


if __name__ == "__main__":
    app()

To run the CLI defined in csgo.py:

$ python csgo.py
Usage: post.py [OPTIONS] COMMAND [ARGS]...

Options
  --help                          Show this message and exit.

Commands:
  connect
  servers

And to execute a specific command:

$ python csgo.py connect
Connection to a server...

At the end, I explain how to set up the CLI so that you can run it like $ csgo servers and not $ python csgo.py servers

Let’s add the proper functionality to the commands!

Steam stores the favorite marked servers in a file called serverbrowser_hist.vdf, and I found it here: /c/Program Files (x86)/Steam/userdata/160616678/7/remote/serverbrowser_hist.vdf. This file will be necessary for both commands but let’s start implementing the servers command.

Command: servers

In python, there are packages for everything! I use the vdf for reading the server serverbrowser_hist.vdf into a dict, and I use python a2s for querying information from the servers. The servers function looks like this:

@app.command()
def servers():
    """
    List your favorite GSGO servers
    """
    servers = get_servers()
    # Print out a nice table with the live information about the servers
    table = tabulate(servers, headers="keys")
    typer.echo(f"{table}\n")
    
def get_servers():
  # Read the serverbrowser_hist.vdf
  # PATH is is the path to the serverbrowser_hist.vdf file. 
  serverbrowser = vdf.load(open(PATH))
  favorite_servers = serverbrowser['Filters']['Favorites']

  server_infos = []
  for server in favorite_servers:
      # Extract server information
      server = favorite_servers[server]
      server_ip = server['address'].split(':')[0]
      server_port = int(server['address'].split(':')[1])
      address = (server_ip, server_port)
      try:
          # Try to query live server information
          info = a2s.info(
              address, 
              timeout=a2s.defaults.DEFAULT_TIMEOUT, 
              encoding=a2s.defaults.DEFAULT_ENCODING
            )
          server_info = {
              "INDEX": len(server_infos),
              "NAME": info.server_name,
              "MAP": info.map_name,
              "PLAYERS": f"{info.player_count}/{info.max_players}",
              "ADDRESS": f"{server_ip}:{server_port}",
          }
          server_infos.append(server_info)
      except socket.timeout:
          pass
  

Running this command results in the following output:

$ python csgo.py servers
  INDEX  NAME                                                                        MAP             PLAYERS    ADDRESS
-------  --------------------------------------------------------------------------  --------------  ---------  -------------------
      0  BrutalCS - RETAKES ★ 02 ★ MAPVOTE ★ 128T                                   de_cbble        10/10      178.236.67.18:27015
      1  BrutalCS - RETAKES ★ 04 ★ MAPVOTE ★ 128T                                   de_inferno      10/10      178.236.67.33:27015
      2  BrutalCS - RETAKES ★ 13 ★ MI/CA/INF ★ 128T                                 de_mirage       1/12       178.236.67.56:27015
      3  BrutalCS - RETAKES ★ 14 ★ MAPVOTE ★ 128T                                   de_dust2        10/10      178.236.67.20:27015
      4  BrutalCS - RETAKES ★ 12 ★ MI/CA/INF ★ 128T                                 de_inferno      1/12       178.236.67.55:27015
      5  BrutalCS - RETAKES ★ 07 ★ MAPVOTE ★ 128T                                   de_vertigo      1/12       178.236.67.37:27015

Now that I can list my favorite servers from a simple CLI command, I just need to be able to connect to one of them.

Command: connect

A simple way to connect to a steam server from python is to use steams browser protocol. For instance, by entering steam://connect/<ip> where ip is the IP to a CSGO server, two things will happen. First, CSGO gets started, and secondly, it will connect to the server automatically. To take advantage of this steam browser protocol, I used the webbrowser python module.

This command needs to take an argument, namely, the server in the list above to connect to. Arguments for a command are created by typer.Argument(...) see the connect command below for an example.

@app.command()
def connect(
    server_index: int = typer.Argument(None):
    """
    Start CSGO and connect to a specific game server.
    """
    server = get_servers()[server_index]
    typer.echo(f"Starting CSGO and joining server {server['NAME']} ...")
    server_url = server['ADDRESS']
    webbrowser.open(f'steam://connect/{server_url}')
    return

I can now use this connect command for connecting to a server, for instance, server number one in the table above.

$ python csgo.py connect 1
Starting CSGO and joining server BrutalCS - RETAKES ★ 04 ★ MAPVOTE ★ 128T

CSGO gets started and automatically connects to BrutalCS - RETAKES ★ 04 ★ MAPVOTE ★ 128T, the project succeeded.

Good Times!

Simplify the usage of the CLI

Now that I’m happy with the CLI, the last thing to do is simplify its usage. I don’t want to have to type $ python csgo.py for triggering the CLI. Let’s remove that part!

Create setup.py that requires the setuptools package.

import setuptools

with open("requirements.txt") as f:
    required = f.read().splitlines()

setuptools.setup(
    name="csgo-cli",
    version="0.1.0",
    packages=setuptools.find_packages(),
    python_requires=">=3.6",
    install_requires=required,
    entry_points={
        'console_scripts': ['csgo=cli:app'],
    }
)

This setup.py requires a requirements.txt file and the content of that file:

typer
setuptools
tabulate
python-a2s
vdf
typer
setuptools

The setup.py allows easy install of the Python Package. Using a setup.py module is the standard way for distributing Python Modules. To install this CSGO CLI Package locally, I run the following pip command:

$ pip install -e .

Now I’m happy! The CLI is finished, and I can use it by typing csgo in the terminal!

Thanks for reading!