home

Changing game messages you're not supposed to: exploring a bug with Serious Sam player names

2025 May 18

Intro

In the classic Serious Sam games, "Serious Sam: The First Encounter" and "Serious Sam: The Second Encounter", there are eight player profiles that you may use to play the game with. Think of them as eight different presets that you can use for settings such as name, crosshair type, controls, player model, and so on. The part I want to focus on is the name of the profile, which is mainly used by the game to refer to you. Whenever you die, respawn or kill another player, the game will show a message in the top left corner, something like
Sam blew himself away
An unlisted feature of these games is the ability to decorate your name using effects like color, transparency and flashing. The syntax is as follows: the caret symbol (^), followed by the decoration type (c for color, f for flashing, a for transparency, b for bold, i for italic) and, optionally, the values given to the decoration. You could choose to name yourself ^cff0000^iSam, which would result in your name appearing red and italic, like so
Sam blew himself away
Or, you could be infinitely flamboyant and name yourself [^f32^F^b2^f3^B8] ^f3^cddecffF^cbfdcffo^ca7cefer^c98c6fes^c78b5fea^c63a9fek ^c4397fee^c2587fen^f8^b^cfff977L^cfff546o^cfff30dr^cd2c800d, which would appear in the game as
[228] Forsaken Lord blew himself away
Keen eyes able to decipher that messy name might notice how some decorations don't have a value at all, such at ^b, which makes text bolded. Others, like flashing, take a number from 0 to 9, the higher the number the faster the rate of flashing. It's also possible to prevent a decoration from affecting the remainder of your name by using the uppercase letter of the decoration type. For example, you can make only the first letter of your name red by naming yourself ^cff0000^iS^Cam
Sam blew himself away
What happens if you put a color code after your name, like ^cff0000^iSam^cff0000?
Sam blew himself away
Not much. What about an incomplete code, like ^cff0000^iSam^cffff?
Sam blew himself away
An interesting result. Let's take it a step further and add an unsual character. ^cff0000^iSam^c-f0 gives
Samblew himself away

An even more interesting result: the text became yellow and there's no more space after your name. What does -f0 actually mean? To answer that, we need to understand a few computer concepts. If you're already familiar with computer variable types, binary numbers and hexadecimal numbers, click here to skip to the code explanation, or click here to skip directly to the explanation of the bug if you can read and understand C++ code.

The theory

Variables
You can imagine computer variables as the combination of a box (the value), the shape of the box (the type) and a label (the name). Let's see some examples.
  1. Our first box has the label 'age' on it, is in the shape of an integer, and contains the value 42.
  2. Our second box has the label 'name' on it, is in a long shape that can accommodate a lot of text, and contains the value "Samuelus".
  3. Our third box has the label 'money' on it, is in the shape of a number with decimals, and contains the value 1.618033.
  4. Our last box has the label 'bankAccount' on it, has the shape of a piece of paper, and contains written instructions on how to find the 'money' box.
If we were to rewrite these boxes as lines of code, they would look like this:

  /////////   Shape    Label       Box contents
  /*Box 1*/   int      age         = 42;
  /*Box 2*/   string   name        = "Samuelus";
  /*Box 3*/   float    money       = 1.618033;
  /*Box 4*/   float*   bankAccount = &money;

Binary numbers

Computers, like the one you're using to read this, work with something called binary numbers. Ones and zeroes. So very many of them. They're also very good at fooling you into thinking that they're not doing this. After all, you don't see ones and zeroes on your screen, you see programs, moving objects, colors, regular text. But that's the magic of computers. Would you like to see some binary numbers? Sure, here you go:
01101000 01110100 01110100 01110000 01110011 00111010 00101111 00101111 01111001 01101111 01110101 01110100 01110101 00101110 01100010 01100101 00101111 00101101 01110000 01110010 00101101 01010111 01010101 01100001 00111000 01100101 01000101 01110011

You might be wondering how to make sense of that wall of digits. Let's start with base 10, which is one of mankind's premier achievements. Also known as decimal numbers, you don't often find something that the vast majority of people on Earth agree with, like they did with how to represent quantities. It's called base 10 because it uses ten unique symbols that I'm sure you're already familiar with: 0 1 2 3 4 5 6 7 8 9. Now consider this way of representing 1239:
1239
= 1000 + 200 + 30 + 9
= 1 × 1000 + 2 × 100 + 3 × 10 + 9 × 1
= 1 × 103 + 2 × 102 + 3 × 101 + 9 × 100
Observe how a number in base 10 can be represented as a sum of multiples of powers of 10. Here are a couple other examples:
255
= 200 + 50 + 5
= 2 × 102 + 5 × 10 1 + 5 × 100

