diff -urN xpilot-4.U.3/README.rank xpilot-4.U.3.svan/README.rank
--- xpilot-4.U.3/README.rank	Thu Jan  1 01:00:00 1970
+++ xpilot-4.U.3.svan/README.rank	Mon Mar 29 02:52:05 1999
@@ -0,0 +1,41 @@
+
+How to use the server.
+
+I suppose the important part is the rankingstuff.
+You control it with two different environmentvariables:
+XPILOTRANKINGPAGE and XPILOTSCOREFILE
+
+if XPILOTSCOREFILE is set and contains a filename then the server
+will remember the scores of the 400 most recent players and use that
+file to save the scores between server uptimes.
+
+if XPILOTRANKINGPAGE is set and contains a filename then the server
+will write a webpage with the rankings in that file.
+
+example: on svan I have:
+export XPILOTSCOREFILE=~/lib/xpilot/scores
+export XPILOTRANKINGPAGE=~/public_html/xpilot/ranks.html
+
+To reset the scores, simply delete the XPILOTSCOREFILE file.
+
+There is another variable you can use: XPILOTSERVERMOTD points to the
+motd-file to use.
+Example: on svan I have
+export XPILOTSERVERMOTD=~/lib/xpilot/servermotd
+
+This is just in case the server would not be able to find it otherwise.
+(I compile and run svan on two different systems)
+
+
+Also, the server is slightly modified by me so that the ballexplosions
+hopefully won't kill you. Which reminds me that this server is basically
+aimed at Blood's Music or similar maps. Which does not mean that it won't
+work with other maps, just that ... well, whatever.
+The server should also automatically pause inactive players but I do not think
+that works very well.
+
+And I know there are bugs in the code that counts deaths/kills'n'stuff, cause
+sometimes you get wierd "Deadliest player" values.
+Guess if I'm gonna root them out.
+
+Anything else I've forgotten.
diff -urN xpilot-4.U.3/src/server/Imakefile xpilot-4.U.3.svan/src/server/Imakefile
--- xpilot-4.U.3/src/server/Imakefile	Sun Aug 30 14:15:34 1998
+++ xpilot-4.U.3.svan/src/server/Imakefile	Mon Mar 29 02:30:11 1999
@@ -40,13 +40,13 @@
 	cannon.c cmdline.c collision.c contact.c event.c frame.c id.c map.c \
 	metaserver.c netserver.c objpos.c option.c play.c player.c \
 	robot.c robotdef.c rules.c saudio.c sched.c server.c \
-	update.c walls.c
+	update.c walls.c rank.c
 
 OBJS = \
 	cannon.o cmdline.o collision.o contact.o event.o frame.o id.o map.o \
 	metaserver.o netserver.o objpos.o option.o play.o player.o \
 	robot.o robotdef.o rules.o saudio.o sched.o server.o \
-	update.o walls.o
+	update.o walls.o rank.o
 
 DEPLIBS = ../common/libxpcommon.a 
 
