Sunday, May 1, 2011

Multi-User Games and CGI:Chapter 20


Chapter 20

Multi-User Games and CGI





CONTENTS


Ever since the World Wide Web was created, its multimedia potential has attracted both game designers and game players. In addition, the worldwide connectivity of the Internet makes the Web a natural arena for multi-player games.
CGI programs are, for the most part, the backbone of gaming on the Web. Whether it is a two-player game of Checkers or an extensive open-ended MUD, CGI provides the capabilities required to provide on-the-fly page generation and state retention necessary to maintain a multi-player game on the Web.
The first step in creating a multi-user game for the Web is to check to see if it has already been done. The Web is a big place, and there is a good chance that someone else has done something similar to what you have planned. There is no need to re-invent the wheel (unless, of course, your wheel is better).
A quick glance at Yahoo! shows several dozen entries under the category of "Interactive Web Games" (see Figure 20.1). Only a few of these are true multi-player games, but even those that do not fit this description can be useful as a learning tool in the quest for the perfect multi-player game. Web-based games as a whole can be grouped by "multi-playerness" to gauge better how they relate to true multi-player gaming:
Figure 20.1: Yahoo!'s interactive Web games.

  • Single-Player Games-These are the most common games on the Web, ranging from board games (Chess, Checkers, and so on) and simple image manipulation (Mr. Potato Head, Dart Boards, Sliding Picture Puzzles), to fully realized, intricate Virtual Worlds. What connects these games is the fact that each person playing a particular game is playing alone, with no contact with any other players.
  • Noninteractive Multi-Player Games-These are games in which many people may be playing at once. Although each player may be aware of each other's actions, no player can affect another player's game. Making a noninteractive multi-player game can be as simple as adding a high score table to a single-player game. By far, the most widespread of this type of game are the many "Scavenger Hunts" on the Web (see Figure 20.2). Often sponsored by commercial entities, many of these games have real-life money and prizes at stake.
Figure 20.2: Lingua Center's Web Scavenger Hunt.
  • Cooperatively Interacting Multi-Player Games-Most of these "games" are not really games at all but cooperative endeavors between several people. Web-based "Interactive Fiction" and "Interactive Art" have become enormously popular, enabling people to build upon others' ideas in a public environment. The theory behind these "games" is useful when considering true multi-player games because, unlike most noninteractive games, most cooperative games require the information entered by one player to be remembered and to be automatically available to all players. This creates the problem of "state retention" that is crucial to multi-player games.
  • "True" Multi-Player Games-A "true" multi-player game is a game in which more than one player is present, and the actions of one player can possibly affect (positively or negatively) another player's game. This usually also entails informing the other player that his or her game has been altered. Because the Web is a "connectionless" medium, this can be a nontrivial (even downright difficult) task. Some games, such as Netropolis (http://www.delphi.co.uk/netropolis, shown in Figure 20.3), use a turn-based mechanism, so each player knows when to update his or her browser. Others, such as Onslaught (http://www.webplayer.com/, shown in Figure 20.4), are real-time, requiring the players to be constantly on their toes. No matter the type of game, making playable multi-player games is one of the most difficult tasks to accomplish over the Web.
Figure 20.3: Welcome to Netropolis.
Figure 20.4: On the battlefield of Onslaught.

Multi-User Games

The first (and arguably the most important) part of creating a Web-based multi-player game (or any game, for that matter) is planning. This is also the part that most programmers skip or gloss over. After you have your idea for BlastMasters from Outer Space in 3D, it is very tempting to begin hacking away at it all at once with only your inspiration to guide you. Not giving in to this temptation is important, especially for games meant for the Internet. It is a waste of time to code an earth-shattering 24-bit graphics full-motion video epic only to find out that no browser on the planet can play it.

Planning

