About a year ago I went to a LAN-Party. For the first time in a long time I played Counter-Strike. also for the first time I had a really fun time playing because of the team spirit. After 10 minutes of playing we got into our first competition. Of course we failed miserably – but had a blast doing so.
The game really got to me in a good way. So the group that I went with started practicing. Of course just for he fun of it but it is really fun. Also I came across a few of the finals on youtube and that was when I saw it. The lighting on the stage.
If you don’t know what I’m talking about, have a look here:
The bomb goes off and they have flames and all sorts of cool things. Wouldn’t it be awesome if we could have that at home? (minus the flames)
Well, as it turns out – you can!
A friend of mine send me this link:
https://developer.valvesoftware.com/wiki/Counter-Strike:_Global_Offensive_Game_State_Integration
That perfectly describes how to get game state information directly from the game client. Actually the site even have sample code available that works just like that!
So what do I need?
There are 3 parts in this setup.
1. the CS:GO Config file that tells the client what information to send.
2. the HTTP server that the client sends its data to.
3. the hardware that interface with the lighting or whatever you want it to.
Configuration file
following the site (see link above) I created a simple config for data retrieval:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
"Console Sample v.1" { "uri" "http://127.0.0.1:3005" "timeout" "5.0" "buffer" "0.1" "throttle" "0.5" "heartbeat" "5.0" "output" { "precision_time" "3" "precision_position" "1" "precision_vector" "3" } "data" { "round" "1" // round phase ('freezetime', 'over', 'live'), bomb state ('planted', 'exploded', 'defused'), and round winner (if any) } } |
I have deliberately removed everything that I didn’t need along with the authentication key. This is just for testing and I want to keep it simple.
One thing I did change after testing was the ‘heartbeat’ value. the standard was 60 seconds but the problem when debugging was that if I restarted the HTTP server it would take ages before I would another game state update. the heartbeat sets this interval so with a low value, even if I restart the server I get the game state pretty quickly.
HTTP Server
Again, the instruction page is really helpful. They provide a (simple) server written in Node.js. I have no experience with this, at all so how hard can it be?
I started with the provided file and then added a few things. I added a JSON deserializer so that I could do some logic on the game states. Then I installed ‘serialport’ so I could send a message to the hardware when I needed to.
The protocol is not pretty. really, it’s not. But it does the job. For each specific state I want to react on, I send a letter over the serial port. ‘b’ is for bomb (obviously). the rest is just the next letter in the alphabet. it’s simple and it works.
This is the complete code for the server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
http = require('http'); fs = require('fs'); const SerialPort = require('serialport'); const serial = new SerialPort('COM4', { baudRate: 115200 }); //serial connection to the lighting controller serverport = 3005; //defines the port we're listening on host = '127.0.0.1'; server = http.createServer( function(req, res) { if (req.method == 'POST') { res.writeHead(200, {'Content-Type': 'text/html'}); var body = ''; req.on('data', function (data) { body += data; }); req.on('end', function () { //console.log("POST payload: " + body); //uncomment this line if you want to see the JSON message res.end( '' ); if (res.statusCode === 200) { try //we better pack the entire thing in a try statement or the server might crash... { var data = JSON.parse(body); //deserialize the JSON contents // data is available here: console.log(data.round.phase); //print the phase to the screen if(data.round.phase=='freezetime') { serial.write('a'); } if(data.round.phase=='live') { serial.write('c'); } if(data.round.phase=='over') { console.log(data.round.win_team); if(data.round.win_team=='CT') serial.write('f'); else serial.write('g'); } //this is only valid if the JSON contains information about the bomb if(data.round.bomb!=undefined) { console.log(data.round.bomb); //better print it for clarity if(data.round.bomb=='planted') serial.write('b'); if(data.round.bomb=='exploded') serial.write('d'); if(data.round.bomb=='defused') serial.write('e'); } } catch (e) { console.log('Error parsing JSON!'); } } }); } else { console.log("Not expecting other request types..."); res.writeHead(200, {'Content-Type': 'text/html'}); var html = '<html><body>HTTP Server at http://' + host + ':' + serverport + '</body></html>'; res.end(html); } }); server.listen(serverport, host); console.log('Listening at http://' + host + ':' + serverport); |
At this time the serial port is hardcoded but it probably won’t matter much. maybe if I change to another USB port – uh-oh..
LED interface
Now for the fun part. the hardware.
again it’s really simple. just an Arduino with a strip of WS2812b LED’s on. Neopixels if you insist. Really, it’s nothing fancy. of course you can add whatever you want to on this platform so let’s look at the code instead.
The protocol just checks if a new char has arrived. if there is it sets the state accordingly
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
#include <Adafruit_NeoPixel.h> #define LED_PIN 2 // How many NeoPixels are attached to the Arduino? #define LED_COUNT 10 // Declare our NeoPixel strip object: Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); //define all the game states enum states { off, freezetime, live, bombActive, bombExploded, bombDefused, ctWin, tWin }; states state=off; //start with everything off. the off state don't have any code so nothing will happen here. void setup() { strip.begin(); // INITIALIZE NeoPixel strip object (REQUIRED) strip.show(); // Turn OFF all pixels ASAP strip.setBrightness(250); // Set BRIGHTNESS to about 1/5 (max = 255) Serial.begin(115200); } int bombBeeps = 0; //how many beeps have the bomb emitted int nextBeep = 0; //the interval until next beep should occur unsigned long bombLastBeep=0; //when was the last beep of the bomb? int intensity = 0; //current LED intensity char fadeDir =1; //are we faiding up or down? long firstPixelHue=0; //counting variable for the rainbow function void loop() { //the JSON data is updated quite frequently (every 5 seconds currently) and the switch if therefore run multiple times if(Serial.available()>0) //if something came in over the serial port we better check it out { char recieved = Serial.read(); //get the char switch(recieved) { case 'a': //stop the timer //bombActive=false; state=freezetime; break; case 'b': //turn on the bomb sequencer if(state!=bombActive) //if we're already active don't set the state again { state=bombActive; bombBeeps=0; //we have had 0 beeps so far bombLastBeep=millis(); //set the time of activating the bomb delay(600); //this delay is used to match up the timing with the first beep. blinkRed(); //blink the first time } break; case 'c': if(state!=bombActive) //'live' state will override the timing for the blinks. state=live; break; case 'd': //bomb exploded state=bombExploded; break; case 'e': state=bombDefused; break; case 'f': //CT win ( by time or kills) state=ctWin; break; case 'g': //T win (by kills) state=tWin; break; default: break; } } if(state==bombActive) //if the bomb is active flash red using the same interval as the bomb beeps { // calculate the next beep time nextBeep = (0.13*sq(bombBeeps)-20*bombBeeps+990); //if the bomb beep sequence changes we need to update this polynomial... if((millis()-bombLastBeep)>nextBeep) { bombLastBeep=millis(); blinkRed(); bombBeeps++; } if(bombBeeps>80) //after 80 beeps the bomb is exploding no matter what so we just light up the red LEDs { bombBeeps=0; setColor(strip.Color(255,0,0)); } } //game states. either we're playing (live) or we are in freezetime. the state 'over' will result in a win for one of the teams if(state==freezetime) fadeColor(strip.Color(0,0,intensity)); if(state==live) fadeColor(strip.Color(intensity,intensity,0)); //These are the 4 win states: if(state==bombExploded) //if the bomb exploded, show a nice rainbow. we get this exact state from the game itself { for(int i=0; i<strip.numPixels(); i++) { int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels()); strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue))); } strip.show(); //if we want a slower rainbow cycle we should insert a (small) delay here) firstPixelHue += 256; } if(state==bombDefused) //if the bomb was defused we blink green flashColor(strip.Color(0,255,0),50); if(state==ctWin) //if the CT win we blink blue flashColor(strip.Color(0,0,255),50); if(state==tWin) //if the T win we blink red flashColor(strip.Color(255,0,0),50); } void blinkRed(void) { setColor(strip.Color(255,0,0)); delay(75); setColor(strip.Color(0,0,0)); } //fade a color on the LED strip. Change the limit values and delay to change the fade properties void fadeColor(uint32_t color) { setColor(color); delay(35); if(fadeDir) intensity+=2; else intensity-=2; if(intensity>100) fadeDir=0; if(intensity<40) fadeDir=1; } //Flash a single color. waitingTime sets the time on void flashColor(uint32_t color, int waitingTime) { setColor(color); delay(waitingTime); setColor(strip.Color(0,0,0)); delay(waitingTime); } //fill the entire LED strip with a single color and show it void setColor(uint32_t color) { for(int i=0;i<strip.numPixels(); i++) //fill all pixels with color strip.setPixelColor(i,color); strip.show(); } |
I have tried to condense the code and reuse the different blocks to save a bit of space.
Basically there are 4 ways of winning, 2 for each team
- Terrorists win by setting the bomb and making it explode
- Terrorists win by killing the other team
- Counter-terrorists win by defusing the bomb
- Counter terrorists win by killing the other team.
Each of these wins have a different light effect. The bomb, being the most visual, have a rainbow effect running across the LEDs. the other 3 are simply quick flashes of light either red, green or blue.
Lastly the controller can indicate the freezetime (a slow blue fade) and the ‘live’ time – the actual playing time (a slow yellow fade).
The really fun part was the bomb timing sequence.
Getting the timing right
So how do i synchronize the flashing to the beeps in-game? It turned out to be a bit harder than I thought.
The bomb beeps faster and faster. I started with just using 1 second between each beep and then making it 10ms faster for each beep. that worked but it was clear that the beeping was not linear. Sigh. but here is a challenge!
So to find out what was going on I recorded myself planting the bomb. Standing close to the bomb I could record the sound of the beeps.
This video I imported into Audacity for further analysis.
It’s pretty easy to see what’s going on. first, the ‘bomb has been planted’ voice-over. then a lot of beeps and then an explosion.
I was mostly interested in the beeping part so I removed the other stuff and normalized the beeps
The reason for normalizing the beeps was to utilize the ‘sound finder’ in audacity.
Setting the threshold really high I could make reasonably sure that I would only get the beeps and not any ambient sounds. There really are a lot of animal noises in the game! who whould have thought?
the Sound finder creates a ‘label’ track next to the sound.
The REALLY great thing is that the labels can be exported directly! pretty nifty!
I ended with a text file like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0.010000 0.140000 1 1.000000 1.130000 2 1.950000 2.080000 3 2.880000 3.010000 4 3.810000 3.940000 5 4.690000 4.820000 6 5.560000 5.690000 7 6.420000 6.550000 8 .......................... 32.700000 32.820000 62 32.940000 33.070000 63 33.180000 33.530000 64 33.650000 36.020000 65 |
or rather – a list with the beginning and ending time of each beep. or at least a lot of them.
Next up was importing that list into libreOffice Calc (you can use Excel, that’s fine too!). First I found the time difference between each beep. Next, using the plotting tool I could find the polynomial expression for the time change.
Now, this is in seconds, so I wanted to convert to milliseconds. I multiply by 1000…
It turns out that it’s pretty close to:
0.1x^2-18x+1000
A quick plot on wolframAlpha reveals
Yeah, it’s pretty close. the reason it bottoms out is that I only did have the timing information from the first 65 or so beeps.
Using this formula I can calculate when the next beep should occur and thus when the next blink should be.
Now, action!
Let me just record a video of everything in action!
This was a really fun build. I was intrigued by the way the timer is made and it was great fun to find out the formula. I have only used free tools to solve this, OBS studio for recording, Audacity for audio analysis (FFMPEG plugin as well) and LibreOffice for plotting along with WolframAlpha.
Isn’t it great what can be achieved with free tools?