diff -urN xpilot-4.U.3/src/server/collision.c xpilot-4.U.3.svan/src/server/collision.c
--- xpilot-4.U.3/src/server/collision.c	Mon Mar 29 00:59:31 1999
+++ xpilot-4.U.3.svan/src/server/collision.c	Mon Mar 29 02:30:11 1999
@@ -400,7 +400,7 @@
 {
     player	*pl = Players[ind];
 
-    pl->score += (points);
+    AddScore(pl,points);
 
     if (pl->conn != NOT_CONNECTED)
 	Send_score_object(pl->conn, points, x, y, msg);
@@ -584,7 +584,7 @@
 			sound_play_sensors(Players[j]->pos.x,
 					   Players[j]->pos.y,
 					   PLAYER_RAN_OVER_PLAYER_SOUND);
-			pl->kills++;
+			AddKill(pl);
 			if (IS_TANK_IND(i)) {
 			    sc = (int)floor(Rate(Players[i_tank_owner]->score,
 					    Players[j]->score)
@@ -611,7 +611,7 @@
 			Set_message(msg);
 			sound_play_sensors(pl->pos.x, pl->pos.y,
 					   PLAYER_RAN_OVER_PLAYER_SOUND);
-			Players[j]->kills++;
+			AddKill(Players[j]);
 			sc = (int)floor(Rate(Players[j]->score, pl->score)
 				   * runoverKillScoreMult);
 			Score_players(j_tank_owner, sc, pl->name,
@@ -666,6 +666,7 @@
 		    pl->ball = NULL;
 		    sound_play_sensors(pl->pos.x, pl->pos.y,
 				       CONNECT_BALL_SOUND);
+		    pl->grabbedBallFrame = main_loops;
 		}
 	    }
 	} else {
@@ -950,7 +951,7 @@
 			  OBJ_Y_IN_BLOCKS(pl),
 			  Players[killer]->name);
 		} else {
-		    Players[killer]->kills++;
+		    AddBallKill(Players[killer]);
 		    sc = (int)floor(Rate(Players[killer]->score, pl->score)
 			       * ballKillScoreMult);
 		    Score_players(killer, sc, pl->name,
@@ -1220,7 +1221,7 @@
 			      OBJ_Y_IN_BLOCKS(pl),
 			      (killer == -1) ? "[Explosion]" : pl->name);
 		    } else {
-			Players[killer]->kills++;
+			AddKill(Players[killer]);
 			sc = (int)floor(Rate(Players[killer]->score, pl->score)
 				   * explosionKillScoreMult);
 			Score_players(killer, sc, pl->name,
@@ -1362,7 +1363,7 @@
 			      Players[killer]->name);
 		    } else {
 			DFLOAT factor;
-			Players[killer]->kills++;
+			AddKill(Players[killer]);
 			switch (obj->type) {
 			case OBJ_SHOT:
 			    if (BIT(obj->mods.warhead, CLUSTER)) {
@@ -1712,7 +1713,7 @@
 						   PLAYER_ROASTED_SOUND);
 				Set_message(msg);
 				if (pl && pl->id != vic->id) {
-				    pl->kills++;
+				    AddLaserKill(pl);
 				    Robot_war(victims[j].ind, ind);
 				}
 			    }
diff -urN xpilot-4.U.3/src/server/contact.c xpilot-4.U.3.svan/src/server/contact.c
--- xpilot-4.U.3/src/server/contact.c	Sat Aug 29 21:49:54 1998
+++ xpilot-4.U.3.svan/src/server/contact.c	Mon Mar 29 02:30:11 1999
@@ -197,7 +197,8 @@
     for (i = NumPlayers - 1; i >= 0; i--) {
 	if (Players[i]->conn != NOT_CONNECTED
 	    && BIT(Players[i]->status, PAUSE)
-	    && (team == TEAM_NOT_SET || Players[i]->team == team)) {
+	    && (team == TEAM_NOT_SET || Players[i]->team == team)
+	    && !Players[i]->isoperator) {
 	    if (team == TEAM_NOT_SET) {
 		sprintf(msg,
 			"The paused \"%s\" was kicked because the game is full.",
diff -urN xpilot-4.U.3/src/server/event.c xpilot-4.U.3.svan/src/server/event.c
--- xpilot-4.U.3/src/server/event.c	Mon Mar 29 00:59:31 1999
+++ xpilot-4.U.3.svan/src/server/event.c	Mon Mar 29 02:30:12 1999
@@ -220,6 +220,7 @@
     int			i;
 
     if (onoff != 0 && !BIT(pl->status, PAUSE)) { /* Turn pause mode on */
+        Detach_ball(ind, -1);
         Swappers[pl->team]=-1;
 	pl->count = 10*FPS;
 	pl->updateVisibility = 1;
@@ -227,6 +228,7 @@
 	SET_BIT(pl->status, PAUSE);
 	pl->mychar = 'P';
 	updateScores = true;
+	strcpy(pl->scorenode->logout, "paused");
 	if (BIT(pl->have, OBJ_BALL))
 	    Detach_ball(ind, -1);
     }
@@ -234,6 +236,7 @@
 	if (pl->count <= 0) {
 	    bool toolate = false;
 
+	    pl->idleCount = 0; /* idle */
 	    CLR_BIT(pl->status, PAUSE);
 	    updateScores = true;
 	    if (BIT(pl->mode, LIMITED_LIVES)) {
@@ -249,6 +252,7 @@
 		    }
 		}
 	    }
+	    strcpy(pl->scorenode->logout, "playing");
 	    if (toolate) {
 		pl->life = 0;
 		pl->mychar = 'W';
diff -urN xpilot-4.U.3/src/server/global.h xpilot-4.U.3.svan/src/server/global.h
--- xpilot-4.U.3/src/server/global.h	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/global.h	Mon Mar 29 02:30:12 1999
@@ -251,6 +251,10 @@
 extern int              timerResolution;
 extern char             *password;
 extern int              numberOfRounds;
+
+extern int		nBallRunsShown;
+extern bool		rankBallRuns;
+
 #endif
 
 #endif /* GLOBAL_H */
diff -urN xpilot-4.U.3/src/server/netserver.c xpilot-4.U.3.svan/src/server/netserver.c
--- xpilot-4.U.3/src/server/netserver.c	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/netserver.c	Mon Mar 29 04:11:10 1999
@@ -159,6 +159,7 @@
 #include "saudio.h"
 #include "checknames.h"
 #include "server.h"
+#include "rank.h"
 
 char netserver_version[] = VERSION;
 
@@ -954,6 +955,30 @@
  * and if this succeeds update the player information
  * to all connected players.
  */
+static void LegalizeName(char *string)
+{
+    while ( *string != '\0' ) {
+	char ch = *string;
+	if ( ch == ' ' )
+	    ch = 0xA0;
+	else if ( ch == '\"' )
+	    ch = '\'';
+	else if ( !isprint(ch) )
+	    ch = '.';
+	string++;
+    }
+}
+
+static void LegalizeHost(char *string)
+{
+    while ( *string != '\0' ) {
+	char ch = *string;
+	if ( !isalnum(ch) && ch != '.' )
+	    ch = '.';
+	string++;
+    }
+}
+
 static int Handle_login(int ind)
 {
     connection_t	*connp = &Conn[ind];
@@ -1007,9 +1032,13 @@
 	return -1;
     }
     pl = Players[NumPlayers];
+    strcpy(pl->illegalName, connp->nick);
     strcpy(pl->name, connp->nick);
     strcpy(pl->realname, connp->real);
     strcpy(pl->hostname, connp->host);
+    LegalizeName(pl->name);
+    LegalizeName(pl->realname);
+    LegalizeHost(pl->hostname);
     pl->isowner = (!strcmp(pl->realname, Server.name) &&
 		   !strcmp(connp->addr, "127.0.0.1"));
     if (connp->team != TEAM_NOT_SET) {
@@ -1089,6 +1118,10 @@
 		pl->name, pl->realname, World.name, World.author);
     }
     Set_message(msg);
+    if (getenv("XPILOTGREETING") != NULL) {
+	sprintf(msg, "%s", getenv("XPILOTGREETING"));
+	Set_player_message(pl, msg);
+    }
 
     conn_bit = (1 << ind);
     for (i = 0; i < World.NumCannons; i++) {
@@ -1153,6 +1186,14 @@
 	Set_message(msg);
     }
 
+    for (i = 0;  /* idle */
+	  i < NumPlayers;
+	  i++ )
+	if ( Players[i]->mychar == ' ' ) 
+	    Players[i]->idleCount = 0;
+
+    Get_saved_score(pl); /* ranking */
+
     return 0;
 }
 
@@ -1485,7 +1526,7 @@
 		      "%c%hd" "%c%c" "%s%s%s" "%S",
 		      PKT_PLAYER, pl->id,
 		      pl->team, pl->mychar,
-		      pl->name, pl->realname, pl->hostname,
+		      pl->illegalName, pl->realname, pl->hostname,
 		      buf);
     if (connp->version > 0x3200) {
 	if (n > 0) {
@@ -1931,6 +1972,7 @@
 	pl = Players[GetInd[connp->id]];
 	memcpy(pl->last_keyv, connp->r.ptr, size);
 	connp->r.ptr += size;
+	Players[GetInd[connp->id]]->idleCount = 0; /* idle */
 	Handle_keyboard(GetInd[connp->id]);
     }
     if (connp->num_keyboard_updates++ && (connp->state & CONN_PLAYING)) {
@@ -2440,6 +2482,8 @@
     char		*cp,
 			msg[MSG_LEN * 2];
 
+    pl->flooding += FPS/3;
+
     if ((cp = strchr (str, ':')) == NULL
 	|| cp == str
 	|| strchr("-_~)(/\\}{[]", cp[1])	/* smileys are smileys */
@@ -2789,7 +2833,7 @@
       break;
       
     case VERSION_CMD:
-      sprintf(msg,"Xpilot 4.1.0 + patch 4.U.3");
+      sprintf(msg,"Xpilot 4.1.0 + patch 4.U.3 + Svan ranking");
       break;
       
     case HELP_CMD:
@@ -2830,17 +2874,31 @@
 	return;
       }
       
-    case PASSWORD_CMD:
-      if (!password || !args || strcmp(args,password))
+    case PASSWORD_CMD: {
+      char salt[3];
+      
+      if (!password || !args)
 	sprintf(msg,"Wrong.");
       else {
-	if (!pl->isoperator)
-	  NumOperators++;
-	pl->isoperator=1;
-	sprintf(msg,"You got operator status.");
+        if (strlen(password) < 2) {
+		sprintf(msg,"Bogus password in server.");
+		break;
+	}
+	salt[0] = password[0];
+	salt[1] = password[1];
+	salt[2] = '\0';
+	if (strcmp(crypt(args,salt),password)) {
+		sprintf(msg,"Wrong.");
+	} else {
+	  if (!pl->isoperator)
+	     NumOperators++;
+	  pl->isoperator=1;
+	  sprintf(msg,"You got operator status.");
+	}
       }
+    }
       break;
-      
+
     case LOCK_CMD:
       if (!args)
 	sprintf(msg,"The game is currently %s.",game_lock?"locked":"unlocked");
@@ -3148,7 +3206,11 @@
 
 	motd_loops = main_loops;
 
-	if ((fd = open(SERVERMOTDFILE, O_RDONLY)) == -1) {
+#define XPILOTSERVERMOTD	"XPILOTSERVERMOTD"
+	if ((fd = open(getenv(XPILOTSERVERMOTD) ?
+		       getenv(XPILOTSERVERMOTD)
+		       : SERVERMOTDFILE,
+		       O_RDONLY)) == -1) {
 	    motd_size = 0;
 	    return -1;
 	}
diff -urN xpilot-4.U.3/src/server/object.h xpilot-4.U.3.svan/src/server/object.h
--- xpilot-4.U.3/src/server/object.h	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/object.h	Mon Mar 29 02:34:03 1999
@@ -50,7 +50,10 @@
 #include "NT/winNet.h"
 #endif
 
-/*
+#include "rank.h"
+#include "rules.h"
+ 
+ /*
  * Different types of objects, including player.
  * Robots and tanks are players but have an additional bit.
  * Smart missile, heatseeker and torpedoe can be merged into missile.
@@ -378,6 +381,7 @@
     char	mychar;			/* Special char for player */
     char	prev_mychar;		/* Special char for player */
     char	name[MAX_CHARS];	/* Nick-name of player */
+    char	illegalName[MAX_CHARS];	/* Nick-name of player */
     char	realname[MAX_CHARS];	/* Real name of player */
     char	hostname[MAX_CHARS];	/* Hostname of client player uses */
     u_short	pseudo_team;		/* Which team is used for my tanks */
@@ -425,6 +429,57 @@
 #ifdef __cplusplus
 		player() {}
 #endif
+
+    int		idleCount;		/* idle */
+    short	flooding;
+    ScoreNode * scorenode;
+
+    int	 grabbedBallFrame;
 };
+
+/* Converted from C++ */
+#define StartBallRun(pl) \
+	{ (pl)->grabbedBallFrame = main_loops; }
+#define AbortBallRun(pl) \
+	{ (pl)->grabbedBallFrame = -1; }
+#define ClearKills(pl)	{ (pl)->kills = 0; }
+
+#define ClearDeaths(pl)	\
+	{ if ((pl)->scorenode) (pl)->deaths = 0; }
+#define IgnoreLastDeath(pl) \
+	{ if ((pl)->scorenode) (pl)->scorenode->deaths--;}
+
+#define AddRound(pl)	{ if ((pl)->scorenode) (pl)->scorenode->rounds++; }
+
+#define FireShot(pl)	{ if ((pl)->scorenode) (pl)->scorenode->firedShots++; }
+
+#define SavedBall(pl)	{ if ((pl)->scorenode) (pl)->scorenode->ballsSaved++; }
+#define LostBall(pl)	{ if ((pl)->scorenode) (pl)->scorenode->ballsLost++; }
+#define CashedBall(pl)	{ if ((pl)->scorenode) (pl)->scorenode->ballsCashed++;}
+#define WonBall(pl)	{ if ((pl)->scorenode) (pl)->scorenode->ballsWon++; }
+
+#define AddDeath(pl) { \
+	(pl)->deaths++; \
+	if ((pl)->scorenode) (pl)->scorenode->deaths++; \
+}
+
+#define AddKill(pl) { \
+	(pl)->kills++; \
+	if ((pl)->scorenode) (pl)->scorenode->kills++; \
+}
+
+#define AddBallKill(pl) { AddKill(pl); }
+#define AddTargetKill(pl) { AddKill(pl); }
+#define AddLaserKill(pl) { AddKill(pl); }
+
+#define AddScore(pl,add) { \
+	(pl)->score += add; \
+	if ((pl)->scorenode) (pl)->scorenode->score += add; \
+}
+
+#define SetScore(pl,newScore) { \
+	(pl)->score = newScore; \
+	if ((pl)->scorenode) (pl)->scorenode->score = newScore; \
+}
 
 #endif