Planning is often not that difficult. A couple of pages of outline and a little research can cut your coding time in half. To begin, write down the answers to the following questions:
  1. What is your game about? This is the easy part. What do you want to make? A space game? A western? An abstract mind game? Here, your imagination is the only limit.
  2. How are the players going to interact? This is a base of your actual coding. Are your players going to fight? If so, will it be close-up or long-range combat? Will they trade? Communicate? All of the above?
  3. What constraints are going to be on the game? Because the Web is a connectionless medium, there is no constant link open between the client and the server. This makes on-the-fly updates difficult, if not impossible, by means of standard CGI (it is possible by using tools like Java, but that's another can of worms). Because of this, turn-based games are most natural for CGI. You must decide how long you want your turns to be. A minute? You better hope your players have fast connections to keep up. A day? You better hope your players have long attention spans. Also included in this question is the idea of "scalableness." Are you expecting five players to play this game? 100? Do you want to start at five and move to 100? These are questions best answered before you type your first printf() statement.

Outlining

Now you know what you want to do, you have a vague idea of how you are going to do it, and you have an idea of the size of the project. The next step is outlining. Ask yourself what your game entails. Break it up into segments and even write pseudo-code for parts. As an example, consider one of the most basic game designs: A bunch of people in a room shooting at each other. (Remember "Tank" on the Atari 2600?)
What does this game entail?
  • Character creation-People must be able to make new characters in order to join the game.
  • Map generation-A map that shows the positions of all the players has to be displayed.
  • Moving and attack-There must be some mechanism to carry out player requests to move or attack.
  • Report-Players must somehow be notified that they have hit or missed their target (or that they have been hit themselves).
You could even break this down further into more detailed segments, including pseudocode, along the way. The more detailed your outline is, the sooner design flaws will show up, and the quicker coding will be.
Now we must choose how the information in this game will be stored. Again, because the Web is a connectionless medium, the server forgets about the client after data has transferred. The CGI program itself is therefore responsible for "remembering" information about the player. There are many ways to do this, but the three most common (and easiest to implement) are E-Mail, Database, and Daemon.
  • E-Mail: This is by far the easiest because Form-to-E-Mail programs exist in bounty for every language used in CGI. (For example, http://www.boutell.com/email or, for Mac users, http://www.lib.ncsu.edu/staff/morgan/E-mail-cgi.html.) However, the price to pay for ease is speed and power. Unless you use an automatic mail-handling program (such as procmail), the game will progress only every time you read your mail. Even if you use mail filtering, you must write programs to interpret the forms and act accordingly. For slow-moving, long-term games, this may not be a bad thing, and an e-mail interface should be considered (maybe a "shared-fiction" type game where players are basically role-playing, for example). However, e-mail is probably not the best interface for "Shoot 'Em Up Alley."
  • Database: This is perhaps the most useful (and most used) method of interaction. This entails taking the output from the form and storing it in a database of some sort. This can be as simple as dumping the information to a text file for later manipulation, or as complicated as connecting to a full-featured database server for on-the-fly addition and modification of data. (Several packages exist to ease CGI interaction with most popular database servers, including mSQL, Sybase, Oracle, and many others.) For its simplicity in retrieval, this is the method we are going to use for our sample game.
  • Daemon: Though the Web itself is a connectionless medium, there is nothing stopping your CGI program from taking the data given to it from the form and then connecting to some sort of daemon and dealing with it at its leisure. The major disadvantage of this method is that it requires a daemon process to be continually running for the life of the game. This can be nontrivial in DOS/Windows platforms. Even on UNIX, it can be inconvenient if you are a user on a time-sharing system with fixed quotas. This method does have a large speed advantage and the added bonus of elimination of file manipulation, which can be bothersome in the CGI arena. Also included in this category are CGIs that, instead of connecting to a custom server, connect to a server that already exists for another purpose. For example, a CGI could take input from a form, connect to a (probably modified) MUD server, and then perform actions on the MUD based on the content of the form. This sort of MUD-WWW interaction is one of the most popular frontiers of Internet gaming at the present.
In addition to these three options, you can make your form interface with your game in other ways. For example, you can store retrievable information about users on their browsers in the form of small packets of data called cookies. However, cookies are supported in only a small number of browsers (and in a nonstandard way among those browsers).
For our own small multi-player game (let's call it "The Cage" because it's really just a bunch of people in a cage shooting at each other), we'll use a database to store information about the players and the game. This allows us to make the game relatively fast paced and still avoid the programming complexities of an entire daemon.

Coding

Only now, after completing a good portion of the game in thought and theory, do we make the step of choosing a language in which to write the game. There are several choices available for CGI programming, all of which have strengths and weaknesses in regard to game necessities. The first thing to consider is complexity. Which languages do you know? While it never hurts to learn a new language, if you can write the program with a language you already know, it may be best to stick with that language. If you've already had classes in Visual Basic or Pascal, it's very possible that they will be able to handle the great majority of your CGI needs. Then there is speed. How fast does this really have to be? If your game is to have hundreds of simultaneous players and large computations, speed will be a factor, and a fast language such as C or C++ might be called for. In our case, there will be only 5 to 10 players at once, and the computations are minimal, so this is not really a factor. Power is another concern. Whatever type of game you are making, it will be necessary to parse text (if only to read the form input). For this reason, Perl has become, by far, the most widely used language on the Web and the one we will use to write "The Cage."
As per our earlier outline, the first thing to provide is a means for character generation. What is a character? The character is the human player's presence in the world of the game, and until computers acquire the capability to be subjective, our only means of representation of a character is by statistics. Luckily, computers are very good at statistics. In our case, the statistics are relatively straightforward. Each player needs a unique identifier; in this case, we'll use an ID number. A player name is also nice for color. Because we'll be displaying on a two-dimensional screen, a 2D grid is natural for our playing field (although 3D or even more is possible and worth exploring). So we need two coordinates to place our players in the "cage." If players will be attacking each other, we need some measure of health; call it "hit points" for lack of originality. A mechanism for preventing other players from interfering with a player's move would also be helpful. We will implement that mechanism with another stat that is a "hidden" password. Finally, we will include a list of players who have hit the player recently, so he or she will know who to plan that sneak attack against. For simplicity, we will represent these
statistics in a form similar to a UNIX password file:
id:name:x position:y position:hit points:password:attackers
To avoid conflicts, before we write a new character to our database, we first have to check it against the existing players. So our first function will gather information from our database (in a file called state.dat) and store it in an associative array.

Caution
Perl 5 is required for this example because of the use of references. Perl 5 contains many new features and is almost completely backward compatible. It is available from http://www.perl.com/perl.

sub getinfo {            # Get information about existing players.
    open (P, "state.dat");     # Open player database.
        my $n = 0;         # Number of active players.
        while (<P>) {         # Read through each player.
                chop;         # Remove newline.
                @dat = split(/:/,$_,7);    # Get values from lines.
                $p[$dat[0]]{'name'} = $dat[1];    # Get Name.
                $p[$dat[0]]{'xpos'} = $dat[2];    # Get X Position
                $p[$dat[0]]{'ypos'} = $dat[3];     # Get Y Position
                $p[$dat[0]]{'pass'} = $dat[4];    # Get Password
                $p[$dat[0]]{'hp'} = $dat[5];    # Get Hit Points
                $p[$dat[0]]{'mes'} = $dat[6];    # Get List of Attackers
                if ($p[$dat[0]]{'name'}) {$n++;} # If there was a character
                        # on this line, increment $n.
        }
        close P;
        $n­­; # Since the last character was incremented. Decrement $n.
        return $n; # Return $n. The rest of the data is stored in the
           # associative references \%$p[0..n]
}
This function uses some of the most basic and common functions of Perl with no actual CGI interface. You can use functions similar to this to retrieve and store any kind of information from text files (including e-mail messages).
Now we can actually create the new character. The actual HTML form can be anything as long as it returns a field with the name name. In this function, I make use of a Perl module called cgi_head.pm. This takes input from a form and stores it in variables called $FORM{'x'} where x is the name of the input field. It also prints the correct HTML header so the browser knows to expect HTML code. There are many modules available for Perl that provide similar functionality (including CGI.pm from CPAN http://www.perl.com/perl/CPAN/).
require cgi_head; # include the cgi_head.pm module

sub new_char {

$| = 1;  # Turn off buffering. Mostly for good luck.
srand; # initialize the random number generator.

while (-e "statelock") { sleep 1; } # If someone else is modifying the
                    # database, wait till they finish.
system("touch statelock");           # Lock the database for our use.

$n = &getinfo;    # Get player information.

$xpos = int(rand(28)) + 1; # Create x and y coordinates
$ypos = int(rand(13)) + 1; # for new character.
loop:
for ($I = 0; $I <= $n; $I++) {   # Look through all existing players.
                 # If another player has these
                 # coordinates, make new ones.
        if (($xpos == $p[$I]{'xpos'})&&($ypos == $p[$I]{'ypos'})) {
                $xpos = int(rand(28)) + 1;
                $ypos = int(rand(13)) + 1;
                next loop;
        }
}
open (P, ">>state.dat");   # append to database.
$pass = int(rand(1000));   # Make new password.

if ($n > 0) {    # A kludge to correctly increment $n if there are 0 or 1
        # existing players.
        $newp = $n++;
} elsif ($n eq "0") { $newp = 1; }
else { $newp = 0; }

print P "$newp:$FORM{'name'}:$xpos:$ypos:$pass:10:\n"; # make database entry.
close P;
system ("rm statelock"); # Remove lock.
print <<EOP; # Print confirmation.

<HTML><HEAD><TITLE>Welcome!</title></head><BODY>
<H2>Your character has been approved!</h2>
<FORM ACTION="begin.cgi">
<INPUT TYPE=HIDDEN NAME="name" VALUE="$FORM{'name'}">
<INPUT TYPE=HIDDEN NAME="pass" VALUE="$pass">
<INPUT TYPE=SUBMIT VALUE="Enter the Cage!">
</form></body></html>

EOP
There are two main things to note about this function. First, the "password" is just a random number out of 1000. The only purpose for this is to discourage players from forging other players' actions. The password is silently included on all transactions and matched with the password in the database. Also, the end of the script just prints out an HTML page that links to the game. The only reason this exists is to allow people to refresh the game screen by reloading the page on their browser. We could have directly loaded the game page from this CGI, but a reload would then re-create the character.
Now that we have any number of characters created, we need to design the actual game page on which to place them. Creating games on the Web is unique among all Web applications. It is not necessarily a bad thing (in fact, it can be a very good thing) to push the speed and technology limits of the Web. While you don't want your game to take 15 minutes to load, it does have to hold your player's attention, and graphics are vital to that. Additionally, while you'd like your game to be playable on as many browsers as possible, a game is the perfect showcase for the latest HTML extension or browser toy. Our example is going to be extremely Spartan, with one concession: frames. While frames have serious issues for serious pages, they bring a great advantage to game pages: the capability to display a "control panel" independent of the main display.
To accomplish this, we are actually going to have three CGI programs controlling the game: one to generate the frames, one for the control panel, and one for the game itself (the functions we've already created can be made part of the cage CGI with the use of hidden variables in the form, or they can be separate files).
The first CGI (which was referenced as begin.cgi in the new_char function) simply prints out the frames page (passing along the name and password of the player):
#!/bin/perl
require cgi_head;

$| = 1;

print <<EOP;

<HTML><HEAD><TITLE>The Cage!</title></head>
<FRAMESET ROWS="80%,20%">
<FRAME SRC="cage.cgi?name=$FORM{'name'}&pass=$FORM{'pass'}" NAME=Cage>
<FRAME SRC="con.cgi?name=$FORM{'name'}&pass=$FORM{'pass'}" NAME=Con>
</frameset>
</html>

EOP

The control panel (called "con.cgi" here) is virtually the same code.

#!/bin/perl

require cgi_head;

print <<EOP;

<HTML><HEAD><TITLE>Control Panel</title></head><BODY>
<A HREF="cage.cgi?d=up&name=$FORM{'name'}&pass=$FORM{'pass'}" TARGET=Cage>Go Up
Â</a><br>
<A HREF="cage.cgi?d=down&name=$FORM{'name'}&pass=$FORM{'pass'}" TARGET=Cage>
ÂGo Down</a><br>
<A HREF="cage.cgi?d=left&name=$FORM{'name'}&pass=$FORM{'pass'}" TARGET=Cage>
ÂGo Left</a><br>
<A HREF="cage.cgi?d=right&name=$FORM{'name'}&pass=$FORM{'pass'}" TARGET=Cage>
ÂGo Right</a><br>

To attack, click on the player you wish to attack.
</body></html>

EOP

Again note that the generated links pass the name and password with every transaction.

Finally, the heart of the game: "cage.cgi":

#!/bin/perl

require cgi_head;
require getpos; # Or include the text of the getpos function from above.

srand; # Initialize the random number generator.
$| = 1;

$name = $FORM{'name'}; # Grab name and password from the form.
$pass = $FORM{'pass'};

while ( -e "statelock") { sleep 1; } # Wait until database is free.

system ("touch statelock"); # Lock database.

$n = &getpos; # Get player information.

loop:                   # Find the player that initiated
for ($I = 0; $I <= $n; $I++) {          # this transaction.Store their id
    if ($name ne $p[$I]{'name'}) {  # number in $me.
        next loop;
    }
    $me = $I;
}


# Make sure the correct password was passed to us.
# If not, print an error message and die (rememebering to free the database.)
if ($p[$me]{'pass'} ne $pass) { print <<EOP;
<HTML><HEAD><TITLE>Sorry!</title></head><BODY>
You are not authorized to play this character.
<A HREF="index.html">Go to the entry room</a> to create a new character.<br>
</body></html>

EOP
    system ("rm statelock");
    die;
}

@atts = split(',',$p[$me]{'mes'}); # Make an array of the recent attackers.

if ($d = $FORM{'d'}) {  # If the player is requesting movement, first
            # Check to see if they're trying to move past the
            # Boundries of the cage.  If not, move them.
            # We've chosen the cage to be 30x15, which displays
            # well on most browsers.

    if (($d eq "up") && ($p[$me]{'ypos'} == 1)) {
    } elsif ($d eq "up") { $p[$me]{'ypos'}--; }
    if (($d eq "down") && ($p[$me]{'ypos'} == 13)) {
    } elsif ($d eq "down") { $p[$me]{'ypos'}++; }
    if (($d eq "right") && ($p[$me]{'xpos'} == 28)) {
    } elsif ($d eq "right") { $p[$me]{'xpos'}++; }
    if (($d eq "left") && ($p[$me]{'xpos'} == 1)) {
    } elsif ($d eq "left") { $p[$me]{'xpos'}--; }
}

if ($a = $FORM{'a'}) {                # If attacking,
    $I = 0;                  # Find id number of target
    while ($p[$I]{'name'} ne $a) { $I++; }  # and store it in $I.

    # Now calculate the distance between the players (using the
    # Pythagorean theorem.) The maximum range is (arbitrarily) sqrt(50)
    # (Which is just above 7.)
    $dis = abs(sqrt(abs($p[$I]{'xpos'}**2 + $p[$I]{'ypos'}**2)) -
           sqrt(abs($p[$me]{'xpos'}**2 + $p[$me]{'ypos'}**2)));

    # If the player are out of range, set a message.
    if ($dis**2 > 50) {
        $message = "$p[$I]{'name'} is out of range!<br>\n";
    } else {
    # If they are in range, give them a 50-50 chance of hitting.
        $roll = int(rand(100)) + 1;
    # If they miss, set a message.
        if ( $roll < 50 ) {
            $message = "You missed $p[$I]{'name'}<br>\n";
        } else {
    # If they hit, set a message, add the attackers name to the target's
    # list of attackers, and decrement the target's hit points.
               $message = "You hit!\n";
               $p[$I]{'mes'} = "$me,";
               $p[$I]{'hp'}-;
        }
    }
}

# Now we actually print the game page.  The "cage" itself is done in a
# 30x15 table, bordered with '*'s. Enemies are presented by their id number
# in a link which initiates attack. The player himself is represented by
# an asterisk.

print "<HTML><HEAD><TITLE>The Cage</title></head><BODY><TABLE BORDER=0>\n";
for ($y = 0; $y < 15; $y++) {
    print "<TR>\n";
    for ($x = 0; $x < 30; $x++) {
        for ($I = 0; $I <= $n; $I++) {
            if (($x == $p[$I]{'xpos'}) && ($y == $p[$I]{'ypos'})) {
                if ($I == $me) { $c = "*"; }
                else {
$c = "<A HREF=\"cage.cgi?name=$p[$me]{'name'}&pass=$p[$me]{'name'}&a=$p[$I]{'name'}
Â\">$I</a>";
                }
            }
        }
        if ($c eq "") { $c = "&nbsp"; }
        if (($x==0)||($y==0)||($x==29)||($y==14)) { $c = "*"; }
        print "<TD>$c</td>";
        $c = "";
    }
    print "\n</tr>\n";
}
print "</table>\n";

# Now print any message that may have been set above.
if ($message) { print $message, "<BR>"; }
# Then print the names of all recent attackers.
foreach $att (@atts) {
    print "$p[$att]{'name'} hit you!<br>\n";
}
# Print the player's hit points.
print "You have $p[$me]{'hp'} hit points left!<br>\n";
print "</body></html>\n";

# Now open the database and regenerate the entire thing. We must rewrite
# all players, because we may have changed another player's hit points.
open (P, ">state.dat");
for ($I = 0; $I <= $n; $I++) {
    print P "$I:$p[$I]{'name'}:$p[$I]{'xpos'}:$p[$I]{'ypos'}:$p[$I]{'pass'}:$p[$I]
Â{'hp'}:$p[$I]{'mes'}\n";
}
close P;
system ("rm statelock"); # Unlock database.
And that's it. An entire multi-player Web game in less than 200 lines of Perl. Granted, it's not Mortal Kombat 5, but it has possibilities. It also has a few notable areas that could be improved:
  • The program completely violates namespace by making the reference $p[0..$n] global. This is fairly easily rectified by returning a reference to the entire @p array instead of $n.
  • It's not turn based! Despite all the earlier talk of the advantages of a turn-based mechanism, this game has no turns at all as it is. This means that all players will be able to move and attack as fast as they can click the mouse. This isn't a bad thing if all players are in the same room using similar equipment. However, if one player is two feet from the server and another is on another continent, problems will arise. The Internet is not instantaneous (yet), and to make the game fair, a turn-based mechanism is required. Fortunately, it wouldn't be hard to add one to our sample game. One way to accomplish this would be to queue moves. An extra stat could be added to the database for move requested ("move left," "attack Gary"). If a move already exists, further requests could be ignored. Then at the beginning of each turn (every 10 seconds, minute, or whatever), another script would run, probably as a cron job or maybe a running daemon (cron is a UNIX program that runs other programs at given time intervals, anywhere from once every minute to once every year). This script would execute each move (perhaps in random order) and then erase all moves to allow a new turn to begin.
  • There is no player collision. This is pretty trivial to implement by including a check for player positions when checking for the boundaries when a player moves.
  • Nothing happens when a player "dies." As is, when a player drops below 0 hit points, he or she just has negative hit points. Again, fixing this is trivial. If a player with less than 1 hit point attempts to make a move, the script could generate a "Game Over" page and erase that player from the database.
  • Security, security, security. This is an overriding concern for all CGI programs, especially those dealing with databases. Perhaps you typed in the previous scripts, and while they ran fine from the command line, they did nothing when accessed from the Web. This is because most Web servers are run as a user with minimal access rights to prevent security breaches (usually user "nobody" on UNIX machines). This means the server (and hence the CGI program run by the server) does not have permission to modify the database. The easiest method around this is to set the database with world writable permissions. (On UNIX-like machines, the command is chmod o+w.) Now, however, any user on your machine can modify (or even erase) the database. Because the name of the database is not available from the game pages themselves, you could make the database an obscure file in some obscure directory so that it is unlikely to be found; but if someone is intent on breaking the game, that is hardly going to stop them. In Perl, however, there is another option (which is also available in one form or another when using other languages). A module exists called CGIWrap (http://wwwcgi.umr.edu/~cgiwrap). This enables you to run your CGI program as the user who owns the program. Therefore, the cage.cgi could be run as if it were you, and then you could modify the database that is owned by you. However, this creates another problem. Now that the CGI is being run as if it were you, it has all the same permissions you do, and as such it can modify (or delete) any file in your account. You must, therefore, make absolutely sure that your script is foolproof and can be used to access other files in your account. More information on the security aspects of CGI can be found in Chapter 9, "Security."

Tip
Flags are your friends. One good way to check this is to run the script with the -T flag in Perl. This turns on taint checking, which is designed to prevent such abuses of your script. In addition, always test Perl scripts with the -w flag before using them. This turns on Perl's warning mode that gives out helpful hints on any mistakes, or even just bad decisions, you have made. Having Perl spot goofs you might have made can save hours of debugging for mistakes you have made.

These are just the glaring problems. Many (if not most) of the routines in the previous scripts could be made much more efficient and less naive. Also, you can make many improvements by using this game as a springboard:
  • Add graphics! The game does not need to be this plain. Make tiny icons for the players and the background. Instead of putting ASCII characters in the table, put the appropriate icon in (using <IMG SRC="whatever">). This simple modification improves the enjoyment of the game tremendously.

Caution
As of this writing, Mosaic (and possibly other browsers, as well) has problems with generating inline images within tables. Most other graphical browsers (including OmniWeb, Netscape, and Internet Explorer) do handle this correctly.

  • Allow players to chat. Human interaction is the raison d'etre of multi-player games, and any Web game worth its weight will have some capability to allow players to exchange messages. (What would network DOOM be like without the capability to hurl insults with your rockets?) This is a less trivial mechanism to implement, but its importance is paramount. Consider this (relatively) simple approach for implementing chatting in our sample game:
    Add extra fields in "con.cgi" that allow a player to choose another player to talk to (perhaps by pull-down menu) and another field for the player to type in a message.
    "cage.cgi" must then place these messages into the database (or maybe in another database). Each time the game screen is drawn, the messages will be printed and then erased from the database.
This is just the beginning of what you can do with this game. Slight modifications in the idea could produce entirely new games. Perhaps there could be another database that includes coordinates of obstacles (some that block movement, some that block attacks, some that do both). Maybe a goal could be included (the player must reach a certain coordinate while preventing other players from doing the same).
In this chapter, we have used Perl exclusively to create our programs. This is not to say that it could not be done in other languages. Perl's text-handling routines are superb, but C's speed can be vital. If you are working on a Windows machine, perhaps Visual Basic or C++ is all that is available. The sample Perl scripts here are not magical; you could write equivalent programs in any CGI language-even "sh" (the exact implementation is left as an exercise for the reader).

Summary

With the tools in this chapter as a springboard, there is an entire world of possibility when using CGI to design multi-player games for the Web. Take a look at what's out there (and don't limit yourself to multi-player games, either). Plan out your game and make an outline. Test your game on as many browsers as possible, and beware of performance issues. Take care to make your game secure, especially if you are using databases.
Finally, when considering the capabilities of the Web with respect to gaming, don't ignore the non-CGI alternatives available. Java has become a popular platform for gaming because of its graphical capabilities. However, Java does come with a whole bag full of security concerns, so use it with caution. Macromedia Shockwave is another environment that allows you to be very flashy and graphical, but its multi-user capabilities are limited. Finally, Penguin, a new module for Perl, promises to enable Perl code to be executed securely on remote machines. This opens the door for Web games that maintain stable connections with servers and other players, allowing for the real-time interaction that will really bring Web multi-player games to the next level.






Chat Rooms:Chapter 19


Chapter 19

Chat Rooms





CONTENTS


Web-based chat rooms are one of the biggest success stories on the Internet. Though better Internet "chat technology" exists, the widespread availability of Web browsers and their ease of use has made Web chatting a remarkable phenomenon. Special CGI programming issues exist when creating chat rooms, including user tracking, maintaining state, and multiple access serving.

Chat Rooms-Getting a Life on the Internet

I have been living a life on-line in one form or another since 1990, and chat rooms are the reason why. I'm not the only one who has discovered the incredible appeal and even addictive quality of on-line chat rooms. Sysadmins across the world fret and fume about the amount of server activity chat rooms bring. People's lives crumble around them as they fritter away dozens of hours a week within them. Maladjusted misfits taunt other chat room users and maliciously lie for the cheap thrill of hurting other people. Strangers who "meet" in chat rooms often end up spending obscene amounts of money in long-distance phone calls and even plane fare on each other. On balance, chat rooms are fairly destructive creations. And now, I'm going to teach you how to make one.

Prescription for a Chat Room

A user's flowchart for how to use a chat room is a fairly simple, feedback-oriented process:
  • Access the chat room by entering its URL.
  • Provide the chat room with a "handle." This could be your real name, but more often it's just something you make up. (If ever you find an entity in a chat room claiming to be "Mabelrode," you might just be talking to me....)
  • Type your comments into a TEXTAREA box with a form and submit that form to the chat room CGI program.
  • The submission enters your comments into a "stack" of previous comments, and the CGI program outputs the updated stack to your Web browser, plus a form in which to enter new comments.
The last step is repeated until you're tired of chatting, likely many hours later. Of course, this procedure is made interesting by the fact that others are doing the same. In the time span between typing in new comments and having your browser reload the chat room following submission of those comments, other people have done the same. Once the reload is done, you get to read what other people have newly typed into the room. This loop of activity can simulate near-real-time "chatting." Figure 19.1 is a quick view of London Chat, one of my chat rooms.
Figure 19.1: A view of London Chat room writen by the author.
The CGI programmer responsible for a chat room must take into account the cycle I stated previously. An appreciation of the way a chat room is used will lead to a better chat room design. In addition to making the chat room "user-friendly," there are several organization issues that have to be addressed:
  • After the user accesses the page for the first time, the chat room program must know that it must output a form appropriate to that user upon submission. That is, once the user types in his name the first time, he shouldn't need to type it in each subsequent time.
  • The stack of previous postings shouldn't be allowed to grow indefinitely large.
  • Users should be provided with a way of being able to tell how recent the postings in the stack are.
  • The programmer must decide whether or not to allow HTML within the room. Consider this: If users are asked to submit their postings via a textarea within a form, if the output procedure is to simply regurgitate those submissions, any <HTML TAGS> provided in those submissions will, by default, be interpreted as HTML. Is this desirable? If not, the programmer must take steps to avoid this potential problem.
These points only scratch the surface of how a chat room should be organized. There are other reasons for the chat room to need to "know" who a given user is than just to save the user the trouble of typing their name in repeatedly. A knowledge of the state of the stack can be used in more ways than just limiting its growth. Time stamps on messages can be used in other ways, as well. Also, it may be useful to allow certain users the capability to enter HTML into their posts while not others. I'll explore these concepts later in this chapter.
Listing 19.1 is the source code of a chat room that addresses the simplest level of user and programmer issues I mentioned earlier. This program is alive and active at
http://www.anadas.com/cgiunleashed/chatrooms/chat.cgi

Listing 19.1. chat.cgi-A functional, functioning chat room.
#!/usr/bin/perl

#
# This program was written by Richard Dice of Anadas Software Development
# as part of the Sams Net "CGI Programming Unleashed" book.  The author
# intends this code to be used for instructional purposes and not for
# resale or commercial gain.
#
# Any questions or comments regarding this program are welcome.  You
# may contact the author by Internet email: rdice@anadas.com
#

# puts all POST query information the variable $input_line
read(stdin, $input_line, $ENV{CONTENT_LENGTH});

# replace all '+' coded spaces with real spaces
$input_line =~ tr/+/ /;

# creates array of all data files in $input_line from & separated info
@fields = split(/\&/,$input_line);

#
# decodes hex info for each name/value pair and places pairs in
# %input associative array
#
foreach $i (0 .. $#fields) {
   ($name,$value) = split(/=/,$fields[$i]);
   $name =~ s/%(..)/pack("c",hex($1))/ge;
   $value =~ s/%(..)/pack("c",hex($1))/ge;
   $input{$name} = $value;
}

#
# I put a few of the CGI environment variables in their own variables
# for ease of understanding later on in the program.  also, I create
# a variable $time which records the current time and date
#
$refer = $ENV{HTTP_REFERER};
$ip = $ENV{REMOTE_ADDR};
$browsername = $ENV{HTTP_USER_AGENT};
chop($time = `date`);

# ====== Configuration Variables ======
$progname = 'chat.cgi#Comments';
$baseurl = 'http://www.anadas.com/cgiunleashed/chatrooms';
$html = 0; # set to 1 if HTML is allowed in postings
$maxlines = 100; # sets the size of the stack
$admin_name = 'Richard Dice';
$admin_email = 'rdice@anadas.com';

print "Content-type: text/html\n\n";

#
# the following line traps forms being submitted from invalid locations
#
&print_error if(  (!($refer =~ /$baseurl/)) && defined(%input) );

&remove_html if !($html);

&print_header;
&print_form;
&update_stack if $input{'comments'} ne '';
&print_stack;
&print_footer;

exit 0;

sub print_error {
   print <<END;
<HTML><HEAD><TITLE>Invalid Submission</TITLE></HEAD>
<BODY>
<P>
The URL of the page which submitted the form which has this CGI program
as its action was not valid.  Please go to <A HREF=$refer>$refer</A> to
legally access this CGI program.
</BODY></HTML>
END
   exit 1;
}

sub remove_html {

#
# removes all characters between < and >, inclusive
# ( or, between < and EOL or line beginning and > )
#
   foreach ( keys %input ) {
      $input{$_} =~ s/<([^>]|\n)*>//g;
   }
}

sub print_header {

   print <<END;
<HTML>
<HEAD><TITLE>Richard's First Example Chat Room</TITLE></HEAD>
<BODY>
<CENTER>
<P><FONT SIZE=+2><B>
My First Chat Room
</B></FONT>
<BR><BR>
<FONT SIZE=-1><B><I>
This chat room is the beginning example of the CGI Programming Unleashed
Chat Room Example.  While fully functional, I've left some "hooks" for
later improvement.
</I></B></FONT>
</CENTER>
<BR><HR>
END

}

sub print_form {

#
# if this is the first access of a chatting session, create the ID#
# and put a message into the handle string
#
   if ( !(defined($input{'id'})) ) {
      $input{'handle'} = 'Put your handle here!';
      ($input{'id'} = $ip) =~ s/\.//g;
      substr($input{'id'},$[,3) = ''; # removes first 3 digits of IP info
      for $i ( 0 .. (length($browsername)-1) ) {
         $input{'id'} += ord(substr($browsername,$[+$i,1));
      } # adds Browser info to end of the string
      $input{'id'} .= ".$$"; # process ID becomes part of ID#
   }

   print <<END;
<FORM METHOD=POST ACTION=$baseurl/$progname>
<PRE>
<B>Name     :</B> <INPUT TYPE="text" SIZE=40 NAME="handle" MAXLENGTH="40" ÂVALUE="$input{'handle'}">
<INPUT TYPE="hidden" NAME="id" VALUE="$input{'id'}">
<A NAME="Comments"><B>Comments :</B>
<TEXTAREA NAME="comments" ROWS=3 COLS=50></TEXTAREA></A>
</PRE>
<BR>
<INPUT TYPE="submit" VALUE="Submit Comments or Update Room">
<INPUT TYPE="reset" VALUE="Clear Form">
</FORM>
END
   print "Current time: $time\n";
}

sub print_stack {

   $printed_lines = 0;

#
# referring to the posts/ directory, ls -1t *.posts will force a 1 column
# output, organized most recent file to least recent, of all .post entries
#
   chop(@posts = `ls -1t posts/*.post`);
   foreach ( @posts ) {
      if ( $printed_lines < $maxlines ) {
         open(POST,$_);
         while ( $this_line = <POST> ) {
            print $this_line;
            $printed_lines++;
         }
         close(POST);
      } else {
         system("rm $_"); # clear old entries from the stack
      }
   }
}

sub update_stack {

   $post_name = $input{'id'} . ".$$.post";
   $id_snippet = substr($input{'id'},-2,2);
   open(POST,"> posts/$post_name");

# allows users to control line breaks without access to HTML by simply
# hitting ENTER in the comments textarea
   $input{'comments'} =~ s/\cM\n/<BR>\n/g;
   print POST <<END;
<HR>
<!-- $input{'handle'} -->
<!-- ID#: $input{'id'} -->
<TABLE WIDTH=100%><TR><TD ALIGN=LEFT VALIGN=CENTER><B>$input{'handle'}</B>
<FONT SIZE=-2>($id_snippet)</FONT></TD><TD ALIGN=RIGHT VALIGN=CENTER><I>$time</I>
Â</TD></TR></TABLE>
$input{'comments'}
END
   close(POST);
}

sub print_footer {

   print <<END;
<HR>
<P><FONT SIZE=-1><B>This chat room is maintained by
<A HREF="mailto:$admin_email">$admin_name</A></B></FONT>
</BODY>
</HTML>
END

}

With this small amount of Perl code, we have a working World Wide Web chat room at our disposal. I'll take some time now to discuss how it works and tell you how to make it work on your system.
All form data is passed to chat.cgi using the POST method. The name-value pairs are decoded and placed in a Perl associative array named %input. If the URL is being accessed directly, then %input won't exist because no POST information will come to chat.cgi. Thus, I have chat.cgi check for the existence of fields within %input to decide whether or not the user is accessing the chat room for the first time.
If the access is direct, chat.cgi creates an ID# rather than simply perpetuating the existing one. Also, a message is inserted into the form handle field. After the form is submitted for the first time, whatever is in that field upon submission becomes the value of that field upon reload. This doesn't prevent a user from modifying his name after he's started chatting, though. An ID# cannot be changed. ID# will be used for more than just decoration in amendments to the code later in this chapter.
Previous postings are recorded as files in the "posts" subdirectory beneath the directory that holds chat.cgi. Each file is "free-floating HTML" in the sense that they contain HTML markup but aren't by themselves full HTML files. This is permissible because they are meant to be output as part of a greater stream of data that all together is a full and valid HTML file.
The stack of previous posts is limited in a sense by the number of newlines compared against the variable $maxlines. Each time a line within a .post file is output to the Web, a counter is incremented. Once the counter is greater than $maxlines, all subsequent .post files are deleted rather than output; .post files are output in a last-in, first-output way. This makes sense in the context of how an HTML page is displayed. I can accomplish this quite simply in my code by using the Perl command
chop(@posts = `ls -1t posts/*.post`);
The logic in this line is nested. I'll start on the inside and work out.
ls is the UNIX command similar to dir in DOS. It provides a listing of the files within a directory. ls -1t posts/*.post provides a one-column listing of all files in the posts/ directory ending with .post, arranged in order of newest first to oldest last.
By placing this command in backticks (``), Perl is instructed to shell to UNIX, perform the command within the backticks, and use the standard output of that command as a return value. I then place the standard output into the array @posts. Each line of standard output will occupy one element in the @posts array.
Finally, chop() removes the newline character from each element in the @posts array. Now, I have an array with the filenames I need in the order I need. Note that, by default, Perl keeps newline characters attached to the ends of the lines it reads (from both files and stdin). Most other systems don't do this.

Tip
Perl is the UNIX uber-toolbox. Not only is it a powerful and highly usable tool in its own right, but it has facilities that allow it easy access to all of the rest of UNIX-backticks (``) is a perfect example of this. I almost go out of my way to find situations where I can put the combination of UNIX and Perl to use.

I have made the "executive decision" to disallow HTML in chat.cgi. I accomplish this in a combination of ways. A flag, called $html, is provided. If it equals zero, then HTML is "turned off." This allows an if statement to apply the following code:
$input{$_} =~ s/<([^>]|\n)*>//g;
This line demonstrates the great power and utter confusion of the Perl s/// command. "s" stands for "substitute." Whatever is between the first and second slashes is what is to be looked for. Whatever is between the second and third slashes is what that is to be replaced with. Both clauses are expressed in terms of Perl's regular expression syntax. Conceptually, this isn't a difficult topic. However, regular expressions often "bloat" so much that they become scary-so scary that a voluntary PG-13 rating would be appreciated. The string of characters between the first two slashes in the preceding line of code means: "match on everything between < and >, or between < and a newline character, or between the beginning of a line and a >." I think.
We all just have to get used to it, I guess. Regardless, the net effect (no pun intended) of this incantation is to remove all HTML tags.
Another option for disallowing HTML is somewhat simpler:
$input{$_} =~ s/\</\&lt\;/g;
$input{$_} =~ s/\>/\&gt\;/g;
This simply replaces > with &gt; and < with &lt;, the harmless HTML codes for these characters. People can still try to use HTML with these lines in place, but their unsuccessful attempts will be visible for all to see.

Getting chat.cgi to Work for You

It's nice to be able to use someone else's CGI code where appropriate, but sometimes setting up that code to work on your system can be as hard as writing your own CGI program. To help you out, I'll go through and talk about what's needed to make the chat.cgi code work for you.
There are three things to take into account when installing this code on your system:
  • The Perl code itself
  • Your configuration of UNIX
  • Your server configuration
Near the top of the Perl code, there is a section discussing "Configuration Variables." This section is mostly straightforward, and parts of it are even documented within the code. You should change the variables starting with $admin to your name and e-mail address. $baseurl is the portion of the URL that references your chat.cgi installation up to but not including the final / character. $progname is whatever you call your installation of chat.cgi. As a bit of a trick, a #name can be added after the name of the program in $progname to reference a <A NAME=name> tag that is added elsewhere in the program.
Perl programs can often reference UNIX system calls and programs. Different UNIX systems provide different command paths and directory structures, so any Perl CGI program you acquire might have to be modified to reference these UNIX commands correctly on your local system. Within the Perl CGI program, be on the lookout for system ("...") statements and backticks `...`.
If you do end up having to change the Perl code to correctly reference UNIX commands on your local system, you might have a difficult time finding exactly where these commands are kept. Here is a list of the four things I try before going to the sysadmin to find a command I need on a UNIX system:
  • which COMMAND will tell me which instance of COMMAND comes first on my command path.
  • whereis COMMAND will report all instances of COMMAND on my command path.
  • If COMMAND isn't on my command path at all, man COMMAND will bring up the manual page COMMAND. There are often clues in the man page that you can use to find the location of COMMAND.
  • Failing these three methods, you can use the UNIX find command. It's slow, and its usage is famously cryptic, but it does work. You can learn its mysteries through man find.
With regards to UNIX directly and not the commands available through it, chat.cgi requires that a "posts" subdirectory be made within the directory that contains chat.cgi. The directory containing chat.cgi and the "posts" must be writable by the Web server. The Web server must also be able to run chat.cgi; both chat.cgi and the directory that contains it must have adequate execute permission to do this. For a discussion on file and directory permissions, type man chmod at your UNIX shell.
Even after you have done an appropriate chmod of the files and directories involved, your CGI programs won't execute unless the Web server is prepared to accept them. To ensure this, chat.cgi must either be located in the /cgi-bin/ directory of your system, or the .cgi Magic MIME type must be enabled in your server's srm.conf file. If you are a sysadmin, these are both easy tasks to accomplish. If you require assistance from your sysadmin to do either of these things, tell them that I said it was all right. Trust me, this will work.

Chat Room Systems and Entry Pages

Listing 19.1, chat.cgi, is a multistate CGI-no .html file is needed as a "springboard" to activate it. In general, I don't feel it necessary to provide an .html bridge for CGI programs. Instead, I make my CGIs "aware" of the context in which they are being invoked. However, in the realm of chat room programming, there are two particular schools of thought on the matter. Neither is right or wrong; they simply reflect different philosophies and methods of organization.
The method I have modeled chat.cgi after is the multistate system. My reasons for this are that it allows users to see what has happened in the discussion stack before actually becoming part of the conversation, and that any chat room CGI will ultimately need the capability to use itself as both source and destination of form information-that is, without going through any gross and unnecessary contortions just to "prove a point" or something.
The other school entails the creation of a static .html page that is a "gateway" into the CGI. There are a couple of reasons that would motivate this sort of setup:
  • The chat room is "member's only," and people were forced to access it through the "front door" where a username and password must be supplied.
  • The chat room is actually a system of chat rooms. The .html entry page would contain a form that would ask for both a handle and a choice of room within the chat system (for example, the Aloha Deck, the Acapulco Lounge or, umm... Ten-Forward).
If the programmer wants to have several different rooms in a chat room system, they might still rely on the same CGI program. On the entry page, the form would have a selection of rooms available to the user. The user would select one, and then that selection would become part of that users set of hidden input tags. The room hidden tag would then be recognized by the chat room CGI program, and if statements could be used to set the variable that decided the name of the directory that held the .post files, for instance.

Extension to the Basic Chat Room

As with any program, there is good, and then there is better. A chat room is better than no chat room at all. A chat room that allows you to carry on private conversations with your best friend or block out any posts from that annoying 13-year-old weenie who wants to call himself !!!!!!THE MAGNIFICENT DEATH glaDIATOR!!!!!! would be better yet. I'll devote most of the rest of this chapter to describing situations that an advanced chat room could address.

Intelligent User Identification

So far, I've introduced a cryptic datum called the ID#. What good is it? Well, so far it's not being used for much. The next few pages will change that.
Why is it a good idea to keep track of users within a chat room? There are two broad categories of reasons: to allow beneficial things for the good users of the chat room and to show the door to troublemakers. Unfortunately, they exist and are legion. I don't think the problem is nearly so great in a members-only chat site; people only feel safe being obnoxious in the extreme when they're anonymous. But if you're planning on building a publicly available, anonymous chat site, you'd better plan some defenses.
The ID# is a hash based on a few individual items:
  • The numeric IP address of the user.
  • The name of the user's browser, as passed by the HTTP_USER_AGENT environment variable.
  • The Process Identification Number (PID) of the chat.cgi program the first time the user activates it.
The most important factors are the first two because these are dependent on information about the person using the chat room. The PID is thrown in just as a way to build up the number a bit more. I take the last two digits of PID into $id_snippet, which is displayed to the chat room. The idea behind this is to give two people the opportunity to use the same handle ("Molly Millions," for instance), but they won't look absolutely identical to other users.
As defined in chat.cgi, the ID# can't be "reverse-engineered" to provide the numeric IP or browser names, even given the algorithm that created it. I have intentionally done this to preserve privacy on-line. It's good enough for that job I'll be using it for; it doesn't have to go any further.
In addition to the preceding, the section of the ID# that relates to numeric IP address and browser name isn't even unique. If someone else enters the chat room with the same browser and from the same IP, chat.cgi will determine that part of the ID# for someone else matches your information. My intention is to then forbid that second user to enter the chat room. This seems reasonable to me as a way of preventing one user from having multiple chat sessions going at the same time. The drawback of potentially having other legitimate users denied access exists, but I don't think it's worth dwelling on. With the predominance of Internet dial-up giving people their own IP addresses and the slim odds that two people from the same lab will want to access one specific chat site simultaneously, this is a restriction I'm willing to allow.
So now that we have an individual ID# for each user, what do we do with this data? I'm going to put them all in a file. This file will have four data fields: ID#, Handle, expiry time, and "blocked status." Here's what I'm going to do:
  • Each time chat.cgi is invoked, the datafile is read into memory.
  • The entry in the datafile that corresponds to the user who invoked chat.cgi is updated such that his or her expiry time is the current time plus five minutes.
  • The datafile is parsed, and info regarding each user name is added to the form as a checkbox. If that checkbox is turned on as of the next form submission, then any posts from that person won't be displayed in your stack.
  • Also, the form will have a menu that will allow the user to specify which person his or her current posting will be directed towards. "Everyone" will be the default option.
All this is accomplished with use of an ID#, an auxiliary data file, and a lot of brain-racking code. Well, sort of.

Caution
Since two (or more) people could make their form submission very near in time to each other, this auxiliary data file stands the chance of being overwritten. To avoid this, a file- locking mechanism must be devised. You'll see how I do this in my code example.
File locking is one of the fundamental concepts in multi-user systems programming. Be on the lookout for circumstances in your code, chat room or otherwise, where it should be used.

ChatMaster-The Chat Room Administrator

In the chat rooms I have administered, I've found that users get a sense of security knowing that they're talking to the person "in charge of it all." To that end, I've created a feature in my chat rooms that I call "ChatMaster Mode."
There are three special privileges possessed by the ChatMaster:
  • The ID# is customized.
  • The ChatMaster can use HTML.
  • If the ChatMaster blocks a user's posts, then those posts are blocked for everyone. This is accomplished by setting the "blocked status" field to BLOCKED in the auxiliary data file mentioned before.
ChatMaster mode is activated when a handle is entered into the form that has the value of a special ChatMaster password. There is also error trapping for direct entry of the handle "ChatMaster."
The first two aspects of ChatMaster mode are easily implemented by if statements within the code. Universal blocking is accomplished by storing the term "BLOCKED" as the fourth data field within the auxiliary data file.

Private Messaging

It would be good to provide the users of this chat room with the capability to direct messages to specific people within the room. The form interface will now include a drop-down selection menu that will allow the user to choose a recipient for his or her current message. The default value is everyone, a recognized special term indicating that all members of the chat room should receive the message. The selection menu is created by parsing the @userdata array for a list of current users of the chat room.
From the point of view of the program, an $input{'private'} array entry signals the &update_stack subroutine to make a stack entry with a special naming convention. Also, a special header will be given to private messages. The &print_stack subroutine has been rewritten to be aware of private messages and to ignore all private messages that aren't being sent to the invoking user.
Intelligent user identification, blocking, ChatMaster mode, and private message features are included in newchat.cgi. The source code for this is presented in Listing 19.2, and it resides on the Web at
http://www.anadas.com/cgiunleashed/chatrooms/newchat.cgi

Listing 19.2. newchat.cgi-A more sophisticated chat room that incorporates some of the features discussed in this chapter.
#!/usr/bin/perl
#
# This program was written by Richard Dice of Anadas Software Development
# as part of the Sams Net "CGI Programming Unleashed" book.  The author
# intends this code to be used for instructional purposes and not for
# resale or commercial gain.
#
# Any questions or comments regarding this program are welcome.  You
# may contact the author by Internet email: rdice@anadas.com
#

# ====== Configuration Variables ======
$progname = 'newchat.cgi#Comments';
$baseurl = 'http://www.anadas.com/cgiunleashed/chatrooms';
$html = 0; # set to 1 if HTML is allowed in postings
$maxlines = 100; # sets the size of the stack
$admin_name = 'Richard Dice';
$admin_email = 'rdice@anadas.com';
$cmpswd = 'Aaron Thunderfist'; # Handle entry which gives ChatMaster access
$datafile = 'userdata.dat';
$reset_time = 600; # 600 seconds without update before session is terminated

#
# I put a few of the CGI environment variables in their own variables
# for ease of understanding later on in the program.  also, I create
# a variable $time which records the current time and date
#
$refer = $ENV{HTTP_REFERER};
$ip = $ENV{REMOTE_ADDR};
$browsername = $ENV{HTTP_USER_AGENT};
chop($time = `date`);

#
# read datafile into the @userdata array, remove terminating newlines
# from all array elements
#

#--
while ( -e "lock_file" ) {
   sleep(1);
}
#---
open(LF,"> lock_file");
print LF "Locked!\n";
close(LF);
chmod 0660,'lock_file';
#---
open(DF,$datafile);
chop(@userdata = <DF>);
close(DF);
#---
unlink 'lock_file';
#---

#
# remove all elements of @userdata which are past their expiry time
#
foreach $ud ( @userdata ) {
   @field = split(/\t/,$ud);
   push(@udtemp,$ud) if $field[2] > time;
}
@userdata = @udtemp;
undef(@udtemp);

# puts all POST query information the variable $input_line
read(stdin, $input_line, $ENV{CONTENT_LENGTH});

# replace all '+' coded spaces with real spaces
$input_line =~ tr/+/ /;

# creates array of all data files in $input_line from & separated info
@fields = split(/\&/,$input_line);
$input_line = (); # free up memory

#
# decodes hex info for each name/value pair and places pairs in
# %input associative array
#
foreach $i (0 .. $#fields) {
   ($name,$value) = split(/=/,$fields[$i]);
   $name =~ s/%(..)/pack("c",hex($1))/ge;
   $value =~ s/%(..)/pack("c",hex($1))/ge;
   if ($name ne 'block') {
      $input{$name} = $value;
   } else {
      $input{$name} .= $value . "$;";
   }
}
chop($input{'block'}) if defined($input{'block'}); # remove trailing $;

print "Content-type: text/html\n\n";

#
# if this is a first-time access and another user is from the IP and using
# the same browser as this user, prevent this user from using the chat room
#
if (!defined($input{'id'})) {

#
# "postulate" an ID# for this new user
#
   ($temp_id = $ip) =~ s/\.//g;
   substr($temp_id,$[,3) = ''; # removes first 3 digits of IP info
   for $i ( 0 .. (length($browsername)-1) ) {
      $temp_id += ord(substr($browsername,$[+$i,1));
   } # adds Browser info to end of the string

#
# compares the "postulated id#" against those id#s found in the @userdata
# array... if it finds a match, terminate this login
#
   foreach (@userdata) {
      @field = split(/\t/);
      substr($field[0],index($field[0],'.')) = '';
      if ( $field[0] eq $temp_id ) {
         &id_conflict_error;
      }
   }

#
# ID# checks out okay, so add on PID info and store in %input
#
   $input{'id'} = $temp_id . ".$$";

}

#
# determine whether or not the current user is ChatMaster
#
if ( ($input{'handle'} eq $cmpswd ) && (!($input{'id'} =~ /ChatMaster/)) ) {
   $cmmode = 1;
} elsif ( ($input{'handle'} eq 'ChatMaster' ) &&
($input{'id'} =~ /ChatMaster/) ) {
   $cmmode = 1;
}

#
# if the current user is the chatmaster, adjust user's ID# in both
# $input{'id'} variable and in the @userdata array
#
if ($cmmode) {
   $input{'handle'} = 'ChatMaster';
   $id = $input{'id'};
   if (!($id =~ /ChatMaster/)) {
      substr($input{'id'},$[,0) = 'ChatMaster.';
   }
   foreach ( @userdata ) {
      @field = split(/\t/);
      if (!($field[0] =~ /ChatMaster/)) {
         if ( $field[0] eq $id ) {
            substr($_,$[,0) = 'ChatMaster.';
         }
      }
   }
}

#
# Give an error message if someone tries to be ChatMaster without the
# correct password
#
if ( ($input{'handle'} eq 'ChatMaster' ) &&
(!($input{'id'} =~ /ChatMaster/)) ) {
   &chatmaster_error;
}

#
# the following line traps forms being submitted from invalid locations
#
&print_error if ( (!($refer =~ /$baseurl/)) && defined($input{'handle'}) );

#
# If HTML is disallowed, go to the remove_html section... except for
# the ChatMaster
#
&remove_html if ( (!($html)) && (!($cmmode)) );

&print_header;
&print_form;
&update_stack if $input{'comments'} ne '';
&print_stack;
&print_footer;

&update_datafile;

exit 0;

sub print_error {
   print <<END;
<HTML><HEAD><TITLE>Invalid Submission</TITLE></HEAD>
<BODY>
<P>
The URL of the page which submitted the form which has this CGI program
as its action was not valid.  Please go to
<A HREF=$baseurl/$progname>$baseurl/$progname</A> to
legally access this CGI program.
</BODY></HTML>
END
   exit 1;
}

sub chatmaster_error {
   print <<END;
<HTML><HEAD><TITLE>Invalid use of ChatMaster</TITLE></HEAD>
<BODY>
<P>
An attempt was made to access ChatMaster mode illegally.
</BODY></HTML>
END
   exit 1;
}

sub id_conflict_error {
   print <<END;
<HTML><HEAD><TITLE>Multiple Login Sessions not allowed</TITLE></HEAD>
<BODY>
<P>
You may only possess one login session at a time.
<P>
It is (just a little) possible that someone else is using your exact data
port information right now.  If this is the case, you'll have to wait for
them to finish their session in this chat room.
</BODY></HTML>
END
   exit 1;
}

sub remove_html {

#
# removes all characters between < and >, inclusive
# ( or, between < and EOL or line beginning and > )
#
   foreach ( keys %input ) {
      $input{$_} =~ s/<([^>]|\n)*>//g;
   }
}

sub print_header {

   print <<END;
<HTML>
<HEAD><TITLE>Richard's More Impressive Chat Room</TITLE></HEAD>
<BODY>
<CENTER>
<P><FONT SIZE=+2><B>
My Second Chat Room
</B></FONT>
<BR><BR>
<FONT SIZE=-1><B><I>
This chat room has more features than its predecessor.  Enjoy!
</I></B></FONT>
</CENTER>
<BR><HR>
END

}

sub print_form {

#
# if this is the first access of a chatting session, create the ID#
# and put a message into the handle string
#
   if (!(defined($input{'id'}))) {
      $input{'handle'} = 'Put your handle here!';
   }

#
# if the ChatMaster has left ChatMaster mode, strip the special string
# from the ID# in both $input{'id'} and in the @userdata array
#
   if ( (!($cmmode)) && ($input{'id'} =~ /ChatMaster/) ) {
      substr($input{'id'},$[,11) = '';
   }
   foreach ( @userdata ) {
      substr($_,$[,11) = '' if $_ =~ /ChatMaster/;
   }

   print "<FORM METHOD=POST ACTION=$baseurl/$progname>\n";

   foreach ( @userdata ) {
      @field = split(/\t/);
      undef($check);
      if ($field[0] ne $input{'id'} ) {
         $check = ' chECKED' if &blocked($field[0]);
         print "<INPUT TYPE=checkbox NAME=block VALUE=$field[0]$check>";
         print " <FONT SIZE=-1><I>block $field[1]</I></FONT><BR>\n";
      }
   }

   print <<END;
<PRE>
<B>Name     :</B> <INPUT TYPE="text" SIZE=40 NAME="handle" MAXLENGTH="40"
VALUE="$input{'handle'}">
<INPUT TYPE="hidden" NAME="id" VALUE="$input{'id'}">
<A NAME="Comments"><B>Comments :</B>
<TEXTAREA NAME="comments" ROWS=3 COLS=50></TEXTAREA></A>
To: <SELECT NAME="post_to">
<OPTION VALUE="everyone">Everyone</OPTION>
END
   foreach ( @userdata ) {
      @field = split(/\t/);
      if ( $field[0] ne $input{'id'} ) {
         print "<OPTION VALUE=\"$field[0]\">$field[1]</OPTION>\n";
      }
   }
   print "</SELECT>\n";
   print <<END;
</PRE>
<BR>
<INPUT TYPE="submit" VALUE="Submit Comments or Update Room">
<INPUT TYPE="reset" VALUE="Clear Form">
</FORM>
END
   print "Current time: $time\n";
}

sub print_stack {

    $printed_lines = 0;

#
# referring to the newposts/ directory, ls -1t *_posts will force a 1 column
# output, organized most recent file to least recent, of all .post entries
#
   chop(@posts = `ls -1t newposts/*_post`);
   foreach ( @posts ) {

#
# Reclaim ID# from filename and test to see if that ID# is blocked by either
# the current user or the chatmaster.  Also, skip posting if it's not
# "addressed" to this user.
#
      @field = split(/\_/);
      if ( $field[2] ne 'everyone' ) {
         next if $field[2] ne $input{'id'}; # skip if not for my ID#
      }
      next if &blocked($field[0]); # if blocked, skip this posting
      next if &cm_blocked($field[0]); # if blocked, skip this posting

#
# prints valid entries, clears old ones from the stack
#
      if ( $printed_lines < $maxlines ) {
         open(POST,$_);
         while ( $this_line = <POST> ) {
            print $this_line;
            $printed_lines++;
         }
         close(POST);
      } else {
         system("rm $_"); # clear old entries from the stack
      }
   }
}

sub update_stack {

   $post_name = $input{'id'} . '_to_' . $input{'post_to'} . '_post';
   $id_snippet = substr($input{'id'},-2,2);
   open(POST,"> newposts/$post_name");

# allows users to control line breaks without access to HTML by simply
# hitting ENTER in the comments textarea
   $input{'comments'} =~ s/\cM\n/<BR>\n/g;

   print POST <<END;
<HR>
<!-- $input{'handle'} -->
<!-- ID#: $input{'id'} -->
<TABLE WIDTH=100%><TR><TD ALIGN=LEFT VALIGN=CENTER>
<B>$input{'handle'}</B> <FONT SIZE=-2>($id_snippet)</FONT></TD>
<TD ALIGN=RIGHT VALIGN=CENTER><I>$time</I></TD></TR></TABLE>
END

   if ( $input{'post_to'} ne 'everyone' ) {
      print POST "<FONT SIZE=+1><B>Private Message</B></FONT>\n";
   }
   print POST "$input{'comments'}";

   close(POST);
}

sub print_footer {

   print <<END;
<HR>
<P><FONT SIZE=-1><B>This chat room is maintained by
<A HREF="mailto:$admin_email">$admin_name</A></B></FONT>
</BODY>
</HTML>
END

}

#
# this routine checks to see whether or not the ChatMaster is blocking a
# given individual
#
sub cm_blocked {

   ($temp_id) = @_;

   foreach ( @userdata ) {
      @field = split(/\t/);
      if ( ($temp_id eq $field[0]) && ($field[3] eq 'BLOCKED') ) {
         return 1;
      }
   }

   return 0;
}

#
# this routine returns true if the ID# passed to it is found within the
# $input{'block'} string
#
sub blocked {

   ($temp_id) = @_;

#
# if the user isn't blocking anyone, return false
#
   return 0 if $input{'block'} eq '';

#
# check for a match between the ID# being checked up on and the list of
# blocked ID#s.  If there is a match, return true
#
   @blocks = split(/$;/,$input{'block'});
   foreach ( @blocks ) {
      return 1 if $temp_id eq $_;
   }

   return 0;
}

sub update_datafile {

#
# don't proceed while $datafile is locked
#
   while ( -e 'lock_file' ) {
      sleep(1);
   }

#
# enable locking mechanism
#
   open(LF,"> lock_file");
   print LF "Locked!\n";
   close(LF);
   chmod 0660,'lock_file';

   foreach ( @userdata ) {
#
# if the particular element in the @userdata array pertains to this user,
# updates this user's expiry time info (and handle, since that too might
# change)
#
      @field = split(/\t/);
      if ($field[0] eq $input{'id'}) {
         $found = 1;
         $field[2] = time + $reset_time;
         $field[1] = $input{'handle'};
      }
#
# if current user is the ChatMaster, write appropriate blocking info into
# @userdata array
#
      if ($cmmode) {
         if ( $field[0] =~ /$input{'block'}/ ) {
            if ($#field == 2) {push(@field,'BLOCKED'); }
         }
      }
      $_ = join("\t",@field);
   }

#
# if this is the first time a user is accessing the chat site, add a new
# entry to the user data file (via the @userdata array) to that effect
#
   if (!($found)) {
      push(@userdata,join("\t",$input{'id'},$input{'handle'},time+$reset_time));
   }

#
# rewrite datafile with new info on expiry time and possibly blocking info
#
   open(DF,"> $datafile");
   foreach(@userdata) {
      print DF "$_\n";
   }
   close(DF);

   unlink 'lock_file'
;
}

Other Chat Room Features and Examples

Chat rooms are a good example of a software system that suffers from "creeping featurism." A lot of things can be added to chat rooms, but it's questionable as to whether or not certain features should be added. Before coding a new feature, ask yourself a few questions: Is it needed? Is it understandable? Will people actually use it?
For examples of every chat room feature imaginable, you can go to Hulaboy's list of Web chat sites:
http://www.hula.net/~hulaboy/1_chtlnx.htm

Auto-Updates

In general, Web-based chat rooms require that users actively refresh the stack themselves, either by submitting an entry to the stack or by clicking on some sort of update link. With server-push/client-pull mechanisms, this can be accomplished automatically. The HealthyChoice Web site has a very nice chat system within it. You can see many of the features I've created here and a few I haven't, including auto-updates at their site:
http://chat.HealthyChoice.com/cgi-bin/Test.exe

Frames

One of the problems with automatically updating chat rooms is that your page might update while you're in the middle of typing in a new posting-this is not good. One possibility is to combine automatically updating pages with Netscape's frame feature. You could program the room so that the form portion of the page doesn't update, but the stack portion does.
The validity of frames as a feature is a hotly debated topic on the Internet, and I don't mean this as an endorsement of frames at all. Personally, I don't like them. Still, it's interesting to consider them when planning a chat room.

Previous Postings

With the way chat.cgi and newchat.cgi are coded, postings beyond the $maxlines limit are deleted. This isn't the only way to deal with these postings. Keeping them around could be a good idea, too.
One feature I've seen that I really like is a page of previous postings. The chat system at Bianca's Smut Shack keeps postings around for several days. Rather than being deleted, old postings are moved into a separate area and can be read with a different CGI program. Strictly speaking, this isn't a "chat room" feature, but it's certainly useful and could become part of your chat system. For the brave at heart, you can find Bianca and her "trolls" at
http://www.bianca.com/

Sign-Up Boards

Bianca's Smut Shack also attaches a sign-up board to each chat room within. Again, this is not a chat room feature, but it's a great idea. This way, people can leave messages for each other to arrange times to chat, for example.

Dynamic Room Generation

Occasionally, chat room users might want to split off from the main crowd and have a chat room all their own. This can be accomplished with a mechanism very similar to private messaging. InfiNet's popular Talker chat room does a good job in this regard:
http://www2.infi.net/talker/

Alternatives to CGI Chat Rooms

One of the things I find most remarkable about CGI chat rooms is how popular they are given the superior alternatives. Yes, you heard me correctly. CGI chat rooms aren't all that good. Here are a few alternative schemes that do a great job of allowing people to chat with each other.

IRC-Internet Relay Chat

This isn't so much a program as a whole client/server (or peer-peer) protocol on the Internet. There are a great number of IRC clients and servers that allow countless net-heads to chat themselves into an early grave. Winsock IRC clients can be found at Stroud's Consummate Winsock Application List.

PowWow

This exceptional Winsock client allows many people to chat simultaneously, each in their own window. While chatting, people can send pictures, sounds, and other files to each other. Again, look for this at Stroud's list.

Java Chat Rooms

I think this is where things are headed. Java is poised to become nearly universally supported as part of the standard Web browser, and Java-based chat rooms are technologically quite capable. The Earthweb Java chat server is tops at this point in time:
http://www.earthweb.com/html/products.html

talk and ytalk-Old UNIX Standbys

The UNIX talk command is an oldie but a goodie. The talk protocol is quite respectable, but the talk environment is limited to two users. ytalk is an extension of talk that uses the same protocol backbone but allows for multiple chatters. Winsock talk and ytalk clients can be found at Stroud's list.

MUD, MUSH, and MOO Systems

These are very nearly entire operating systems meant to be accessed by the telnet command. The earliest of these was the MUD-Multi-User Dungeon-which started as a role-playing game system. These environments are vast and offer features far beyond any other chat system. However, their telnet interfaces often leave much to be desired.

Summary

As long as you have the system resources, I recommend putting a chat room into your Web site. They're technically impressive, they can promote a real feeling of community in your Web site, and they tend to draw crowds-and hits are the name of the game in this business.
When you plan out your chat room, try to keep the following things in mind:
  • Program with the end-user in mind. Make the room as functional-yet-simple as possible.
  • Plan a data structure for postings and users that allows for flexible data management (for example, an ID# scheme, embedded header fields within postings, and so on).
  • Consider whether a multistate CGI or an "entry page" scheme is more appropriate for what you're trying to accomplish with your chat room.
You'll want to plan out a feature-set that best serves your users. This might include blocking options or maybe schemes that prevent people from using the same handle. You might want to build a "registered user database" with a login system or maybe an http cookie server instead. Remember that chat rooms are meant to be used by many people simultaneously-you'll likely need to build a file-locking mechanism. Maybe you'll want to be a ChatMaster in your own house. Private messaging is always fun, too.
Chat rooms are an exceptionally rich field to explore in terms of the things you (or others) might want them to do. Of course, each new feature means more work for the programmer, but that's what we're paid for, right?
Chat rooms don't exist in a vacuum, either. Given their complexity, it's understandable that you're bound to come up against a lot of server-specific issues. Don't fight it-configuration can be a powerful ally if you respect how they're meant to be used.
Finally, don't be afraid to consider other options to CGI chat rooms. You might find that, for your needs, something else does a better job. However you decide to approach the job, the best piece of advice I can give you is to plan ahead, and that includes planning for the "unplanable"-don't paint yourself into a corner!