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.
///////// 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;
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
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.
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.
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.
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.
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.
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.
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.