diff -urN xpilot-4.U.3/src/server/play.c xpilot-4.U.3.svan/src/server/play.c
--- xpilot-4.U.3/src/server/play.c	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/play.c	Mon Mar 29 03:04:36 1999
@@ -1002,8 +1002,7 @@
 		|| (BIT(Players[i]->status, PAUSE)
 		    && Players[i]->count <= 0)
 		|| (BIT(Players[i]->status, GAME_OVER)
-		    && Players[i]->mychar == 'W'
-		    && Players[i]->score == 0)) {
+		    && Players[i]->mychar == 'W')) {
 		continue;
 	    }
 	    if (Players[i]->team == td->team) {
@@ -1051,18 +1050,23 @@
 	if (Players[i]->team == td->team) {
 	    SCORE(i, -sc, tt->pos.x, tt->pos.y,
 		  "Treasure: ");
+	    LostBall(Players[i]);
 	    if (treasureKillTeam)
 		SET_BIT(Players[i]->status, KILLED);
 	}
 	else if (Players[i]->team == tt->team &&
 		 (Players[i]->team != TEAM_NOT_SET || i == ind)) {
+ 	    if (i == ind && lose_team_members > 0) {
+		    CashedBall(Players[i]);
+	    }
+ 	    WonBall(Players[i]);
 	    SCORE(i, (i == ind ? 3*por : 2*por), tt->pos.x, tt->pos.y,
 		  "Treasure: ");
 	}
     }
 
     if (treasureKillTeam) {
-	Players[ind]->kills++;
+	AddKill(Players[ind]);
     }
 
     updateScores = true;
