Text manipulation in Quake (Part V): coroutines

Solutions

As usual we start with solutions to the questions from last time. 1) was a rare “non-coding” question, and so has an answer rather than a solution. Our cursor code ignores characters sent to it once the count has exceeded the limit. It doesn’t distinguish between characters sent by a T_ function and the 0 we send it directly, so the 0 doesn’t get printed and the centerprint goes unterminated. This leads at best to extra junk printed on the screen and much more likely a crash.

2) is fairly straight-forward, replace the return statement in S_Print_Cursor with WriteByte (MSG_ONE, ' ');

For 3), you needed to spot that we actually only cared about the difference between S_Print_Cursor_Limit and S_Print_Cursor_Count. So we can set the count to the number of characters we want to print, and decrease it each time we print one. When it’s below 1 print the cursor, and below 0 skip the character.

Coroutines

In part III I introduced an idiom that we should close our centerprint with a 0 call to the S_ function, without explanation. The pay-off comes today, but first we need to think about the answer to Q 1) above and fix S_Print_Cursor. Start with the S_Print_Cursor code from last time (so without any homework changes).

float S_Print_Cursor_Limit;
float S_Print_Cursor_Count;
void(float c) S_Print_Cursor =
{
	S_Print_Cursor_Count = S_Print_Cursor_Count + 1;
	if(S_Print_Cursor_Count > S_Print_Cursor_Limit)
		return;
	else if(S_Print_Cursor_Limit-S_Print_Cursor_Count<1)
		WriteByte (MSG_ONE, 11);
	else
		WriteByte (MSG_ONE, c);
}

We can add an extra block of code to the top to handle incoming 0 characters before the cursor

if(c==0)
{
	WriteByte (MSG_ONE, 0);
	return;
}

Now we’ve got a block of code that runs every time a centerprint is closed. We can be clever, and use that to reset our character count! Add S_Print_Cursor_Count = 0; before the return, and now we don’t have to reset it in PlayerPostThink. Our code became a bit more self contained. If you imagine how the repeated calls to S_Print_Cursor work, we get a loop and then some extra code once the loop terminates – like a bigger function but spread out over several calls. This brings us onto the topic of the day – coroutines (skip 4 paragraphs if you dislike theory).

In QuakeC our functions are subroutines – we enter them at the top, run through until we hit a return (or the bottom of the function) and then they are over. All the local variables from the subroutine are wiped clean before the function runs again. We in turn can launch other subroutines, but we know that when control returns to us, they are finished and forgotten, with perhaps just a return value to show for it. Coroutines are a generalisation of subroutines: we can move between two coroutines and both will remember their state.

Imagine we are writing a web browser, and we have a “producer” and a “consumer” working together. The “producer” is listening for data as it comes in from our internet connection and decodes it packet by packet. The “consumer” is taking the decoded data and turning it into a web page on the screen. Both the producer and consumer need to keep track of their current state.

This system can be expressed well as a pair of coroutines. When the producer has decoded some data, it can yield it to the consumer. When the consumer has finished processing the data, it yields control back to the producer. In all this back and forth, both coroutines remember exactly how far they have got through their tasks. Part of the value of having separate coroutines is that we could have several different producers (perhaps different connections, http vs https etc.) and different consumers (maybe for different filetypes)

Coroutines in C is a good article to read more about the concept. It explains the idea using actual C code, and also describes how you might get something that looks like a coroutine.  Oh wait, that sounds a bit ominous! Yes,  C doesn’t directly support coroutines, and so it’ll come as no surprise to learn that QuakeC doesn’t either. The most useful bit of technique we will steal from that article is to have a progress tracking state variable, which is used to jump to the correct part of the coroutine.

OK, welcome back to the practical people. One of the unsightly parts of our code to date is the boilerplate stuff we have to put in along the lines of:

 msg_entity = self;
 WriteByte(MSG_ONE, SVC_CENTERPRINT);

It isn’t so obvious in our test examples, but in real code we would only write the S_ functions once, then call them with various bits of text in many different places in the code. So having to include this bit of code everywhere first would become tiresome. We can modify S_Print_Cursor to do this part for us at the start of the message, in the same way we got it to reset the counter at the end of the message.

How can we detect when we are at the start of the message? Well, as luck would have it that’s very easy in the case of S_Print_Cursor, since S_Print_Cursor_Count will equal zero then! So we can add the following code to the very top of the function:

if(S_Print_Cursor_Count == 0)
{
	msg_entity = self;
	WriteByte(MSG_ONE, SVC_CENTERPRINT);
}

Now we can (and in fact must) simplify our calling code in PlayerPostThink to:

S_Print_Cursor_Limit = time * 6 - 12;
T_Location(S_Print_Cursor);
S_Print_Cursor(0);

Tradeoffs

We’re actually making a couple of decisions here that are worth examining. The first is that our S_ function is now locked into performing a centerprint. These articles didn’t explore it, but by sending a different SVC_ number, we could create sprint or bprint messages, or even send commands to the console. It was neat that we could use our S_ functions for all of these targets interchangably, but creating a specific S_ functions is not so bad either.

The second is that our S_ function now always prints to self, when before we had the choice. I think I can argue in favour of this one: Centerprints have to go to players, so almost always self will be the correct entity to send messages to. The small number of cases where we need to do oldself = self; self = other; type stuff are outweighed by the sensible default.

What if we tried to do the same to one of our earlier functions, like S_Print_Red? Well, we wouldn’t have a counter to check if the message had begun yet, so we would need to create a new state variable like
float S_Print_Red_Active.
In our “message begins” section of code, we would set
S_Print_Red_Active = TRUE;
and in our “message ends” segment set it back to FALSE. A bit messier, but still workable.

Homework

Just a single question to ponder today. Suppose our code prints T_Location with S_Print_Red and then T_Date with S_Print. What goes wrong if we try to update both of these functions to be coroutines? Is there an easy way round this?

Advertisements

One thought on “Text manipulation in Quake (Part V): coroutines

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