Showing posts with label tech stuff. Show all posts
Showing posts with label tech stuff. Show all posts

Tuesday, November 8, 2011

The Truth Behind Wimpy and Trip

This blog post has two parts.  The first part is a rant, and the other part is a little more relevant.  If you're interested in just reading the detailed technical info, just scroll down past the rant.

Rant / Background Info

Back in April of this year, a small bugfix went in to address a problem with wimpy.  For some reason, a number of players read this change and assumed that the fact that wimpy can kick in when tripped was something new that we had added as a part of that change.  The truth of the matter was that trip always triggered wimpy, and the only thing that we had changed was the need to type "stand" after having wimpy-fled from being tripped.  The lag on both the tripper and the person being tripped remained the same, the damages were the same, and the fact that wimpy could be activated by the trip skill itself remained the same.

Despite this fact that we did not change anything, a number of players insisted that this wimpying on trip was something new and made a big stink about it.  Again, I emphasized that it was not new, but some people refused to believe it, either claiming that (a) they knew more about what I changed in the code than I did, or (b) I was purposely lying to them for some nefarious reason.  Both accusations were leveled at me directly (that is, at least one player literally called me a liar on OOC).

This bothered me more than usual because it's a gross mischaracterization of the admin staff, and it reeks of a total lack of understanding of how the admin staff runs Dark Risings.  We don't typically change major aspects of PK on a whim, try to sneak it into a bug fix, then lie about it.  In fact, literally every significant (and even almost every minor) change to PK we have made has been vetted by players, either by inviting select PKers to participate in closed testing or by hosting an open Hell Night.  That's just not how we operate, and I've written about our approach to implementation before.

In the next section, I'll walk through the code and explain exactly what is going on when someone wimpys out due to being tripped, and exactly what I changed in April that got everyone so convinced that we screwed up wimpy+trip.

Detailed Technical Info

I think a large reason people don't understand/believe what I did and did not change in April is that they don't fully understand how trip works.  To the casual player, the trip command does these things:
  1. sends messages saying "So-and-so trips you and you go down!"
  2. does a little damage ("So-and-so's trip scratches you.")
  3. sets you to the sprawled position so you have to stand up before you can do other stuff
and they all happen at once.  You successfully trip someone, and they're on the ground instantaneously.  This isn't an unreasonable assessment, but the truth is that they don't all happen at once.  In fact, computers really can't do two things at once**; they take an instruction to do something, then they do it, then they take the next instruction, do it, and so on.  The reason it seems to happen all at once is because computers do this VERY quickly--our server's 3.2GHz processors can carry out 3.2 billion instructions per second**, or over three instructions every nanosecond.  To put that into perspective, in the time it takes you to blink your eye (~300ms), Dark Risings knocks out a billion instructions...so it's all effectively instantaneous to the player.

However, let's look at the code for the trip command from August 26, 2006, which predates the changes I made in April 2011 by a good many years:

  1. act("$n trips you and you go down!",ch,NULL,victim,TO_VICT);
  2. act("You trip $N and $N goes down!",ch,NULL,victim,TO_CHAR);
  3. act("$n trips $N, sending $M to the ground.",ch,NULL,victim,TO_NOTVICT);
  4. check_improve(ch, gsn_trip, TRUE, 1);
  5.  
  6. WAIT_STATE(victim, PULSE_VIOLENCE*2);
  7. WAIT_STATE(ch, PULSE_VIOLENCE*2+4);
  8. damage(ch, victim, number_range(2, 2 + 2 * victim->size), gsn_trip,
  9.    DAM_BASH, TRUE);
  10.  
  11. if ( victim->position != POS_DEAD && victim->position != POS_UNCONCIOUS )
  12.     victim->position = POS_SPRAWLED;