@@ -1286,6 +1290,7 @@
 	    Add_fuel(&(pl->fuel), (long)(ED_SHOT));
 	    sound_play_sensors(pl->pos.x, pl->pos.y, FIRE_SHOT_SOUND);
 	    pl->shots++;
+	    FireShot(pl);
 	}
 	if (!ShotsGravity) {
 	    CLR_BIT(status, GRAVITY);
diff -urN xpilot-4.U.3/src/server/player.c xpilot-4.U.3.svan/src/server/player.c
--- xpilot-4.U.3/src/server/player.c	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/player.c	Mon Mar 29 02:30:12 1999
@@ -44,6 +44,7 @@
 #include "saudio.h"
 #include "error.h"
 #include "objpos.h"
+#include "rank.h"
 
 char player_version[] = VERSION;
 
@@ -441,8 +442,12 @@
     pl->player_round	= 0;
     pl->player_count	= 0;
 
-    pl->kills		= 0;
-    pl->deaths		= 0;
+    ClearKills(pl);
+    ClearDeaths(pl);
+
+    pl->idleCount = 0;
+    pl->flooding = -1;
+    pl->grabbedBallFrame = -1;
 
     /*
      * If limited lives and if nobody has lost a life yet, you may enter
@@ -603,12 +608,15 @@
 		    i--;
 		    continue;
 		}
+		IgnoreLastDeath(pl);
 	    }
 	}
 	CLR_BIT(pl->status, GAME_OVER);
 	CLR_BIT(pl->have, OBJ_BALL);
-	pl->kills = 0;
-	pl->deaths = 0;
+	ClearKills(pl);
+	ClearDeaths(pl);
+	if ( !BIT(pl->status, PAUSE) && pl->mychar != 'W' )
+		AddRound(pl);
 	pl->round = 0;
 	pl->check = 0;
 	pl->time = 0;
@@ -961,6 +969,8 @@
     Count_rounds();
 
     free(best_players);
+
+    Rank_score(); Show_ranks();  /* ranking */
 }
 
 void Individual_game_over(int winner)
@@ -1116,6 +1126,8 @@
 	    Kill_player(i);
 	else
 	    Player_death_reset(i);
+	IgnoreLastDeath(pl);
+
 	if (pl != Players[i]) {
 	    continue;
 	}
@@ -1683,6 +1695,14 @@
 	NumPseudoPlayers--;
     }
 
+    else if ( IS_HUMAN_PTR(pl) ) { /* ranking */
+	Save_score(pl);
+	if ( NumPlayers == NumRobots + NumPseudoPlayers ) {
+	    Rank_score();
+	    Print_saved_scores();
+	}
+    }
+
     if (pl->team != TEAM_NOT_SET && !IS_TANK_PTR(pl)) {
 	World.teams[pl->team].NumMembers--;
 	if (IS_ROBOT_PTR(pl))
@@ -1869,7 +1889,7 @@
 	pl->life++;
     }
 
-    pl->deaths++;
+    AddDeath(pl);
 
     pl->have	= DEF_HAVE;
     pl->used	|= DEF_USED;
