Text manipulation in Quake (part II): Inversion of Control

Solutions

Solutions to homework from last time

1) You should have ended up with something like:

local float i;
msg_entity = self;
WriteByte (MSG_ONE, SVC_CENTERPRINT);
WriteByte (MSG_ONE, '[');
for(i = 0; i < self.health; i = i + 1)
{
 WriteByte (MSG_ONE, '.');
}
WriteByte (MSG_ONE, ']');
WriteByte (MSG_ONE, 0);

This was meant to be the question that checked you stayed awake down to the end of the post : – )

2) This one might have been a bit tricky since QuakeC doesn’t have a modulus operator – the natural thing to do is test whether i divides by 32. We can exploit bitfields since 32 is a power of 2, and calculate i & 31 in place of i % 32. The inner loop I came up with looks like:

for(i = 0; i < self.health; i = i + 1)
{
 WriteByte (MSG_ONE, '.');
 if(i & 31)
  WriteByte (MSG_ONE, 10);
}

3) Again we modify inside the loop, testing the value of i. The intended challenge here was to apply the information about where the red characters are in the quake character set. My solution was to add 128 to the character constant ‘.’ and let the compiler do the maths, but full credit if you calculated the red dot value yourself. The inner loop ends up like:

for(i = 0; i < self.health; i = i + 1)
{
 if(i < 100)
  WriteByte (MSG_ONE, '.');
 else
  WriteByte (MSG_ONE, '.' + 128);
 if(i & 31 == 31)
  WriteByte (MSG_ONE, 10);
}

4) Here we combine the trick from 2) for i % 32 with a new idea of continuing to use the value of i after the for loop has completed. Insert the following code after the for loop, but before the terminating zero-byte has been sent:

while(i & 31 != 0)
{
 WriteByte(MSG_ONE, ' ');
 i = i + 1;
}

inversion of control

So, on with the new stuff. In the last post we saw the power of WriteByte. It let us send text to the player that was dynamic – which changed as your health went up and down. However, the text itself was just a boring pile of dots. The next challenge is to start sending something more substantial to the screen, and to do that we have to struggle with some QuakeC limits – in particular what to do without arrays. If you want to jump straight to the practical solution without reading the theory, you need to head about 6 paragraphs south from here…

Imagine that we want to take a text, transform it so that it uses the red set of characters, and then print it to the screen. If we had an array to use, then our code might go something like:

  1. Retrieve an array containing the text
  2. Take the first character from the array
  3. Convert the current character to the corresponding red character
  4. Print that to the screen
  5. Take the next character from the array
  6. Return to 3 unless the array is empty

Although we don’t have an array in quake, imagine that we could write a function which acted like an array – we call it with an index and it returns that character from the string. Our code would end up looking like

We decide we want to print a red string

We start a loop for the length of the string

We ask our array-function for the nth character

We take the returned character and make it red

We print it to the screen

We loop round again

We finish printing to the screen with a null character

Here the indents are meant to indicate function calls/nested loops – so asking the array-function for a value is the deepest call. This works more or less like the array version. The problem is that QuakeC isn’t any good at writing that kind of array replacement function either! Grabbing a particular element from a list given an index can’t be coded in a way that’s efficient and scalable. Still, viewing the problem in this way leads us to a solution.

The closest thing to an arbitrary sized array in QuakeC is a function itself; you can make a function as long as it needs to be, and it executes in sequence. You can’t get random access, but you can get iteration. To take advantage of this we will use a technique called Inversion of Control. You don’t have to read that linked article to understand what we’re doing today, but it is interesting reading for general programming, and it’s nice to know there’s some underpinning to the plan.

Inversion of Control here means swapping the bit of code that retrieves a record from the array with the bit of code that acts on the record. That’s a bit vague, let’s illustrate it with the example above:

We decide we want to print a red string

We ask the array-function to run the print_red_character function on all of its characters

The array function runs as far as getting its first character…

and calls print_red_character on that character

Then runs a bit further to get the second character…

and calls print_red_character on that character

Repeat to the end of the array

We finish printing to the screen with a null character

The array-function now passes the character as a parameter to an inner function, rather than returning it to an outer function. Notice how the array-function doesn’t need a method to get character n from the array, it just spits them out in the order built into the function. The inversion of control pattern lets us avoid the bit which QuakeC is no good at.

In practice

That’s enough theory, it’s time to get stuck into the practical side. The most difficult bit is the format of the array-function, we need it to be able to send it a command which it will perform on every element. This means we need to use a function pointer, something scarcely used in the standard QuakeC code. First let’s remind ourselves how function pointers work with an existing example. In defs.qc you can find the following definition:

void(vector tdest, float tspeed, void() func) SUB_CalcMove;

We are interested in the third parameter to the function, which is of type void(). This means it’s a function pointer – in particular to a function which takes no parameters and which returns nothing. Function pointer sounds a bit technical and scary, but if we look at a line of code which uses this function:

SUB_CalcMove (self.pos1, self.speed, button_done);

We can see that all you need to do is use the name of the function to supply a “function pointer”. Grab a copy of the clean quake source, and in defs.qc make the same changes as last time so that SVC_CENTERPRINT is defined. Then just above PlayerPostThink in client.qc, start writing our new function with

void( void(float c) out) T_Location =