To translate a little, here's what's going on:
  • Lines 1-3 are the messages that get sent to the person getting tripped (called victim), the person doing the tripping (called ch), and the rest of the room
  • Line 4 is the check to see if your trip skill% should go up
  • Line 6 is what puts the trip lag on the person getting tripped (victim).  It says PULSE_VIOLENCE*2, which means two rounds of combat, or six seconds.
  • Line 7 is what puts the trip lag on the person using trip (ch).  PULSE_VIOLENCE*2+4 means six seconds (PULSE_VIOLENCE*2) plus another full second, so a total of seven seconds of lag, or one more second of lag than the victim.
  • Line 8-9 is what does the damage from trip.  The messages ("Your trip scratches so-and-so.") is generated within this damage() routine, which is why they don't appear here.
    • the first parameter, ch, indicates who is dealing the damage
    • the second parameter, victim, indicates who should receive it
    • the third parameter, number_range(2, 2+2*victim->size), is the amount of damage that should be done before sanc/ward/resists/etc.  In this case, it's doing a random amount of damage between 2 and 2+2*(the size of your race)..it's not much.
    • the fourth parameter, gsn_trip, indicates the damnoun for the damage (in this case, it'll show up as "Your trip scratches ...")
    • the fifth parameter, DAM_BASH, indicates the damage type of the damage.  Since it's DAM_BASH here, it'll do bash-type damage.  If it were DAM_HOLY, it'd do holy damage; DAM_ACID would do acid damage, and so on.
    • the sixth parameter, TRUE, just indicates if the damage message ("Your trip scratches ...") should be sent out at all.  If this were set to FALSE, you'd take damage but it would be silent (like how dirt kick is).
  • Line 11 is a bugfix that was added back in 2000.  More on this below.
  • Line 12 is what puts the victim on the ground instead of in the regular fighting position

The next question is, where does wimpy factor into this?

As it turns out, a lot of functionality is wrapped up in the damage() routine (Line 8), and wimpy is a part of this.  When you trip someone and the game gets to executing Line 8 in the code, lines 11 and 12 don't get executed until the damage() routine is fully complete.  Here's what's going on in damage(), in no particular order:
  • the effects of sanctuary are applied
  • a check is made to see if the damage should hit a mirror image
  • hp is subtracted from the victim
  • if the victim's resulting hp falls below 1, either knock them out or kill them (depending on whether ch is a player or a mob)
  • if the victim's resulting hp falls below their wimpy, make them flee
That last bullet point is the killer here.  Keeping in mind that computers can only do one thing at a time, here's what's happening with the trip command:
  1. Everyone gets sent the appropriate "trips you and you go down!" message (lines 1-3)
  2. ch's trip skill might improve (line 4)
  3. both parties get lagged (lines 6-7)
  4. damage is dealt to victim (line 8-9)
    • if this damage drops victim's hp below his wimpy, he flees at this point 
    • if this damage drops victim's hp below 1, he is knocked out at this point
  5. victim is tossed on the ground AFTER the damage() routine completes
So what's happening is that, since the instruction to actually knock the victim to the ground comes after the damage is dealt, the victim will have already fled (due to damage()) before the trip code ever gets to the point where the victim gets sprawled.  Thus, this old code from 2006 was making people sprawled out even though they had already wimpy-fled from the room.

Line 11 above is a special check that was added in 2000 to prevent a similar problem.  Before it was added, some PKers had found out that if you manage to KO someone using a knockdown, they would get set to the unconscious position by damage(), but then immediately get woken back up when the rest of the trip code finished and set the person to the sprawled position.  Thus, they didn't have to wait the 45 seconds before they recovered from the KO; they could immediately stand up (after their PULSE_VIOLENCE*2 lag from trip) and run away before anyone could loot them.  What Line 11 does is only set the person's sprawled position if they weren't rendered (!= means "not equal") dead or unconscious as a result of the damage() routine immediately preceding it.

So, there you have it.  Code from 2006 showing that, due to the order in which damage is dealt relative to the instruction to sprawl out the victim, it is possible to wimpy out due to the damage from trip.  Sort of.

As Sintar pointed out (and much to my embarrassment), you can wimpy out of a fight even if you're sprawled.  Initially I said this was impossible because wimpy just issues the "flee" command automatically for the player, and the "flee" command can only be used by the player if his character is fighting, not sprawled.  Go get bashed by a mob and type "flee."  You'll see what I mean.

So how can Sintar get bashed by Heimdall, stay on the ground, and still wimpy out?  Well, let's look at the wimpy code in the damage() routine, which happens to be at the very end of it:

  1. if ( !IS_NPC(victim)
  2. &&   victim->hit > 0
  3. &&   victim->hit <= victim->wimpy
  4. &&   victim->wait < PULSE_VIOLENCE / 2 )
  5.    do_flee( victim, "" );

with a brief translation:
  • if the victim is not an NPC (that is, they are a PC, or a player character)... (line 1)
  • AND the victim's hp is above zero (they aren't KO'ed or dead yet)... (line 2)
  • AND the victim's hp is less than or equal to their wimpy setting... (line 3)
  • AND their current lag is less than PULSE_VIOLENCE/2... (line 4)
  • then execute the "flee" command directly, bypassing all the regular checks to make sure the person is standing, of the appropriate level to use the command, etc. (line 5)
The reason Sintar's wimpy lets him use the flee command is a bit hard to explain; in essence, the part of the code that takes whatever command you type (e.g., "flee") and translates that into a command that the game understands (e.g., do_flee()) is what imposes the restrictions on whether or not you have to be standing to use a command.  Wimpy bypasses that command interpreter entirely, so there's no position check.

So does this mean that you will always wimpy flee if sprawled out in PK?

Not quite.

Line 4 is the key here; although wimpy can make you flee regardless of if you're sprawled, Line 4 says that wimpy won't work unless you've got less than PULSE_VIOLENCE/2, or 1.5 seconds, worth of lag left.  Since trip gives the victim 6 seconds of lag, this guarantees that, as long as the trip damage itself doesn't cause wimpy

CRAP

The trip damage shouldn't ever cause wimpy to fire, because the lag is applied before the damage() (and therefore the wimpy code) gets run.  So, by the time the trip damage is dealt, the victim already has the 6 seconds of lag.  Line 4 will always cause wimpy to fail both when damage() is called by the trip command and for the first 4.5 seconds of lag caused by trip, which translates to a guaranteed minimum of one round of combat before the victim has a chance to flee-while-sprawled.

So why does the damage caused by trip cause the victim to wimpy out?

Because in the change I made back in April, I moved the WAIT_STATE lines in trip (and bash, entangle, armthrow, et cetera) below the damage() line:

  1. act("$n trips you and you go down!",ch,NULL,victim,TO_VICT);
  2. act("You trip $N and $N goes down!",ch,NULL,victim,TO_CHAR);
  3. act("$n trips $N, sending $M to the ground.",ch,NULL,victim,TO_NOTVICT);
  4. check_improve(ch,gsn_trip,TRUE,1);
  5.  
  6. damage(ch,victim,number_range(2, 2 +  2 * victim->size),gsn_trip,
  7.     DAM_BASH,TRUE);
  8.  
  9. WAIT_STATE(victim,PULSE_VIOLENCE*2);
  10. WAIT_STATE(ch,PULSE_VIOLENCE*2+4);
  11.  
  12. /* damage() can cause wimpy which changes victim->in_room */
  13. if (victim->position > POS_FLATFOOTED && victim->in_room == ch->in_room)
  14.     victim->position = POS_SPRAWLED;

Remembering that computers execute commands in-order**, this means that when the damage from trip is dealt, the victim hasn't been lagged by trip yet.  So, wimpy will kick in as long as the victim doesn't have more than 1.5 seconds of lagged already queued up from some other source.

Thus, contrary to the big long tirades I've gone on explaining how we never changed anything, we (that is, I) inadvertently did change (and break) wimpying from trip.  While it was true that it was always possible to wimpy out from being tripped, trip used to guarantee you 4.5 seconds of combat (at least one round) before your opponent could wimpy out.

Now there is a question of what we should do about this; the majority of players seem to agree that wimpy is fine as-is, but its as-is state is really the result of a bug.  If I'm lucky, I will have put everyone to sleep before they read this far down the post, and nobody will ever become aware of my serious folly here.  Realistically though, my inadvertent breakage of wimpy/trip (and subsequent ardent denial of doing such a thing) was not fair to the players, so it should be "fixed" and functionally restored to the way it used to be.  It was a reasonably fair way of doing it, too; people could still wimpy out of being tripped, but tagging with trip guaranteed you a round of combat to dish out damage before that happened.  The subsequent lag was still in the wimp's favor (6 seconds vs. 7 seconds) which is how it remains; the tripper just had a chance at knocking his enemy out with that one round before the wimp could take off again.

Blearg, I hate being wrong, and I hate more when I've been a jerk in the process of being wrong.  Maybe I should take a page from Nixon's book and just destroy all the evidence before word gets out.

** Note: these statements aren't really true, but they are close enough to the truth to illustrate my point.

Monday, November 7, 2011

Coding Quandary for Brawler Scores

One of the principal challenges in establishing this new brawler scoreboard was establishing the framework for it.  The scoreboard, in the simplest sense, made of a string of individual brawlers and their scores.  As such, it would make sense to attach this information to each brawler's pfile (which is where their stats, equipment, description, et cetera are stored) since brawler scores are just another stat.  At the same time though, pfiles are loaded into the game dynamically--if a character isn't logged in, all of the information stored in his pfile (stats, equipment, description, et cetera) are totally unknown to the game.

Think about it.  Are there any commands that let you get information from another character who isn't logged in?  The only example of this that comes to mind reading notes posted by characters who aren't logged in; this is possible because notes are stored in a fashion similar to areas--they are saved to their own special file that is read in when the game starts up, and then the list of notes just floats in the game's permanent memory forever.  As new notes are posted, they also go into permanent memory, and they are saved to the special note file so that they can be reloaded after the next reboot.

Notes are static though;  once they're up, they really never need to be changed.  Brawler stats, on the other hand, will constantly be changing as people claim victory over each other.  Although these changes will only happen when brawlers are logged in to fight each other, what do we do with their scores when those brawlers log out?  They still need to be accessible by everyone on the scoreboard, and they need to be able to be reloaded after reboots.

This question highlights one of the principal design decisions I had to make in deciding how to actually implement this brawler scoreboard.  On the one hand, brawler scores need to be stored permanently like notes are, but on the other hand, they are fundamentally a stat that is attached to one and only one character.  Furthermore, we may need to be able to modify brawler scores even if the brawlers aren't logged in (e.g., in case of wimpouts or losses that need to be verified by imms).  How can these conflicting ideas be reconciled?  Here are the two options that I first considered:

  1. Store brawler scores in permanent memory, but have players "download" them to their character when they log in and "upload" their new scores when they log out
  2. Store brawler scores in permanent memory AND on the pfile, then keep track of which set of scores (the ones in permanent memory or the ones on the pfile) were updated most recently and use those
Unfortunately, both of these options raise problems with consistency.  Because information gets stored in two places in both cases, it becomes easy to envision a scenario where the information in one place doesn't get updated in the other place and suddenly one brawler has two different sets of scores.  This isn't an issue in 100% bug-free code, but the fact is that the system is not robust; if I (or a future coder) needs to modify the brawler code a few years down the road, we have to remember to make sure that both sets of scores need to be manually synced up or else problems arise.

A better option (and what I wound up doing) is to store brawler stats in permanent memory, and instead of worry about uploading/downloading/syncing those numbers against a pfile when it is loaded, just "attach" each player's brawler score info (still in permanent memory) to the pfile when the character logs in.  When the character logs out, the attachment breaks, but the brawler record is still floating in the permanent memory.

While we still have to make sure that brawler scores are properly attached to characters when they're logged in, this is far less flaky of a process than ensuring data is consistent across two places since it's trivial to perform the attachment.  So, in addition to checking for attachment when a character logs in, we can check for attachment at other critical points in the code such as when a brawler gets thrown out of brawler or scores another kill.

Speaking of getting thrown out of brawler, the reason why I bring this all up is because Stage 2 will include a new "leavebrawler" command that players can use to voluntarily quit.  However, quitting Brawler will also ban you from re-joining it for thirty real-life days.  Because we store brawler scores in permanent memory rather than on the pfile, this ban has the unintentional (but fortuitous) effect of persisting across recreates.  Even though a player may have deleted and re-created his character, that character's brawler record is stored in permanent memory and cannot be deleted by the player.

This might sound a bit trivial ("It'll take 30 days to level back up to 50 anyway!"), but a more powerful result of this is that immortals will have the ability to ban characters from brawler for life (such as may be necessary if they are caught abusing brawler perks).  Recreating won't circumvent that ban, which imposes an interesting incentive for players to not try to get cheap healing in a real fight by cheating the system.  A ban from brawler will truly be a ban for life.

Wednesday, April 13, 2011

Memories

If there's one thing science has taught me, it's that the best way to sell an idea is with pretty pictures. So, here's a pretty picture:


We've been having memory issues in the game intermittently since we came back, and the diagram offers a good illustration of the problem. When this bug first manifested, it so happened that I was trying to edit something on our game server and was rudely surprised by all manner of warnings about having insufficient memory to even open the text editor. Sure enough, somehow our game's memory footprint had blown up (seemingly overnight) to over twice its normal size, and there was very little to indicate what had happened. The solution then was to immediately reboot the MUD to flush memory and start from scratch, contact our provider and up our memory, and create a tool to keep track of our game's memory usage.

I've been checking the data dumped out by this tool daily to see if the memory blowup could be reproduced, and today was the first day I had a bite on the line. I suspected to find logs of someone doing something stupid coinciding with this big spike (like spamming haggle, exploiting some bug, etc), but there was nothing. Going back to my own personal logs of that morning though, the memory spikes happened around a time when Sidonie and I were working out some problems with a builder command. Ah-ha!

As it turns out, a very useful but not-often-used command that was put into the code many years ago had a very severe memory leak in it. The command uses the regular expression engine to parse boatloads of mobprogs iteratively, but the code wasn't cleaning up the tail end of the regex calls because the regex libraries silently malloc huge blocks of memory that aren't automatically freed when the parent routine exits.

This was a pretty easy mistake to make; while most high-level programming languages in which one would utilize regexes (like Perl, PHP, and whatever else) abstract away all of the memory management, C certainly does not. I guess the pitfall here is that the regcomp() call buried mallocs within it, so a headfirst dive into using regexes in C wouldn't directly expose the programmer to the mallocs which would raise alarms. An easy pitfall, sure, but like a wise man once said, "if you are going to write C code, you better be willing to deal with the memory management."

For what it's worth, I've gotten the game's code to be remarkably scarce on memory leaks like this despite the fact that I (nor any past coder of whom I know) has ever profiled the code with something like Valgrind or even gprof. While this case may give a pretty good argument for trying to figure out a realistic way to do this, blind dependence on tools like Valgrind to make up for incompetent coding has been known to cause serious problems. If we start running into memory leaks again though, maybe I'll try to figure out a way to set up profiling on a test port somewhere and have some of you players beat on it for a night.

Monday, March 28, 2011

Crying and complaining about coding

One of the hardest parts of coding for Dark Risings is the fact that it's derived from code that is over two decades old (Diku was first released in 1990) and, even after transforming into the ROM 2.4b6 codebase (released in 1998), it's been hacked on by twelve years of Dark Risings coders. As much as I hate to say it, the greatest amount of damage to the code has, in fact, been done by the long history of DR coders of varying levels of familiarity with both C and how ROM works.

What's prompted this whiny rant is a problem I discovered while generating a list of all of the scrolls in the game for Sidonie. I'd already done this with wands and staves with the help of a little Perl magic that I wrote, so I expected it to be another straightforward five-minute job. In a sense it was, but upon examining my data dump, I noticed that some scrolls had four spells (some of which are listed as "reserved") and others have blanks where spells should've been. Thinking that my Perl script was defective, I checked the area files themselves, and sure enough, some scrolls have "reserved" and others are just "". What's the difference? Since this sort of ill-defined behavior can lead to major issues (area/pfile corruption, crashes, etc), I started digging and was brought to one particular routine that made me a little crazy.

Consider the following code, straight out of the Dark Risings source:
/*
* Lookup a skill by name.
*/
int skill_lookup( const char *name )
{
int sn;

for ( sn = 0; sn < MAX_SKILL; sn++ )
{

if( skill_table[ sn ].name == NULL )
break;

if( strcasecmp( name, skill_table[ sn ].name ) == 0 )
return sn;

/*
if ( skill_table[sn].name == NULL )
break;
if ( LOWER(name[0]) == LOWER(skill_table[sn].name[0])
&& !str_prefix( name, skill_table[sn].name ) )
return sn;
*/
}
...
For whatever reason, a past coder decided it best to comment out the stock bits of this subroutine, presumably to force the subroutine to match the whole skill/spell name instead of allowing abbreviation (which can cause issues when you have spells like "lightning bolt" versus "lightning breath").

This sort of thing drives me crazy for many reasons, but here's one.

The stock version of the code was smart; it first checked the first character of the argument against the first character of the entry under examination in the master skill table. Since there's a very large chance that those first characters won't match when ripping through all 298 skills, you wind up not having to incur the overhead associated with a full-on subroutine call for the majority of the misses. This makes the whole process of finding a skill and returning its associated skill number much faster.

Perhaps I am more mindful of the benefit of these tiny performance benefits since I write very computationally intensive scientific code, where one poorly written line of code can add days or weeks onto compute time, for a living. And perhaps there really is no appreciable speed benefit to not preserving this preliminary first-character check when matching strings on modern hardware. However, I see little reason to pull it out outright since it is a smart way of handling these sorts of lookups.

Furthermore, the str_prefix routine (provided with ROM) was replaced with strcasecmp, which is intrinsic to string.h. However, whoever did this code change must have been extremely unfamiliar with ROM's internals (or perhaps extremely tired), because ROM provides str_cmp, its own equivalent to strcasecmp which is used extensively (and I mean extensively) throughout the code.

If strcasecmp and str_cmp serve the same purpose, why do both exist? As it turns out, strcasecmp is indeed included in string.h, but it is not ANSI C (although it is POSIX 2001). POSIX didn't exist back when Diku was first released (and neither did GCC or glibc for that matter), so it's likely that the str_cmp routine was written into the code decades ago. Providing this routine made the code more portable by not having to rely on special extensions that only existed in specific proprietary compilers. It was also tailored specifically to the needs of the code.

For the sake of consistency and portability, str_cmp (which is probably a little faster than strcasecmp) has been used exclusively in ROM's source. Unfortunately, DR's code has become rife with a sloppy mix of strcasecmp and str_cmp, forfeiting the benefits of portability while gaining literally nothing. As DR coders have touched the code, they've left these marks across it without really considering what the implications may have been.

True, since strcasecmp is now POSIX, it is unlikely that Dark Risings will ever be run on a system without it (e.g., DR compiles on HP-UX 11i, which predates POSIX 2001), but this is only an innocuous example. This sort of messy work, where one feature is implemented many times because various coders were ignorant of their predecessors' work, are all over the DR source, and it drives me a little nuts. I could go through and change all those strcasecmps to str_cmp, but to do so would require quite a bit of testing and would open the doors to new bugs. When it comes down to spending time cleaning up code which will not introduce any new features to the game or spending time adding new features while leaving the existing problems as they are, I find myself always choosing the latter.

Nobody cares if our code reads like garbage as long as that garbage is transparent to the players and imms, so I guess I'll have to take out my frustrations on this blog. It probably doesn't help that I spend my entire day at work cleaning up garbage FORTRAN.

Monday, June 15, 2009

Lag Issues

Although people have always been complaining about lag that originates in their connections, we've started experiencing server-side lag issues in the game in the last few months. While connection-related lag is something you all can feasibly fix (by changing ISPs, fiddling with their wireless connections, upgrading in-home wiring, et cetera), mitigating server-side lag is something beyond your control; it will happen no matter how good your connection is. Fortunately, server-side lag affects all players equally, and you won't find your character dead if a server-side lag spike happens while you're in a fight.

For the sake of appeasing any fears related to this increase in lag, here is some useful information:
  • If you come out of the lag spike and a bunch of text (eg battle spam) dumps out on your screen all at once, the lag is due to your connection. If you come out of the lag spike and only one more round of battle spam happens though, it was a server-side problem.
  • Server-side lag locks up the whole game, including battle, mobs, pk, and everything else. If you're in PK and there is some server-side lag, your opponent feels it just as bad as you do--the tables will not be turned on you. Connection-related lag, though, can be fatal.
  • Similarly, you aren't going to die if you're fighting the hydra and the server locks up momentarily. The fight itself locks up, so the hydra isn't hitting you while your screen is frozen; the hydra is frozen up too. While it can be unnerving to have your screen freeze up mid-fight, if the problem is on the server side, the fight will resume as normal when everything unfreezes. There will be no catching up on battle as there would've been if the lag was due to your connection.
Thus, if your connection is usually good and you notice your screen freezing up momentarily, for the most part you don't have to worry about anything bad happening to your character.

For those of you who are interested in the technical details, this server-side lag is due to the server on which we run being momentarily overloaded, an effect similar to when you run too many programs on your computer and you get the hourglass icon for a few seconds. Dark Risings runs on the same server as a number of other muds, and if one of them reboots, experiences some nasty bug which causes them to go rogue, or does anything which momentarily strains the server as a whole, we can feel the effects of that in the form of a lag spike.

Unfortunately, these server-side lag issues are something we will all have to just put up with for the time being. Dedicated hosting solutions cost between 3x and 4x as much as our current hosting, and our current costs are already about as much as we can afford without relying on some sort of revenue (donations, fees, et cetera). However, there is light at the end of the tunnel--our server is due to receive a hardware upgrade in the coming months. If this upgrade will include moving to a SMP (multicore) platform, the server-side lag spikes should go away almost completely.

Sunday, June 7, 2009

Crash Last Night

Last night the game crashed, bringing our total crash count for 2009 up to three. The problem which caused the crash was a bit tricky to find because the actual crash was caused by memory corruption which occurred fifteen minutes before. Tracking it down and fixing it was ultimately not very difficult, but what was particularly frustrating for me in this case was that the root of the crash involved the bane of my existence: an old CODE SNIPPET.

In this case, the problem lay in the rename snippet, v1.1 written by some fellows named Voltec and belial in 1998. This particular snippet has been in the Dark Risings source for as long as I've been here (so it was likely in from the beginning), and it is what allows us imms to rename characters named "Drizzt" without forcing them to delete and recreate. However, the guys who wrote it really didn't know what they were doing (which, from my experience, has been 100% of the people who author publicly available snippets) and thought this would be a good way to rename a character:
/* grab old name, insert new name, Save the new p-file */
strcpy(old_name, victim->name);
strcpy(victim->name, new_name);
Unfortunately for every mud which has used this snippet, the authors of it did not understand that the game does not allocate entire player characters in the game on the stack as it would a local variable; characters (and therefore the names associated with them) are malloc'ed to some block of permanent memory. Using strcpy on any permanently allocated member of a char_data structure is an incredibly novice mistake; strcpy knows nothing about the size of the space in memory allocated for a character's name, so it does absolutely nothing to ensure that it doesn't either overrun the space allocated for the character's name (and therefore corrupts whatever is in the memory adjacent to it, which is what caused yesterday's crash) or release any unused but allocated memory (which would cause a memory leak).

So at best, this snippet causes memory leaks, and at worst, it overruns the boundaries of the part of memory reserved for a character's name, silently corrupts other parts of adjacent memory, and causes a mysterious crash a few minutes down the road when the game tries to allocate memory for a new string and cannot find the correct boundaries for existing strings because they got corrupted. The correct way of doing this sort of renaming routine is actually already written all over the stock ROM code, so it's not like it is even a novel concept:
free_string(victim->name);
victim->name = str_dup( newname );
ROM and ROM-derivatives have their own internal memory handling and allocation subroutines which must be used when creating and destroying strings in this malloc'ed space; that's what free_string() and str_dup() do. My guess is that the authors of the original rename snippet learned about strcpy in their high school programming class, but did not learn about the importance of understanding how the whole program works before screwing around with pieces of it. Shoddy and broken code was the result.

This is exactly the reason Dark Risings no longer incorporates any publicly available snippets. I found myself spending my Sunday morning rewriting the rename command from scratch because the snippet we have been using for it was sloppy, buggy, and caused our game to crash right in the middle of our peak hours, undoubtedly ticking off some players and disrupting the experience for everyone logged in. It took me less time to just rewrite this command myself than it took to debug the code and trace the crash back to some crumby snippet that triggered a bug a quarter of an hour before the game actually caught up with the corrupted memory and tanked.

The funniest part of all of this is that the snippet says at the top:
* also, comes complete with comments! for the budding
* coder to know whats going on */
I feel genuinely sorry for any mudder who plays on a game whose "budding coder" learned how to code from junk snippets like this. At any rate, anyone who posts an idea or suggests to me "______ should be easy to implement because there is a snippet available" will be very rudely pointed to this blog entry. Dark Risings will never use anyone else's code under my tenure as coder.

Friday, June 5, 2009

Reboot

We rebooted this afternoon and a couple of large revisions to portions of the code went in. Most of these changes should be completely unnoticed by you all, as they were aimed at restructuring the internals of the game to make future changes a bit easier to make. One major revision, though, has resulted in the 'practice' list now being sorted alphabetically by spells, then skills. This should make it much easier to find certain spells or skills on your practice list as you spam, practice, or whatever else.

This process of restructuring the master skill and spell table had some unintended side effects which we quickly patched up before the revision went live. While we're pretty confident that everything should be fine now, if anyone notices anything clearly weird with skills or spells now (such as you clearly missing some spells you had before the reboot, or certain objects in the game no longer having spells on them which they should have), I'd like to hear about it. Most of these anomalies, should they occur, will show up on my screen before they show up on yours, and so far the game has shown no indication that anything went awry. Thus, I don't expect to hear any bug reports related to this most recent reboot for now, and hopefully the number of people crying wolf will remain low.

I am posting this to the blog rather than as a note because I don't think the changes that went in with this reboot are important enough to bring to everyone's attention.

Thursday, May 28, 2009

Did you know?

Now that the introductory stuff is out of the way, I can post some meat.

Soon after becoming an IMP, I posted a change to everyone that in fact was not a change at all. It was entitled "Did you know?" (a name inspired by a series of emails that used to be sent out by a dean of mine) and it contained little features that existed in the game that were underutilized or undocumented. The original change posting is still in the game (change search know), and I think another round of fun insider info is past due. Here goes.

Here are some fun facts:
  • The chance to get were is not 5%. Although it has long been stated as such, the truth is that it's been 7% for a long time--longer than I've been an admin. Granted, a 2% difference isn't that big, but all you statisticians who want to recreate for were twice in a row might find this useful to know.
  • We have an online who list. It's still in its beta testing stage and will probably always be that way, but it's a good way to see who's on so that you can decide which character to log. A link is also provided on this blog's sidebar there.
  • There is no cap for hit, dam, or saves. Hitroll and saves work against other factors, so there is no hard cap on them. For example, having higher hitroll decreases your opponent's ability to parry. At some point of having absurdly high hitroll, your opponent's chance to parry cannot get any lower--sure, that's a cap, but it's different depending on who you're fighting. The same applies to saves. Damroll, on the other hand, never stops being useful. Its effect becomes less apparent the higher it gets because the ranges between the different damage indicators (eg, <<< ERADICATES >>> and <*>_MORTALLY WOUNDS_<*>) widens. That extra +5 dam helps no matter what, but it may not be enough to show up as a change from eradication to mortally wounding.
  • Clerics do not get any sort of casting bonus over any other spellcasting class. They may have at some point, but they have not since I started coding.
  • Barbarians heal much faster than any other class. This is a relatively recent change, but keep this in mind the next time you consider poisoning your barbarian before going into pk. Sure, you might get maladicted up and put in an exitless room, but you'll also be at full health when you wake up, even if your opponent is quick about it.
  • You can get experience while leveling for completing in-game miniquests. Miniquests are being added to the game every day, and completing each one typically rewards you with 1000-5000 experience. The next time you create a new character, check out the sewers area. It has been redone to have quite a number of very cool and very lucrative miniquests.
  • Contrary to how it used to be, the %l tag in prompts will now accurately indicate whether or not you're in latelog. If your prompt does not say you are in latelog, you are not in latelog, period.
  • You can send notes to... admin, brawler, immortal, imm, any guild, any player, any race, any class, and any brood. You can also send notes to Snitch to submit gossip and it'll get read by someone.
  • There is an open bounty on crashing the game. If you can crash the game, you will get a restring token. In the year of 2009, the game has only crashed twice--in both cases, the offending crash bug was identified and fixed in less than ten minutes after the game booted back up.
Here are some goofy and dorky things that most people won't care about.
  • The mud's source is written in C and compiles readily on GNU/Linux systems and Sun Solaris using both GNU and Sun compilers. This cross-compatibility is necessary due to one of the game's testing platforms being a SPARC-based Sun system. The game which everyone actually connects to runs on a regular PC server and is compiled with GNU.
  • There is a limit to how much gold you can have deposited. Because the game stores bank accounts in signed long integers (32 bits), it cannot count past 2,147,483,647 gold. If you deposit anything in excess of this, you wind up having negative gold.
  • As of this writing, the game currently takes up 35.5MB of physical memory. This is not to be confused with how much disk space it takes up; that figure is somewhere in the vincinity of 250MB.
  • Since I have become the coder, Dark Risings' official policy is to not use code snippets, ever. All new code is written specifically for Dark Risings from scratch, as the time it would take to properly audit, port, and test others' code (which is often amateur and unreliable) would take far longer than just writing it correctly from scratch.
As people come up with more goofy rumors and outrageous claims over the OOC channel, I will post more of these little clarifications and fun facts. Of course, this isn't to say that us admins are going to disclose how everything works; some aspects of the game (such as what role intelligence plays in spellcasting) will remain deliberately ambiguous.