Archive for August, 2010


From here to there

I’ve spoken to a few coders/owners of talkers privately this week, and its clear that the majority of you want “inline cname”. Whats been interesting about those conversations is the questions that you want covered. For most of you its “will it be easy to install and when can we download it?”, although one or two of you are interested in the “how is it done?”, “why is Spot’s version slower than Lokie’s?”, “Why doesn’t Silvers version work?”.

The first question is really easy to answer, it’ll be part of the big PGPlus patch/PGPlus Download I’ll put on the web once this is all done. If theres enough demand I’ll also split it out into a separate download.

Now for the fun technical stuff. Theres basically 3 ways to do inline coloured names. The first is really easy, and is the way most coders do it their first time. You start by installing Silvers cname code and placing nameof(p) in a few hundred places. Then you find the swear filter (its in parse.c) and a quick copy and paste drop that into some part of the code path and jobs done. Historically Trekkers Rest did it that way after a certain coder got drunk and was convinced having colours everywhere was a good idea (I’ve since sobered up!).

Now I hated inline cname so we let a year pass before I realise we can never get rid of that damn “”feature”” and coder pride means its time to do it properly. You can remove all of the nameof(p) calls, the hotwired swear filter, and replace it all with a simple check in process_output. For those of you that read the pstack_mid article you can see how a simple piece of state information allows you to ensure the correct colour after a replacement. You can take this even further by completely removing the default swear filter and moving that into process_output, and of course if you read the pstack_mid article theres a really great clue in how to stop people beating it with the old sh^Nit trick. For free you get to fix the swear filter, have a simple way of allowing users to switch on or off the swear and name filtering. Historically Trekkers Rest ended up doing it that way and TrueFriends when they acquired the code started by doing it that way.

The third way requires a little sideways thinking. lets forget the second way of doing things for a moment and look again at the first. Now I mentioned you could hotwire the old swear filter to do this job, I didn’t mention its a real pain if you leave it in the same place as the swear filter to allow users that hate the feature the ability to turn it off. But if you move it far enough down you can remove all the work that Silvers code requires and get an easy on/off switch. The logical place for that is in the tellplayer() function. Forgetting things like the emergency log and tell history… that would give you a tellplayer function that looks something like this in pcode:
void tellplayer(p, str)
{
char *tstr;
tstr = process_dynatext(p, str);
tstr = process_cname(p, tstr);
tstr = process_output(p, tstr);
write_socket(p, tstr); /* yes I know process_output returns a file object, but lets keep things simple! */
}

I suppose we could waste a paragraph talking about some obvious optimisations. Instead of a players name, dynatext should use a players cname. Cname could precompute the client colour codes and save some work in process_output, and so forth….

Finished thinking about cool optimisations and how to save work?

Okay back to the present… no matter how good your optimisations are, method 2 will take 2/3rds the time of method 3. What we have is 3 linear search and replace routines each with their own specialised domain knowledge. Dynatext looks for sequences beginning with ‘<‘, cname/swear looks for words, process_output looks for ‘^’ and ‘\n’. From a holistic point of view they are all the same code!

The third method is about exploiting the fact that what we’re about to send to the user is really a sequence of tokens that require processing, and that instead of having 3 specialised pieces of code to handle that processing we can replace it with a single generalised piece of code. Not only is that far quicker and more memory efficient, but should we decide to add additional tokens (such as smileys, more colour codes, inline images, sound, non telnet support) we can deal with them in the same place.

And Truefriends uses an output system based on those ideas, oh and I renamed ‘cname’ to ‘filtered output code’ as cname no longer seemed appropriate.

Whilst most of you want me to now post a download link, my aims are somewhat different. I want people to understand the code, or at least the rationale behind some of the decisions that shaped it. And so this post is about saying “hey this is why my code performs better than someone else’s, this is the secret sauce I used to solve this problem in what I hope is the best way” and hopefully some of you will go off and experiment. The rest, well I’ve got a few cool projects between here and there that will not only improve your talker, but hopefully give you some further insights ready for when we tackle the feature most users instantly notice.

Advertisements

Stuck in the middle with you

After a few days of tidying up code its time to get back to the job of improving things. Today we’ll tackle a nice simple job, pstack_mid.

To begin with we’ll create a variable argument version of that function, mainly because it annoys me there isn’t one:

void PSTACK_MID(const char *format, ...)
{
char *temp;
va_list argum;va_start(argum, format);
vsprintf(stack, format, argum);
va_end(argum);temp = strdup(stack);
pstack_mid(temp);
if (temp)
FREE(temp);
}

So a pretty standard function, with the only talking point being that I don’t check strdup returned a sane value until its time to tidy up.

The current pstack_mid is a little dumb when it comes to handling strings with colour codes, so we’ll fix that, and give it a slight performance boost as well.