3
= 3 × 100

This needlessly complicated mathematical representation is key to understanding binary. It is base 2, so it only uses 0 and 1. If you want to represent a number, you're gonna have to use powers of two. Using 3 as an example again:
3
= 2 + 1
= 1 × 21 + 1 × 20
= 0b11

From now on, I'll prefix numbers written in binary with 0b, for clarity. This prefix is also a wide-spread way of indicating binary numbers in various programming languages. Below are a couple more examples:
21
= 16 + 4 + 1
= 1 × 24 + 0 × 23 + 1 × 22 + 0 × 21 + 1 × 20
= 0b10101

255
= 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1
= 1 × 27 + 1 × 26 + 1 × 25 + 1 × 24 + 1 × 23 + 1 × 22 + 1 × 21 + 1 × 20
= 0b11111111

Each symbol in a base 10 number is called a digit - 1239 has four digits. Each symbol in a base 2 number is called a bit - 0b10101 has five bits. 0b11111111 has eight bits. Eight bits are also called a byte. From there, you have kilobytes, megabytes and gigabytes, which are often referred to when talking about how much space a program takes or how much storage capacity a computer has. So when there's a game that takes, let's say 300 gigabytes, then that means it needs 300,000,000,000 × 8 bits, which is 2.4 trillion, or 2,400,000,000,000, bits.

Hex numbers

Another remarkable number base in the world of computers is 16, also referred to as hexadecimal numbers, or simply hex. In addition to the existing 0 through 9 digits, base 16 uses A B C D E and F to mean 10 11 12 13 14 and 15, respectively. The prefix for hex numbers is 0x. However, the same rules with the powers apply. We can use them to figure out what, for example, 0x21 is in decimal.
0x21
= 2 × 161 + 1 × 160
= 32 + 1
= 33

Or something like 0x8D:
0x8D
= 8 × 161 + 13 × 160
= 128 + 13
= 141

Because a single hex digit can represent up to sixteen values, a computer would need four bits to store it. If we consider the maximum value of a hex digit, 0xF, we can see that it's equal to 15, which in turn equals 0b1111. If we pair up two hex digits together, we can go up to 0xFF, or 0b11111111, which takes up eight bits. And as we've learned, that's a byte. In general, hex numbers are a more human-friendly way of dealing with binary numbers, particularly because 16 is a power of 2. Humans tend to work in base 10 presumably because of our ten fingers, but you have to remember that computers, deep down, are just moving ones and zeroes around. And 10 is not a power of 2.

One practical example of where bytes are used is the RGB color system. You might have seen that if you've ever used the color picker in microsoft paint, or in general, dealt with colors on a computer. In the bottom right corner of the window, the red, green and blue colors will have values between 0 and 255. In the picture below, we have a combination of Red 255, Green 128 and Blue 64. When combined, they result in a nice orange. Another way to write this combination is in hex. 255 = 0xFF, 128 = 0x80, 64 = 0x40. If you put them all together, the hex code for the nice orange is 0xFF8040.

The color picker in paint

The code

With these concepts under our belt, we can understand how Serious Sam handles text decorations! By the way, the game's engine is freely available on GitHub.

The first hint comes from the output window of visual studio (which is a complicated text editor that can also run code). Whenever a message gets printed in the game's console, a 'copy' appears in its output window too.
^o^cff0000Sam^r blew himself away

There are two new decoration types that the game doesn't show to the player. 'o' is not relevant to us, but 'r' is supposed to reset all previous decorations. There's something about -f0 that makes this reset code not work.

Inside DrawPort.cpp, there's a function called PutText which, as the name might imply, puts text on the screen. The text can be console messages, the player list or ammo. Here's a trimmed down version of it that only contains code relevant to our bug.

  
  else if(chrCurrent=='^') {
    chrCurrent = strText[++iChar];
    COLOR col;
    switch(chrCurrent)
    {
    // ...
    case 'c':
      // ...
      strncpy(acTmp, &strText[iChar+1], 6);
      col=strtoul( acTmp, &pcDummy, 16) << 8;
      glcol.Set( col|glcol.a);
      continue;
    }
    // ...
  }
  