diff -urN xpilot-4.U.3/src/server/rank.c xpilot-4.U.3.svan/src/server/rank.c
--- xpilot-4.U.3/src/server/rank.c	Thu Jan  1 01:00:00 1970
+++ xpilot-4.U.3.svan/src/server/rank.c	Mon Mar 29 04:02:55 1999
@@ -0,0 +1,529 @@
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <time.h>
+
+#define SERVER
+#include "version.h"
+#include "config.h"
+#include "types.h"
+#include "const.h"
+#include "global.h"
+#include "proto.h"
+#include "netserver.h"
+#include "error.h"
+#include "rank.h"
+
+/* MAX_SCORES = how many players we remember */
+#define MAX_SCORES 400
+
+static const char XPILOTSCOREFILE[] = "XPILOTSCOREFILE";
+static const char XPILOTRANKINGPAGE[] = "XPILOTRANKINGPAGE";
+static const char XPILOTNOJSRANKINGPAGE[] = "XPILOTNOJSRANKINGPAGE";
+
+/* Score data */
+static ScoreNode scores[MAX_SCORES];
+static ScoreNode dummyScoreNode;
+
+static int    rankedplayer[MAX_SCORES];
+static double rankedscore[MAX_SCORES];
+static double sc_table[MAX_SCORES];
+static double kr_table[MAX_SCORES];
+static double kd_table[MAX_SCORES];
+static double hf_table[MAX_SCORES];
+static int first = 0;
+
+static void swap2(int *i1, int *i2, double *d1, double *d2)
+{
+	int i;
+	double d;
+
+	i = *i1;
+	d = *d1;
+	*i1 = *i2;
+	*d1 = *d2;
+	*i2 = i;
+	*d2 = d;
+}
+
+static char *rank_showtime(void)
+{
+    time_t		now;
+    struct tm		*tmp;
+    static char		month_names[13][4] = {
+			    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+			    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+			    "Bug"
+			};
+    static char		buf[80];
+
+    time(&now);
+    tmp = localtime(&now);
+    sprintf(buf, "%02d\xA0%s\xA0%02d:%02d:%02d",
+	    tmp->tm_mday, month_names[tmp->tm_mon],
+	    tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
+    return buf;
+}
+
+/* Here's where we calculate the ranks. Figure it out yourselves! */
+void
+SortRankings(void)
+{
+    double lowSC, highSC;
+    double lowKD, highKD;
+    double lowKR, highKR;
+    double lowHF, highHF;
+    bool   foundFirst = false;
+    int i;
+
+    /* Ok, there are two loops: the first one calculates the scores and
+       records lowest and highest scores. The second loop combines the
+       scores into a rank. I cannot do it in one loop since I need to
+       know low- and highmarks for each score before I can calculate the
+       rank. */
+    for (i = 0; i < MAX_SCORES; i++) {
+	ScoreNode *score = &scores[i];
+	double attenuation, kills, sc, kd, kr, hf;
+	if (score->nick[0] == '\0') continue;
+
+	/* The attenuation affects players with less than 300 rounds. */
+	attenuation = (score->rounds < 300) ? 
+	    ((double)score->rounds / 300.0) : 1.0;
+
+	kills = score->kills;
+	sc = (double)score->score * attenuation;
+	kd = ( (score->deaths != 0) ?
+			    (kills / (double)score->deaths) :
+			    (kills) ) * attenuation;
+	kr = ( (score->rounds != 0) ?
+			    (kills / (double)score->rounds) :
+			    (kills) ) * attenuation;
+	hf = ( (score->ballsLost != 0) ?
+			    ( (double)score->ballsCashed /
+			      (double)score->ballsLost ) :
+			    (double)score->ballsCashed ) * attenuation;
+
+	sc_table[i] = sc;
+	kd_table[i] = kd;
+	kr_table[i] = kr;
+	hf_table[i] = hf;
+
+	if ( !foundFirst ) {
+	    lowSC = highSC = sc;
+	    lowKD = highKD = kd;
+	    lowKR = highKR = kr;
+	    lowHF = highHF = hf;
+	    foundFirst = true;
+	} else {
+	    if ( sc > highSC )
+		highSC = sc;
+	    else if ( sc < lowSC )
+		lowSC = sc;
+	
+	    if ( kd > highKD )
+		highKD = kd;
+	    else if ( kd < lowKD )
+		lowKD = kd;
+	    
+	    if ( kr > highKR )
+		highKR = kr;
+	    else if ( kr < lowKR )
+		lowKR = kr;
+	    
+	    if ( hf > highHF )
+		highHF = hf;
+	    else if ( hf < lowHF )
+		lowHF = hf;	    
+	}
+    }
+
+    /* Normalize */
+    highSC -= lowSC;
+    highKD -= lowKD;
+    highKR -= lowKR;
+    highHF -= lowHF;
+
+    {
+	const double factorSC = (highSC != 0.0) ? (100.0 / highSC) : 0.0;
+	const double factorKD = (highKD != 0.0) ? (100.0 / highKD) : 0.0;
+	const double factorKR = (highKR != 0.0) ? (100.0 / highKR) : 0.0; 
+	const double factorHF = (highHF != 0.0) ? (100.0 / highHF) : 0.0;
+	int i;
+
+	for (i = 0; i < MAX_SCORES; i++) {
+		ScoreNode *score = &scores[i];
+		double sc, kd, kr, hf, rsc, rkd, rkr, rhf, rank;
+		rankedplayer[i] = i;
+		if (score->nick[0] == '\0') {
+			rankedscore[i] = -1;
+			continue;
+		}
+
+		sc = sc_table[i];
+		kd = kd_table[i];
+		kr = kr_table[i];
+		hf = hf_table[i];
+
+		rsc = (sc - lowSC) * factorSC;
+		rkd = (kd - lowKD) * factorKD;
+		rkr = (kr - lowKR) * factorKR;
+		rhf = (hf - lowHF) * factorHF;
+
+		rank = 0.20*rsc + 0.30*rkd + 0.30*rkr + 0.20*rhf;
+		rankedscore[i] = rank;
+	}
+
+	/* And finally we sort the ranks, using some lame N^2 sort.. wheee! */
+	for (i = 0; i < MAX_SCORES; i++ ) {
+	    int j;
+	    for (j = i+1; j < MAX_SCORES; j++) {
+		if ( rankedscore[i] < rankedscore[j] )
+		    swap2(&rankedplayer[i], &rankedplayer[j],
+			  &rankedscore[i], &rankedscore[j]);
+	    }
+	}
+    }
+}
+
+/* Sort the ranks and save them to the webpage. */
+void
+Rank_score(void)
+{
+    static const char HEADER[] =
+	"<html><head><title>XPilot @ *.e.kth.se</title>\n"
+
+	/* In order to save space/bandwidth, the table is saved as one */
+	/* giant javascript file, instead of writing all the <TR>, <TD>, etc */
+	"<SCRIPT language=\"Javascript\">\n<!-- Hide script\n"
+	"function g(nick, score, kills, deaths, rounds, shots, ballsCashed, "
+	"           ballsSaved, ballsWon, ballsLost, ratio, user, log) {\n"
+	"document.write('<tr><td align=left><tt>', i, '</tt></td>');\n"
+	"document.write('<td align=left><b>', nick, '</b></td>');\n"
+	"document.write('<td align=right>', score, '</td>');\n"
+	"document.write('<td align=right>', kills, '</td>');\n"
+	"document.write('<td align=right>', deaths, '</td>');\n"
+	"document.write('<td align=right>', rounds, '</td>');\n"
+	"document.write('<td align=right>', shots, '</td>');\n"
+	"document.write('<td align=center>', ballsCashed);\n"
+	"document.write('/', ballsSaved, '/', ballsWon, '/', ballsLost);\n"
+	"document.write('</td>');\n"
+	"document.write('<td align=right>', ratio, '</td>');\n"
+	"document.write('<td align=center>', user, '</td>');\n"
+	"document.write('<td align=center>', log, '</td>');\n"
+	"document.write('</tr>\\n');\n"
+	"i = i + 1\n"
+	"}\n// Hide script --></SCRIPT>\n"
+
+	"</head><body>\n"
+
+	/* Head of page */
+	"<h1>XPilot @ *.e.kth.se</h1>"
+
+	"<a href=\"previous_ranks.html\">Previous rankings</a> "
+	"<a href=\"rank_explanation.html\">How does the ranking "
+	"work?</a><hr>\n"
+
+	"<noscript>"
+        "<blink><h1>YOU MUST HAVE JAVASCRIPT FOR THIS PAGE</h1></blink>"
+	"Please go <A href=\"index_nojs.html\">here</A> for the non-js page"
+	"</noscript>\n"
+
+	"<table><tr><td></td>" /* First column is the position 1..400 */
+	"<td align=left><h1><u><b>Player</b></u></h1></td>"
+	"<td align=right><h1><u><b>Score</b></u></h1></td>"
+	"<td align=right><h1><u><b>Kills</b></u></h1></td>"
+	"<td align=right><h1><u><b>Deaths</b></u></h1></td>"
+	"<td align=right><h1><u><b>Rounds</b></u></h1></td>"
+	"<td align=right><h1><u><b>Shots</b></u></h1></td>"
+	"<td align=center><h1><u><b>Balls</b></u></h1></td>"
+	"<td align=right><h1><u><b>Ratio</b></u></h1></td>"
+	"<td align=center><h1><u><b>User @ Host</b></u></h1></td>"
+	"<td align=center><h1><u><b>Logout</b></u></h1></td>"
+	"</tr>\n"
+
+	"<SCRIPT language=\"Javascript\">\n"
+	"var i = 1\n"
+	;
+
+    static const char HEADERNOJS[] =
+	"<html><head><title>XPilot @ *.e.kth.se</title>\n"
+	"</head><body>\n"
+
+	/* Head of page */
+	"<h1>XPilot @ *.e.kth.se</h1>"
+
+	"<a href=\"previous_ranks.html\">Previous rankings</a> "
+	"<a href=\"rank_explanation.html\">How does the ranking "
+	"work?</a><hr>\n"
+
+	"<table><tr><td></td>" /* First column is the position 1..400 */
+	"<td align=left><h1><u><b>Player</b></u></h1></td>"
+	"<td align=right><h1><u><b>Score</b></u></h1></td>"
+	"<td align=right><h1><u><b>Kills</b></u></h1></td>"
+	"<td align=right><h1><u><b>Deaths</b></u></h1></td>"
+	"<td align=right><h1><u><b>Rounds</b></u></h1></td>"
+	"<td align=right><h1><u><b>Shots</b></u></h1></td>"
+	"<td align=center><h1><u><b>Balls</b></u></h1></td>"
+	"<td align=right><h1><u><b>Ratio</b></u></h1></td>"
+	"<td align=center><h1><u><b>User @ Host</b></u></h1></td>"
+	"<td align=center><h1><u><b>Logout</b></u></h1></td>"
+	"</tr>\n"
+	;
+
+    static const char FOOTER[] = 
+	"</table>"
+	"<i>Explanation for ballstats</i>:<br>"
+	"The numbers are c/s/w/l, where<br>"
+	"c = The number of enemy balls you have cashed.<br>"
+	"s = The number of your own balls you have returned.<br>"
+	"w = The number of enemy balls your team has cashed.<br>"
+	"l = The number of your own balls you have lost.<br>"      
+	"<hr>%s<BR>\n\n" /* <-- Insert time here. */
+
+	"</body></html>"
+	;
+
+    SortRankings();
+    
+    if (getenv(XPILOTRANKINGPAGE) != NULL) {
+	FILE * const file = fopen(getenv(XPILOTRANKINGPAGE), "w");
+	if (file != NULL && fseek(file, 2000, SEEK_SET) == 0) {
+	    int i;
+	    fprintf(file, "%s", HEADER);
+	    for (i = 0; i < MAX_SCORES; i++) {
+		const int j = rankedplayer[i];
+		const ScoreNode *score = &scores[j];
+		if ( score->nick[0] != '\0' ) {
+		    fprintf(file, "g(\"%s\", %d, %u, %u, %u, %u, "
+			    "%u, %u, %u, %u, %.1f, \"%s@%s\", '%s');\n",
+			    score->nick,
+			    score->score,
+			    score->kills,
+			    score->deaths,
+			    score->rounds,
+			    score->firedShots,
+			    score->ballsCashed, score->ballsSaved,
+			    score->ballsWon, score->ballsLost,
+			    rankedscore[i],
+			    score->real, score->host,
+			    score->logout);
+		}
+	    }
+	    fprintf(file, "</script>");
+	    fprintf(file, FOOTER, rank_showtime());
+	    fclose(file);
+	} else
+	    error("Could not open the rank file.");
+    }
+    if (getenv(XPILOTNOJSRANKINGPAGE) != NULL) {
+	FILE * const file = fopen(getenv(XPILOTNOJSRANKINGPAGE), "w");
+	if (file != NULL && fseek(file, 2000, SEEK_SET) == 0) {
+	    int i;
+	    fprintf(file, "%s", HEADERNOJS);
+	    for (i = 0; i < MAX_SCORES; i++) {
+		const int j = rankedplayer[i];
+		const ScoreNode *score = &scores[j];
+		if ( score->nick[0] != '\0' ) {
+		    fprintf(file, 
+			    "<tr><td align=left><tt>%d</tt>"
+			    "<td align=left><b>%s</b>"
+			    "<td align=right>%d"
+			    "<td align=right>%u"
+			    "<td align=right>%u"
+			    "<td align=right>%u"
+			    "<td align=right>%u"
+			    "<td align=center>%u/%u/%u/%u"
+			    "<td align=right>%.1f"
+			    "<td align=center>%s@%s"
+			    "<td align=center>%s\n"
+			    "</tr>\n",
+			    i+1,
+			    score->nick,
+			    score->score,
+			    score->kills,
+			    score->deaths,
+			    score->rounds,
+			    score->firedShots,
+			    score->ballsCashed, score->ballsSaved,
+			    score->ballsWon, score->ballsLost,
+			    rankedscore[i],
+			    score->real, score->host,
+			    score->logout);
+		}
+	    }
+	    fprintf(file, FOOTER, rank_showtime());
+	    fclose(file);
+	} else
+	    error("Could not open the rank file.");
+    }
+}
+
+/* Send a line with the ranks of the current players to the game. */
+void
+Show_ranks(void)
+{
+    char buf[1000] = "";
+    int i;
+
+    for (i = 0; i < MAX_SCORES; i++) {
+	ScoreNode *score = &scores[rankedplayer[i]];
+	if ( score->pl != 0 ) {
+	    char msg[MSG_LEN];
+	    sprintf(msg, "%s [%d], ", score->nick, i+1);
+	    strcat(buf, msg);
+	}
+    }
+
+    Set_message(buf);
+    return;
+}
+
+static void
+Init_scorenode(ScoreNode *node,
+	       const char  nick[],
+	       const char  real[],
+	       const char  host[])
+{
+    strcpy(node->nick, nick);
+    strcpy(node->real, real);
+    strcpy(node->host, host);
+    strcpy(node->logout, "");
+    node->score = 0;
+    node->kills = 0;
+    node->deaths = 0;
+    node->rounds = 0;
+    node->firedShots = 0;
+    node->ballsSaved = 0;
+    node->ballsLost = 0;
+    node->ballsCashed = 0;
+    node->ballsWon = 0;
+    node->pl = 0;
+}
+
+/* Read scores from disk, and zero-initialize the ones that are not used.
+   Call this on startup. */
+void
+Init_saved_scores(void)
+{
+    int i = 0;
+
+    if ( getenv(XPILOTSCOREFILE) != NULL ) {
+	FILE *file = fopen(getenv(XPILOTSCOREFILE), "r");
+	if ( file != NULL ) {
+
+	    const int actual = fread(scores, sizeof(ScoreNode),
+				     MAX_SCORES, file);
+	    if ( actual != MAX_SCORES )
+		error("Error when reading score file!\n");
+
+	    i += actual;
+
+	    fclose(file);
+	}
+    }
+
+    while ( i < MAX_SCORES ) {
+	Init_scorenode(&scores[i], "", "", "");
+	scores[i].timestamp = 0;
+	memset(scores[i].futureextensions,0,sizeof scores[i].futureextensions);
+	i++;
+    }
+}
+
+/* A player has logged in. Find his info or create new info by kicking
+   the player who hasn't played for the longest time. */
+void
+Get_saved_score(player *pl)
+{
+    ScoreNode *score;
+    int oldest = 0;
+    int i;
+    updateScores = true;
+
+    for (i = 0; i < MAX_SCORES; i++) {
+	score = &scores[i];
+	if ( strcmp(pl->name, score->nick) == 0 )
+	    if ( score->pl == 0 ) {
+		/* Ok, found it. */
+		score->pl = pl;
+		strcpy(score->logout, "playing");
+		pl->score = score->score;
+		pl->scorenode = score;
+		return;
+	    } else {
+		/* That scorenode is already in use by another player! */
+		pl->score = 0;
+		pl->scorenode = &dummyScoreNode;
+		return;
+	    }
+	else if ( score->timestamp < scores[oldest].timestamp )
+	    oldest = i;
+    }
+
+    /* Didn't find it, use the least-recently-used node. */
+    score = &scores[oldest];
+
+    Init_scorenode(score, pl->name, pl->realname, pl->hostname);
+    strcpy(score->logout, "playing");
+    score->pl = pl;
+    score->timestamp = time(0);
+    pl->score = 0;
+    pl->scorenode = score;
+}
+
+/* A player has quit, save his info and mark him as not playing. */
+void
+Save_score(const player *pl)
+{
+    ScoreNode *score = pl->scorenode;
+    score->score = pl->score;
+    strcpy(score->logout, rank_showtime());
+    score->pl = 0;
+    score->timestamp = time(0);
+}
+
+/* Save the scores to disk (not the webpage). */
+void
+Print_saved_scores(void)
+{
+    FILE * file = NULL;
+
+    Rank_score();
+
+    if ( getenv(XPILOTSCOREFILE) != NULL &&
+	 (file = fopen(getenv(XPILOTSCOREFILE), "w")) != NULL ) {
+
+	const int actual = fwrite(scores, sizeof(ScoreNode),
+				  MAX_SCORES, file); 
+	if ( actual != MAX_SCORES )
+	    error("Error when writing score file!\n");
+	
+	fclose(file);
+    }
+}
+
+/* This function checks wether the strings contains certain characters
+   that might be hazardous to include on a webpage (ie they screw it up). */
+bool
+IsLegalNameUserHost(const char string[])
+{
+    const int length = strlen(string);
+    int i;
+
+    for (i = 0; i < length; i++ )
+	switch ( string[i] ) {
+	case '<':
+	case '>':
+	case '\t':
+	    return false;
+	default:
+	    break;
+	}
+    return true;
+}
diff -urN xpilot-4.U.3/src/server/rank.h xpilot-4.U.3.svan/src/server/rank.h
--- xpilot-4.U.3/src/server/rank.h	Thu Jan  1 01:00:00 1970
+++ xpilot-4.U.3.svan/src/server/rank.h	Fri Mar 26 05:48:01 1999
@@ -0,0 +1,46 @@
+#ifndef RANK_H
+#define RANK_H 1
+
+#include <string.h>
+
+struct player;
+
+typedef struct ScoreNode {
+    char nick[MAX_CHARS];
+    char real[MAX_CHARS];
+    char host[MAX_CHARS];
+    char logout[MAX_CHARS];
+    int  timestamp;
+    short score;
+    unsigned short kills;
+    unsigned short deaths;
+    unsigned short rounds;
+    unsigned long  firedShots;
+
+    unsigned short ballsSaved;
+    unsigned short ballsLost;
+    unsigned short ballsWon;
+    unsigned short ballsCashed;
+
+    char  futureextensions[8];
+    struct player *pl;
+} ScoreNode;
+
+
+#define SetLogoutMessage(node,msg) { strcpy((node)->logout, msg); }
+
+void Init_saved_scores(void);
+
+void Get_saved_score(struct player *pl);
+
+void Print_saved_scores(void);
+
+void Rank_score(void);
+
+void Save_score(const struct player * pl);
+
+void Show_ranks(void);
+
+bool IsLegalNameUserHost(const char string[]);
+
+#endif /* RANK_H */
diff -urN xpilot-4.U.3/src/server/server.c xpilot-4.U.3.svan/src/server/server.c
--- xpilot-4.U.3/src/server/server.c	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/server.c	Mon Mar 29 02:30:12 1999
@@ -69,6 +69,7 @@
 #include "error.h"
 #include "portability.h"
 #include "server.h"
+#include "rank.h"
 
 char server_version[] = VERSION;
 
@@ -157,6 +158,8 @@
 
     Robot_init();
 
+    Init_saved_scores(); /* ranking */
+
     if (BIT(World.rules->mode, TEAM_PLAY)) {
 	int i;
 	for (i=0; i < World.NumTreasures; i++)
@@ -310,6 +313,9 @@
     Meta_gone();
 
     Contact_cleanup();
+
+    Rank_score();  /* ranking */
+    Print_saved_scores();
 
     Free_players();
     Free_shots();
diff -urN xpilot-4.U.3/src/server/update.c xpilot-4.U.3.svan/src/server/update.c
--- xpilot-4.U.3/src/server/update.c	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/update.c	Mon Mar 29 02:30:12 1999
@@ -652,10 +652,39 @@
 	    }
 	}
 