Like with SUB_CalcMove, the definition includes a function pointer parameter. Notice that this time the signature of the function pointer is slightly different – void(float c) out means that out must be a function with a single float parameter. This is a really confusing idea here – we’re talking about types of function parameters inside the type of another function parameter. Don’t worry if this doesn’t click – revisit it once you’ve see the whole picture. For now it’s just the pattern that makes all our T_ functions work (aside: T_ stands for Text).

The body of T_Location looks like:

{
 out('M');
 out('o');
 out('j');
 out('a');
 out('v');
 out('e');
 out(' ');
 out('D');
 out('e');
 out('s');
 out('e');
 out('r');
 out('t');
 out(',');
 out(' ');
 out('C');
 out('A');
 out(' ');
 out('1');
 out('6');
 out(':');
 out('3');
 out('0');
}

That’s a lot of repetition, but it’s the nicest way we can make a sequence in QuakeC. What this does is calls the out function for each of the letters in “Mojave Desert, CA 16:30”. That’s the more difficult half of the code done, now we just need a printing function. Above our new function add the following:

void(float c) S_Print =
{
 WriteByte (MSG_ONE, c);
}

This very simple function just writes any character you send it, and nothing else. Can you guess how we use it? Well, let’s go down to the bottom of PlayerPostThink and put it to work with the following code:

msg_entity = self;
WriteByte(MSG_ONE, SVC_CENTERPRINT);
T_Location(S_Print);
S_Print(0);

We recognise the first two lines from part I, which initiates the centerprint. Then we call T_Location, passing S_Print to it. The last line is a bit different from before – we used to have WriteByte(MSG_ONE, 0); there, but if you look at the code for S_Print you can see this achieves the same thing. This change will pay off in future posts. If you compile everything up and run it, you’ll get “Mojave Desert, CA 16:30” printed on screen. So far, only as good as the builtin centerprint. Time for a better S_ function.

void(float c) S_Print_Red =
{
	if(c > ' ' && c < 128)
		c = c + 128;
	WriteByte (MSG_ONE, c);
}

Change the lines in PlayerPreThink to use S_Print_Red in place of S_Print. Go back in game, and the text is now red! This post is already quite long, so we’ll leave it there. Next time we’ll create some S_ functions with parameters, and animate them to show off the real potential of the set-up.

CONCHARSHomework

1) Create S_Print_Goldnum, which maps the white digits 0-9 to their gold counterparts (see image on the right to work out the values)

2) Create S_Print_Caps, a function which capitalizes all lowercase letters in white and red text. (Yes, there’s not much difference in the quake font).

3) Create S_Print_Spaced, a function which  s p a c e s  o u t  t e x t.

4) Create T_Date, a text-array which contains the phrase “April 7th 2065”. Print this text on the line below T_Location.

Advertisements

9 thoughts on “Text manipulation in Quake (part II): Inversion of Control

  1. Modified your loop a little to make a neat 02 meter, but I wanted the centerprint to be (4) lines lower than the normal starting centerprint default, and I put in (4) calls to Quake charset character #10 which I thught was cr/lf, but for some reason its setting the print (4) units higher?

    // Cobalt O2 meter in watermove

    if (((self.waterlevel > 2) && (self.health > 0) && (!self.items & IT_INVULNERABILITY) ))
    {
    if (self.air_finished > time)
    {
    local float i;

    msg_entity = self;

    WriteByte (MSG_ONE, 26);
    WriteByte (MSG_ONE, 10);
    WriteByte (MSG_ONE, 10);
    WriteByte (MSG_ONE, 10);
    WriteByte (MSG_ONE, 10);
    WriteByte (MSG_ONE, 79);
    WriteByte (MSG_ONE, 88);
    WriteByte (MSG_ONE, 89);
    WriteByte (MSG_ONE, 71);
    WriteByte (MSG_ONE, 69);
    WriteByte (MSG_ONE, 78);
    WriteByte (MSG_ONE, 10);

    for(i = 0; i < ((self.air_finished – time) – 1); i = i + 1)
    {
    WriteByte (MSG_ONE, 139);
    }

    WriteByte (MSG_ONE, 0);
    }

    }

  2. Hi Cobalt, what engine are you using? This code works fine in fitzquake085 and does what you expected it to.

    Fun bonus task: Make the “OXYGEN” text turn red when the meter is low

    • Try running your mod in a different engine and you’ll see it work correctly. Darkplaces appears to have a non-standard feature where a centerprint more than 4 lines long are moved to a different part of the screen. You’d have to ask LordHavok if there’s a way to disable that.

  3. Hrm, well the progs.dat is running on a Manquake engine which is a Runequake dedicated type server environment based on Proquake. So I guess technically the server is Proquake derived. Seems not to matter what client I connect with, the lines are always (4) units higher not lower.

    I thought perhaps char #10 is a reverse linefeed, and there is another which is a normal lf.

    I had modified a reference img here on my site [ http://www.tekbotctf.com/quakefont.gif ]
    which indicated char pos 10 as the lf, but if you notice there are other “empty” non referenced numbers. Presumably one or more is just a space char, but during my tests, one is actually a quote char (“). Could not find any on the net that explain all the empty references, but perhaps as you noticed they are modified depending on the engine.

    • 10 is the line feed, if you try printing a single 65 (Capital A) before all the 10s to test you should see that they’re working, and that the meter is several lines below the top of the printing, just that the top point that you print to has mopved up.

      I really must recommend again that you try different engines, don’t use a client-server arrangement as that’s got twice as many variables to worry about. Try using vanilla glquake in single player and you should find it works. If not, let me know, but if so then it’s a matter of working around incompatible engines.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s