In plain english, whenever this code encounters the caret, it expects that the next character will be a type of decoration. Then, it looks at what kind of decoration type we have. In case it is c, it will copy the next six characters after c, turn them into a color code, and apply that code to the text. Let's run the code line by line with the simple message, ^o^cff0000Sam^r blew himself away.


  else if(chrCurrent=='^') {

chrCurrent is a variable of type char (stands for character) that can have a single character as a value. It is used to iterate through the entire string that we want to put on the screen, one character as a time. If the character is ^, that means it expects the following character in the string to be a decoration type. So it must run the code between the matching {} curly brackets pair to find out which decoration type it's dealing with.


  chrCurrent = strText[++iChar];

strText is a variable of type string that contains the value "^o^cff0000Sam^r blew himself away". iChar is a variable of type integer that helps with iterating through the string. The string "^o^cff0000Sam^r blew himself away" contains thirty three characters (the quotation marks do not count and are merely convention when representing strings). And since computers start counting from 0, when iChar has the value 0, then it means we're at the first caret symbol, the first character. Value 1 means we've reached the second character, o. Value 2 means we've reached the third character, which is the second caret. Value 3 means we've reached the fourth character, which is the c. And so on. It can go up to 33 (technically 34, look up the C string terminator if you want to know why). You can think of it as counting how many characters of the string we've handled so far - if iChar is 0, we have handled no characters yet, so we're at the beginning. Since iChar is of an integer type, it cannot actually know what character is at the index it contains. That's why chrCurrent (of type char!) also exists. strText[++iChar] does two things. First, ++iChar increments iChar by one. From 2, it becomes 3, the same position as the c. Second, the [] brackets after strText mean to get the character from strText at the position indicated by iChar. Since iChar just turned 3 (happy birthday!), that means we're talking about the c character. Finally, assign the c character to chrCurrent.


  COLOR col;

A variable named col, of type COLOR is defined. It has no value yet. The COLOR type is actually defined elsewhere in the source code as an alias for ULONG - unsigned long. Long means a 32 bit, or 4 byte, integer. Unsigned means that only positive values can be used.


  switch(chrCurrent)
  {
    case 'c':

Here are the lines that define the behavior for each decoration type. switch looks at the value of chrCurrent, and then executes the code found in the appropriate case. For us, chrCurrent has the value c, so the code in case 'c' will be ran.


  strncpy(acTmp, &strText[iChar+1], 6);

acTmp is an array of chars, which is not exactly a string but it serves more or less the same purpose for us, so you can think of it as a string. It exists because a function in the next line of code only accepts arrays of chars. But we'll get there in a bit. strncpy is a function that copies a specific amount of character from a string into a variable. In our case, we copy six characters from strText into acTmp, starting from the position one higher than what iChar contains. iChar has the value 3 (which means fourth position), so we should start copying from the fifth position. Remember, our string is "^o^cff0000Sam^r blew himself away" and quotation marks don't count, so the fifth position is the first f. And six characters starting from the first f means the ff0000 color code. After this line of code finishes, acTmp will contain the value "ff0000".


  col = strtoul(acTmp, &pcDummy, 16) << 8;

The star of this line is strtoul, whose name is the unfortunate result of a programmer attempting to speak English. The name could be expanded to "string to unsigned long". It takes the value acTmp, which is the string "ff0000" and tries to convert it to an unsigned long value. The value 16 represent the base in which we expect the number to be. Since we're using the RGB color system, we're indeed expecting the value of acTmp to be in base 16. The result of strtoul is 0x00FF0000, or 0b00000000_11111111_00000000_00000000 (underscores added for readability), or 16711680. Then, << 8 shifts the bits of the result 8 positions to the left, giving us 0b11111111_00000000_00000000_00000000, or 4278190080. This shifting is done to also make space for the alpha value, which dictates transparency. For the purposes of our bug, we can ignore it. Finally, 4278190080 is assigned to col.


  glcol.Set(col|glcol.a);
  continue;

At the end, the color value we provided and the pre-existing alpha value are combined, and the effect is applied to the text. Remember, when we input the color code, we only provide 3 bytes of information, the RGB values. But the engine expects four bytes. Assuming the alpha value is also 255, or 0b00000000_00000000_00000000_11111111 (i.e., fully visible), then the final value that will be applied will be 0b11111111_00000000_00000000_11111111, or 0xFF0000FF. In other words, completely red with no green and blue, and full visible. The last line of code is continue;, which in this context means to continue iterating through the string, since we're done processing the color decoration.

The bug

Now that we understand what happens normally, let's explore how -f0 throws a wrench into this flow. Once again, have a look at the whole picture, keeping in mind that we are now processing the string "^cff0000^iSam^c-f0^r blew himself away". And we're interested in finding out how it produces this outcome
Samblew himself away
, where there's no space after the name and the text produced by the game is also yellow.

  else if(chrCurrent=='^') {
    chrCurrent = strText[++iChar];
    COLOR col;
    switch(chrCurrent)
    {
      // ...
      case 'c':
      // ...
      strncpy(acTmp, &strText[iChar+1], 6);
      col=strtoul(acTmp, &pcDummy, 16) << 8;
      glcol.Set(col|glcol.a);
      continue;
    }
    // ...
  }

The bug begins with strncpy. Since it copies six characters after the c, we end up with acTmp containing "-f0^r " when handling the color decoration after the name. Yes, also the space! But this string contains non-hex characters, so how come the game doesn't crash or spontaneously combust from trying to interpret what ^r might be in base 16? It's because the function is a bit smart, and simply stops processing the string as soon as it encounters an unexpected character. However, it doesn't get tripped up by the minus sign, as otherwise the game message wouldn't be colored at all.

The minus sign gets considered as part of a negative number, meaning that stroul will take in the string "-f0", or -0xF0, or -240. Negative numbers in C++ are represented in their two's complement form. Without getting into a long-winded explanation about what that is, just know that -240 is represented as 0b11111111_11111111_11111111_00010000. Since the result of stroul must be unsigned, then that binary value is interpreted as a positive number: 4294967056 (0xFFFFFF10). Shifted by 8 bits to the left, we get our fresh, new RGB values: 0b11111111_11111111_00010000_00000000, or 0xFFFF10000, or Red 255 Green 255 Blue 16, resulting in the yellow color found after the name in the game message
Samblew himself away
. Since both the space between the name and the game message, and the reset code, became unwitting victims of stroul, we now know why we have power over the game's messages.

How far does our influence reach? It depends on what kind of limitation you're willing to put up with. Such power does not come for free, after all. We have a few options on how to bypass the reset decoration: An incomplete color code, an incomplete transparency code, an incomplete flashing code, or a stray caret after your name.

The incomplete color code is inherently limited to at most four out of the six possible hex values, by virtue of needing to include ^r. In practice, it means that the range of possible color codes is from -0xFFF to 0xFFFF. This gradient shows you what colors you have at your disposal, and roughly their distribution.

Another way to visualize this range is with the slider below, which starts from -0xFFF (-4095) and ends with 0xFFFF (65535). You may manually drag it, but due to space constraints, it will be a bit imprecise. To help with that, I've included a few playback buttons that can control the slider head. Feel free to play with them.

Time between color codes: ms
Current slider value:

In game message preview: Sam blew himself away

Color code: ^c

RGB values: R= G= B=

Color preview
Binary: 0b
The other ways to subjugate the reset code involve forsaking the integrity of your name. But they are capable of decorating the text after your name in any color you wish. An incomplete flashing code will append an r to your name and cause the text after your name to flash at the highest possible level. ^cf98800Sam^f would give you Samr blew himself away. ^cf98800Sam^a would make everything after your name invisible, since the caret is interpreted as 0 alpha: Sam. This affects your chat messages as well, so you're essentially muting yourself. The source code dealing with the alpha value also uses strtoul, so the non-hex character '^' is treated as 0. Finally, a stray caret, as seen in ^cf98800Sam^, would give you Sam^r blew himself away. This has, by far, the most amount of flexibility, but you'll have to deal with your name now containing a neutered reset code that everyone can see.

Conclusion, personal note

The motivation behind this analysis comes from back in the day when I was playing multiplayer against a guy who had cyan text after his name, and did not respond to my curious inquiries before leaving the server. I even asked around in forums if anyone knew about this feature. The only person who ever responded did not really grasp my question. Decorating your name was well documented, yet there seemed to be nothing online about decorating the text after your name. It continued to bother me long after I stopped playing the game, and as time went on, it receded into the depths of my memories. Upon seeing that the engine code was published, it was the spark needed to burn my frustration from yonder and transform it into curiosity. A few hours of debugging the game later, the secrets of the decoration code were unveiled, and I even learned something about how C++ handles unsigned numbers.