+	if ( pl->flooding > FPS + 1 ) {
+	    char msg[MSG_LEN];
+	    sprintf(msg, "%s was kicked out because of flooding.", pl->name);
+	    Destroy_connection(pl->conn, "flooding");
+	    i--;
+	    continue;
+	} else if ( pl->flooding >= 0 )
+	    pl->flooding--;
+
+#define IDLETHRESHOLD (FPS * 60)
+
+	if ( IS_HUMAN_PTR(pl) ) {
+	    pl->scorenode->score = pl->score;
+	    if ( pl->mychar == ' ' ) {
+		if ( pl->idleCount++ == IDLETHRESHOLD )
+		    if ( NumPlayers - 1 > NumPseudoPlayers + NumRobots ) {
+			/* Kill player, he/she will be paused when returned
+			   to base, unless he/she wakes up. */
+			Kill_player(i);
+			IgnoreLastDeath(pl);
+		    } else
+			pl->idleCount = 0;
+	    }
+	}
+
 	if (pl->count == 0) {
 	    pl->count = -1;
 
 	    if (!BIT(pl->status, PLAYING)) {
+		if ( pl->idleCount >= IDLETHRESHOLD ) { /* idle */
+		    Pause_player(i, 1);
+		    continue;
+		}
 		SET_BIT(pl->status, PLAYING);
 		Go_home(i);
 	    }
diff -urN xpilot-4.U.3/src/server/walls.c xpilot-4.U.3.svan/src/server/walls.c
--- xpilot-4.U.3/src/server/walls.c	Mon Mar 29 00:59:32 1999
+++ xpilot-4.U.3.svan/src/server/walls.c	Mon Mar 29 02:30:12 1999
@@ -987,6 +987,7 @@
 			  tt->pos.x, tt->pos.y, "Treasure: ");
 		    sprintf(msg, " < %s (team %d) has replaced the treasure >",
 			    pl->name, pl->team);
+		    SavedBall(pl);
 		    Set_message(msg);
 		    break;
 		}
@@ -1763,7 +1764,7 @@
     Set_message(msg);
 
     if (targetKillTeam) {
-	Players[killer]->kills++;
+	AddTargetKill(Players[killer]);
     }
 
     sc  = Rate(win_score, lose_score);