extern int valid_char_col(char);void pstack_mid(char *str)
{
int width, half, len;
char *line_pos, *str_pos, cur_col[1], last_col[1];/* if str is empty or non existant, just print a line */
if (!str || !*str)
{
stack = stpcpy(stack, LINE "\n");
return;
}

Thats why we didn’t handle strdup in PSTACK_MID, we’ll handle it in the real function and save duplicating some code.


/* we want the length of str without counting the colour codes */
str_pos = str;
len = 0;
*last_col = 'N';
while (*str_pos)
{
/* check if we have a colour code */
if (*str_pos == '^')
{
if (*(str_pos + 1) == 'N' || *(str_pos + 1) == 'n' || valid_char_col(*(str_pos + 1)))
{
*last_col = *(str_pos + 1);
str_pos += 2;
continue;
}
/* ^^ renders as ^ so check for that case */
else if (*(str_pos + 1) == '^')
str_pos++;
}
/* if we're here count the character */
len++;
str_pos++;
}

The new pstack_mid is really just 3 more copy and pastes of that loop.


/* try and get the current line width */
if (current_player)
width = current_player->term_width;
else
width = 76;
/* ensure width is always long enough to hold str. We'll let process_output
handle getting the line wrap right */
width = width * ((len + 2) / width) + 1);

The original code has a slight flaw. If the length of the string being centered is the same as the line width, it’ll crash the talker. the original code was a little silly when str was larger than width, so we solve both those problems by figuring out the number of lines we need to hold the str and sizing width accordingly.


/* copy the prefix */
half = (width - (len + 2)) / 2;
*cur_col = 'N';
line_pos = LINE;
while (half)
{
/* check to see if we're on a colour code */
if (*line_pos == '^')
{
if (*(line_pos + 1) == 'N' || *(line_pos + 1) == 'n' || valid_char_col(*(line_pos + 1)))
{
*cur_col = *(line_pos + 1);
line_pos += 2;
*stack++ = '^';
*stack++ = *cur_col;
if (!*line_pos)
line_pos = LINE;
}
/* ^^ renders as ^ so check for that case */
else if (*(line_pos + 1) == '^')
line_pos++;
}
*stack++ = *line_pos++;
half--;
if (!*line_pos)
line_pos = LINE;
}

Very similar to our string counting loop. Only real important thing to note is that we don’t use an external function to keep track of where we are in the LINE constant.


/* now to copy the str... */
*stack++ = ' ';
stack = stpcpy(stack, str);

We could have used the same loop to copy str into place, but this way is potentially faster.


/* we need to sync our positions so a quick loop coming up */
half = len + 2;
while (half)
{
/* check to see if we're on a colour code */
if (*line_pos == '^')
{
if (*(line_pos + 1) == 'N' || *(line_pos + 1) == 'n' || valid_char_col(*(line_pos + 1)))
{
*cur_col = *(line_pos + 1);
line_pos += 2;
if (!*line_pos)
line_pos = LINE;
}
/* ^^ renders as ^ so check for that case */
else if (*(line_pos + 1) == '^')
line_pos++;
}
half--;
line_pos++;
if (!*line_pos)
line_pos = LINE;
}

Although we still need that loop to ensure we’re in the right place for when we recopy the line, and have the right colour code for the next part.


/* if your paying attention as opposed to blindly copying and pasting
you'll note I've kept track of the colour code in effect. So now
we can restore the line colour if needed. */
if (*cur_col != *last_col)
{
stack = stpcpy(stack, "^N^");
*stack++ = *cur_col;
}
*stack++ = ' ';

TrueFriends uses a similar system to ensure its cname doesn’t mess up the screen. Well similar in the same way that oranges and lemons are both citrus fruits.


/* now we copy the postfix */
half = (width - (len + 2)) / 2;
/* if width - (len + 2) is odd, then the line is short by 1 character
so lets fix that */
if (width - (len + 2) % 2)
half++;

I’m sorry there just isn’t an intuitive way to test if a number is odd.


while (half)
{
/* check to see if we're on a colour code */
if (*line_pos == '^')
{
if (*(line_pos + 1) == 'N' || *(line_pos + 1) == 'n' || valid_char_col(*(line_pos + 1)))
{
*cur_col = *(line_pos + 1);
line_pos += 2;
*stack++ = '^';
*stack++ = *cur_col;
if (!*line_pos)
line_pos = LINE;
}
/* ^^ renders as ^ so check for that case */
else if (*(line_pos + 1) == '^')
line_pos++;
}
*stack++ = *line_pos++;
half--;
if (!*line_pos)
line_pos = LINE;
}

The final cut and paste of that loop to finish off the line.


/* Now we tidy everything up */
if (*cur_col != 'N')
{
*stack++ = '^';
*stack++ = 'N';
}
*stack++ = '\n';
*stack = '';
}

If colour is on the line, then we end the colour.

And thats a colour aware pstack_mid. Its probably a good idea to quickly scan the code and see where its sane to use the new variable argument version in place of the single argument version.

Sorry, for WordPress being dumb and stripping out the code formatting, I’ll try and find a way to make it saner for the  more complicated code thats coming.