SmaugWizard/Backup/
SmaugWizard/Backup/L/
SmaugWizard/Boards/
SmaugWizard/Building/
SmaugWizard/Corpses/
SmaugWizard/Councils/
SmaugWizard/Deity/
SmaugWizard/Gods/
SmaugWizard/MudProgs/
SmaugWizard/Player/L/
SmaugWizard/Src/
SmaugWizard/Src/res/
/****************************************************************************
 * [S]imulated [M]edieval [A]dventure multi[U]ser [G]ame      |				*
 * -----------------------------------------------------------|   \\._.//	*
 * SmaugWiz (C) 1998 by Russ Pillsbury (Windows NT version)   |   (0...0)	*
 * -----------------------------------------------------------|    ).:.(	*
 * SMAUG (C) 1994, 1995, 1996 by Derek Snider                 |    {o o}	*
 * -----------------------------------------------------------|   / ' ' \	*
 * SMAUG code team: Thoric, Altrag, Blodkai, Narn, Haus,      |~'~.VxvxV.~'~*
 * Scryn, Swordbearer, Rennard, Tricops, and Gorog.           |				*
 * ------------------------------------------------------------------------ *
 * Merc 2.1 Diku Mud improvments copyright (C) 1992, 1993 by Michael        *
 * Chastain, Michael Quan, and Mitchell Tse.                                *
 * Original Diku Mud copyright (C) 1990, 1991 by Sebastian Hammer,          *
 * Michael Seifert, Hans Henrik Staerfeldt, Tom Madsen, and Katja Nyboe.    *
 * ------------------------------------------------------------------------ *
 *		     Character saving and loading module						    *
 ****************************************************************************/

#include	"stdafx.h"
#include	"smaug.h"
#include	"SysData.h"
#include	"skill.h"
#include	"mobiles.h"
#include	"objects.h"
#include	"rooms.h"
#include	"deity.h"
#include	"class.h"
#include	"SmaugWizDoc.h"
#include	"SmaugFiles.h"
#include	"descriptor.h"
#include	"character.h"


// Array to keep track of equipment temporarily.		-Thoric
CObjData *save_equipment[MAX_WEAR][8];
CCharacter *quitting_char, *loading_char, *saving_char;

int file_ver;


// Array of containers read for proper re-nesting of objects.
static	CObjData *	rgObjNest	[MAX_NEST];

// Un-equip character before saving to ensure proper	-Thoric
// stats are saved in case of changes to or removal of EQ
void de_equip_char (CCharacter *ch)
{
	CObjData	*obj;
	int			x, y;

	for (x = 0; x < MAX_WEAR; x++)
		for (y = 0; y < MAX_LAYERS; y++)
			save_equipment [x][y] = NULL;

	POSITION	pos = ch->GetHeadCarryPos ();
	while (obj = ch->GetNextCarrying (pos)) {
		if (obj->wear_loc > -1 && obj->wear_loc < MAX_WEAR) {
			if (ch->GetTrustLevel () >= obj->GetLevel ()) {
				for (x = 0; x < MAX_LAYERS; ++x)
					if (! save_equipment [obj->wear_loc][x]) {
						save_equipment [obj->wear_loc][x] = obj;
						break;
					}
				if (x == MAX_LAYERS) {
					bug ("%s had on more than %d layers of clothing in one"
						" location (%d): %s", ch->GetName (),
						MAX_LAYERS, obj->wear_loc, obj->GetName ());
				}
			} else {
				bug ("%s had on %s:  ch->GetLevel () = %d  "
					"obj->GetLevel () = %d", ch->GetName (),
					obj->GetName (), ch->GetLevel (), obj->GetLevel ());
			}
			unequip_char (ch, obj);
		}
	}
}


/*
 * Re-equip character					-Thoric
 */
void re_equip_char (CCharacter *ch)
{
    int x,y;

    for (x = 0; x < MAX_WEAR; x++)
	for (y = 0; y < MAX_LAYERS; y++)
	   if (save_equipment[x][y] != NULL)
	   {
		if (quitting_char != ch)
		   equip_char (ch, save_equipment[x][y], x);
		save_equipment[x][y] = NULL;
	   }
	   else
		break;
}


// Save a character and inventory.
// Would be cool to save NPC's too for quest purposes,
//   some of the infrastructure is provided.
void save_char_obj (CCharacter *ch)
{
	CString	strsave;
	CString	strback;
	FILE	*fp;

	if (!ch) {
		bug ("Save_char_obj: null ch!", 0);
		return;
	}

	if (ch->IsNpc () || ch->GetLevel () < 2)
		return;

	saving_char = ch;
	// save pc's clan's data while we're at it to keep the data in sync
	if (!ch->IsNpc () && ch->GetPcData ()->GetClan ())
		ch->GetPcData ()->GetClan ()->Save ();

	// save deity's data to keep it in sync -ren
	if (!ch->IsNpc () && ch->GetPcData ()->deity)
		save_deity (ch->GetPcData ()->deity);

	if (ch->GetDesc () && ch->GetDesc ()->m_pOriginal)
		ch = ch->GetDesc ()->m_pOriginal;

	de_equip_char (ch);

	ch->SetSaveTime (CurrentTime);
	strsave = FileTable.PlayerName (capitalize (ch->GetName ()));

	// Save immortal stats, level & vnums for wizlist		-Thoric
	// and do_vnums command
	//
	// Also save the player flags so we the wizlist builder can see
	// who is a guest and who is retired.
	if (ch->GetLevel () >= LEVEL_IMMORTAL) {
		strback = FileTable.MakeName (SD_GOD_DIR, capitalize (ch->GetName ()));

		if ((fp = fopen (strback, "w")) == NULL) {
			bug ("Save_god_level: fopen, file = %s", NCCP strsave);
		} else {
			fprintf (fp, "Level        %d\n", ch->GetLevel ());
			fprintf (fp, "Pcflags      %d\n", ch->GetPcFlags ());
			if (ch->GetPcData ()->r_range_lo && ch->GetPcData ()->r_range_hi)
				fprintf (fp, "RoomRange    %d %d\n",
					ch->GetPcData ()->r_range_lo,
					ch->GetPcData ()->r_range_hi);
			if (ch->GetPcData ()->o_range_lo && ch->GetPcData ()->o_range_hi)
				fprintf (fp, "ObjRange     %d %d\n",
					ch->GetPcData ()->o_range_lo,
					ch->GetPcData ()->o_range_hi);
			if (ch->GetPcData ()->m_range_lo && ch->GetPcData ()->m_range_hi)
				fprintf (fp, "MobRange     %d %d\n",
					ch->GetPcData ()->m_range_lo,
					ch->GetPcData ()->m_range_hi);
			fclose (fp);
		}
	}

	// Find out if player directory exists, and create it if not.
	// Make sure Dir is there, or change strsave to alternate save loc.
	BOOL bChanged = FileTable.CreatePlayerDirectory (strsave,ch->GetName ());

	if (bChanged)
		ch->SendText ("Not Saved in Player Directory. Ask imm's to fix\n");

	if ((fp = fopen (strsave, "w")) == NULL) {
		bug ("Save_char_obj: fopen, file = %s", NCCP strsave);
	} else {
		ch->Write (fp);
		if (! ch->GetCarryList ().IsEmpty ())
			fwrite_obj (ch->GetCarryList (), fp, 0, OS_CARRY, ch->GetLevel ());

		if (ch->HasPet ())
			ch->GetPet ()->Write (fp);

		if (ch->comments)
			fwrite_comments (ch, fp);        // comments
		fprintf (fp, "#END\n");
		fclose (fp);
	}

	// Auto-backup pfile (can cause lag with high disk access situtations
	if (SysData.IsBackup ()) {
		// Find out if backup player directory exists, and create it if not.
		// Make sure Dir is there, or change strsave to alternate save loc.
		// (TRUE means create a backup directory)
		FileTable.CreatePlayerDirectory (strback, ch->GetName (), TRUE);

		if (! CopyFile (strsave, strback, FALSE)) {
			bug ("save_char_obj::Error %d: Unable to copy %s to %s.",
				GetLastError (), NCCP strsave, NCCP strback);
		}
	}

	re_equip_char (ch);

	write_corpses (ch, NULL);
	quitting_char = NULL;
	saving_char   = NULL;
}


// Write an object and its contents.
void fwrite_obj (CObjectList& List, FILE *fp, int iNest, short os_type,
				 short ChLevel /* = 0 */)
{
	if (iNest >= MAX_NEST) {
		bug ("fwrite_obj: iNest hit MAX_NEST %d", iNest);
		return;
	}

	POSITION	pos = List.GetHeadPosition ();
	while (pos) {
		CObjData	&Obj = *List.GetNext (pos);

		Obj.Write (fp, iNest, os_type, ChLevel);

		if (! Obj.GetContentList ().IsEmpty ())
			fwrite_obj (Obj.GetContentList (), fp, iNest + 1, OS_CARRY, ChLevel);
	}
}


// Load a char and inventory into a new ch structure.
BOOL load_char_obj (CDescriptor *d, char *name, BOOL preload)
{
	char		buf [MAX_INPUT_LENGTH];
	char		strsave [MAX_INPUT_LENGTH];
	extern char	strArea [MAX_INPUT_LENGTH];
	FILE		*fp;
	char		*pLine;
	struct stat	fst;
	int			i, x;
	extern FILE	*fpArea;
	extern int	gAcount;

	CCharacter	*ch = new CCharacter;
	ch->InitChar ();

	for (x = 0; x < MAX_WEAR; x++)
		for (i = 0; i < MAX_LAYERS; i++)
			save_equipment [x][i] = NULL;
	loading_char = ch;

	ch->SetPcData (new pc_data);
	pc_data	&Pc = *ch->GetPcData ();

	d->m_pCharacter = ch;
	ch->SetDesc (d);
	ch->SetName (name);
	ch->SetBlank ();
	ch->SetCombine ();
	ch->SetHasPrompt ();
	ch->perm_str			= 13;
	ch->perm_int			= 13;
	ch->perm_wis			= 13;
	ch->perm_dex			= 13;
	ch->perm_con			= 13;
	ch->perm_cha			= 13;
	ch->perm_lck			= 13;
	Pc.condition [COND_THIRST] = 48;
	Pc.condition [COND_FULL] = 48;
	Pc.condition [COND_BLOODTHIRST] = 10;
	ch->SetMentalState (-10);

	ch->m_Style = STYLE_FIGHTING;
	Pc.pagerlen = 24;
	ch->LockMenus (TRUE);
	ch->SetMenuPagelen (24);				// BUILD INTERFACE

	strcpy (strsave, FileTable.PlayerName (capitalize (name)));

	if (stat (strsave, &fst) != -1) {
		if (fst.st_size == 0) {
			strcpy (strsave, FileTable.BackupPlayerName (capitalize (name)));
			ch->SendText ("Restoring your backup player file...");
		} else {
			sprintf (buf, "%s player data for: %s (%dK)",
				preload ? "Preloading" : "Loading", ch->GetName (),
				(int) fst.st_size/1024);
			gpDoc->LogString (buf, LOG_COMM, LEVEL_GREATER);
		}
    }
    // else no player file

	BOOL	bFound = FALSE;
	if (fp = fopen (strsave, "r")) {
		bFound = TRUE;

		for (int iNest=0; iNest < MAX_NEST; ++iNest)
			rgObjNest [iNest] = NULL;

		// Cheat so that bug will show line #'s -- Altrag
		fpArea = fp;
		strcpy (strArea, strsave);
		gAcount = 0;		// clear line count

		for (;;) {
			char	letter;
			char	*word;

			pLine = fread_line (fp);
			letter = *pLine++;
			if (letter == '*')
				continue;

			if (letter != '#') {
				bug ("Load_char_obj: %s, # character not found.", name);
				delete ch;
				d->m_pCharacter = NULL;
				return TRUE;
			}

			word = ParseWord (pLine);
			if (! str_cmp (word, "PLAYER")) {
				if (! ch->Read (fp, preload)) {
					delete ch;
					d->m_pCharacter = NULL;
					return TRUE;
				}
				if (preload)
					break;
			}
			else if (! str_cmp (word, "OBJECT"))		// Objects
				fread_obj (ch, fp, OS_CARRY);
// 1.4add
//			else if (! strcmp (word, "MorphData"))		// Morphs
//				fread_morph_data (ch, fp);
			else if (! str_cmp (word, "COMMENT"))
				fread_comment (ch, fp);				// Comments
			else if (! str_cmp (word, "MOBILE")) {
				CCharacter	*pet = fread_mobile (fp);
				ch->SetPet (pet);
				pet->SetMaster (ch);
				pet->SetCharmed ();
				// Temporarily set the pet in the polymorph room, until
				// the player gets fully loaded.
				pet->SetInRoom (RoomTable.GetRoom (SysData.m_RoomPoly));
//				pet->SendToRoom (ch->GetInRoom ());
			}
			else if (! str_cmp (word, "END"))		// Done
				break;
			else {
				bug ("Load_char_obj: %s, bad section.", name);
				break;
			}
		}
		fclose (fp);
		fpArea = NULL;
		strcpy (strArea, "$");
	}


	if (! Pc.ice_listen)
		Pc.ice_listen = str_dup ("");

	if (! bFound) {
		ch->SetShortDescr ("");
		ch->SetLongDescr ("");
		ch->SetDescription ("");
		Pc.SetClanName (STRALLOC (""));
		Pc.SetCouncilName (STRALLOC (""));
		Pc.SetDeityName (STRALLOC (""));
		Pc.SetPassWord (str_dup (""));
		Pc.SetBamfin (str_dup (""));
		Pc.SetBamfout (str_dup (""));
		Pc.SetRank (str_dup (""));
		Pc.SetBestowments (str_dup (""));
		Pc.SetTitle (STRALLOC (""));
		Pc.SetHomepage (str_dup (""));
		Pc.bio = STRALLOC ("");
		Pc.authed_by = STRALLOC ("");
		Pc.SetPrompt (STRALLOC (""));
		Pc.SetFightPrompt (STRALLOC (""));
    } else {
		if (! Pc.GetClanName ()) {
			Pc.SetClanName (STRALLOC (""));
			Pc.SetClan (NULL);
		}
		if (! Pc.GetCouncilName ()) {
			Pc.SetCouncilName (STRALLOC (""));
			Pc.council = NULL;
		}
		if (! Pc.GetDeityName ()) {
			Pc.SetDeityName (STRALLOC (""));
			Pc.deity = NULL;
		}
		if (! Pc.bio)
			Pc.bio = STRALLOC ("");

		if (! Pc.authed_by)
			Pc.authed_by = STRALLOC ("");

		if (!ch->IsNpc () && ch->IsImmortal ()) {
			if (Pc.wizinvis < 2)
				Pc.wizinvis = ch->GetLevel ();
			if (! preload)
				assign_area (ch);
		}

		for (i = 0; i < MAX_WEAR; i++)
			for (x = 0; x < MAX_LAYERS; x++)
				if (save_equipment [i][x]) {
					equip_char (ch, save_equipment [i][x], i);
					save_equipment [i][x] = NULL;
				}
	}

	loading_char = NULL;
	return bFound;
}


CCharacter *fread_mobile (FILE *fp)
{
	char	*word = "", *pLine;
	
	if (! feof (fp)) {
		pLine = fread_line (fp);
		word = ParseWord (pLine);
	}

	if (strcmp (word, "Vnum")) {
		ScanToEnd (fp, "EndMobile");
		bug ("fread_mobile: Vnum not found");
		return NULL;
	}

	int	vnum = ParseNumber (pLine);

	CCharacter	*mob = create_mobile (MobTable.GetMob (vnum));
	if (! mob) {
		ScanToEnd (fp, "EndMobile");
		bug ("fread_mobile: No index data for vnum %d", vnum);
		return NULL;
	}

	mob->ReadMob (fp);
	return mob;
}



#if defined (KEY)
#undef KEY
#endif

#define KEY(literal,field,value)					\
				if (!str_cmp (word, literal)) {		\
				    field  = value;					\
				    fMatch = TRUE;					\
				    break;							\
				}
#define KEY2(literal, field, value)					\
				if (!str_cmp (word, literal)) {		\
				    field (value);					\
				    fMatch = TRUE;					\
				    break;							\
				}

void fread_obj (CCharacter *ch, FILE *fp, short os_type)
{
	CObjData		*obj;
	char			*word;
	int				iNest;
	BOOL			fMatch;
	CRoomIndexData	*room = NULL;
	char			*pLine;

	obj = new CObjData;
	obj->count = 1;
	obj->wear_loc = -1;
	obj->weight = 1;

	BOOL	fNest = TRUE;		// Requiring a Nest 0 is a waste
	BOOL	fVnum = TRUE;
	iNest = 0;

	for (;;) {
		fMatch = FALSE;
		if (! feof (fp)) {
			pLine = fread_line (fp);
			word = ParseWord (pLine);
		}
		else word = "End";

		switch (UPPER (word [0])) {
		  case '*':
			fMatch = TRUE;
			break;

		  case 'A':
			KEY2("ActionDesc",	obj->SetActionDescr, 	ParseCString (pLine, fp));

			if (!str_cmp (word, "Affect") || !str_cmp (word, "AffectData")) {
				CAffectData	*paf;
				int			pafmod;

				paf = new CAffectData;
				if (! str_cmp (word, "Affect")) {
					paf->type = ParseNumber (pLine);
				} else {
					int		sn = SkillTable.Lookup (ParseWord (pLine));
					if (sn < 0)
						bug ("Fread_obj: unknown skill.");
					else
						paf->type = sn;
				}
				paf->duration = ParseNumber (pLine);
				pafmod = ParseNumber (pLine);
				paf->location = ParseNumber (pLine);
				paf->SetVector (ParseWord (pLine));

				if (paf->location == APPLY_WEAPONSPELL
					|| paf->location == APPLY_WEARSPELL
					|| paf->location == APPLY_REMOVESPELL
					|| paf->location == APPLY_STRIPSN
					|| paf->location == APPLY_RECURRINGSPELL)
						paf->modifier = slot_lookup (pafmod);
				else
					paf->modifier = pafmod;

				obj->AddAffect (paf);
				fMatch = TRUE;
				break;
			}

			if (! str_cmp (word, "AntiClass")) {
				CClassData	&Cl = *ClassTable.Find (ParseCString (pLine, fp));
				if (&Cl)
					obj->SetAntiClass (Cl.GetClass ());
				fMatch = TRUE;
				break;
			}
			break;

		  case 'C':
			KEY ("Cost",	obj->cost,		ParseNumber (pLine));
			KEY ("Count",	obj->count,		ParseNumber (pLine));
			break;

		  case 'D':
			KEY2("Description",	obj->SetDescription,	ParseCString (pLine, fp));
			break;

		  case 'E':
			if (! str_cmp (word, "ExtraFlags")) {
				obj->m_ExtraFlags.Parse (pLine);
				fMatch = TRUE;
				break;
			}

			if (! str_cmp (word, "ExtraDescr")) {
				CExtraDescrData	*ed = new CExtraDescrData;
				ed->keyword = ParseString (pLine, fp);
				ed->SetDescription (ParseString (pLine, fp));
				obj->ExDesList.AddTail (ed);
				fMatch = TRUE;
				break;
			}

			if (! str_cmp (word, "End")) {
				if (! fNest || ! fVnum) {
					bug ("Fread_obj: %s, incomplete object.", obj->GetName ());
					delete obj;
					return;
				} else {
					short			wear_loc = obj->wear_loc;
					CObjIndexData	&Idx = *obj->GetIndex ();

					if (! obj->GetName ())
						obj->SetName (Idx.GetName ());
					if (! obj->GetDescription ())
						obj->SetDescription (Idx.GetDescription ());
					if (!obj->GetShortDescr ())
						obj->SetShortDescr (Idx.GetShortDescr ());
					if (! obj->GetActionDescr ())
						obj->SetActionDescr (Idx.GetActionDescr ());

					if (! obj->serial) {
						cur_obj_serial =
							UMAX ((cur_obj_serial + 1) & (BV30-1), 1);
						obj->serial =
							obj->GetIndex ()->serial = cur_obj_serial;
					}
					if (fNest)
						rgObjNest [iNest] = obj;

					Idx.m_ObjList.AddTail (obj);
					Idx.count += obj->count;

					obj->wear_loc = -1;		// Why?

					// Corpse saving. -- Altrag
					if (os_type == OS_CORPSE) {
						if (! room) {
							bug ("Fread_obj: %s, Corpse without room",
								obj->GetName ());
							room = RoomTable.GetRoom (SysData.m_RoomLimbo);
						}

						// Give the corpse a timer if there isn't one
						if (obj->timer < 1)
							obj->timer = 40;
						if (room->vnum == SysData.m_RoomHallOfFallen 
							&& ! obj->GetContentList ().IsEmpty ())
								obj->timer = -1;

						obj = obj_to_room (obj, room);
					}
					else if (iNest == 0 || rgObjNest [iNest] == NULL) {
						int		slot;
						int     x;
						BOOL	reslot = FALSE;

						if (wear_loc > -1 && wear_loc < MAX_WEAR) {
							for (x = 0; x < MAX_LAYERS; x++)
								if (! save_equipment [wear_loc][x]) {
									save_equipment [wear_loc][x] = obj;
									slot = x;
									reslot = TRUE;
									break;
								}
							if (x == MAX_LAYERS)
								bug ("Fread_obj: too many layers %d",
									wear_loc);
						}
						obj = obj_to_char (obj, ch);
						if (reslot && slot != -1)
							save_equipment [wear_loc][slot] = obj;
					} else {
						if (rgObjNest [iNest-1]) {
							separate_obj (rgObjNest [iNest-1]);
							obj = obj_to_obj (obj, rgObjNest [iNest-1]);
						}
						else
							bug ("Fread_obj: nest layer missing %d", iNest-1);
					}
					if (fNest)
						rgObjNest [iNest] = obj;
					return;
				}
			}
			break;

		  case 'I':
			KEY ("ItemType",	obj->item_type,		ParseNumber (pLine));
			break;

		  case 'L':
			KEY2("Level",	obj->SetLevel,			ParseNumber (pLine));
			break;

		  case 'N':
			KEY2("Name",	obj->SetName,			ParseCString (pLine, fp));

			if (! str_cmp (word, "Nest")) {
				iNest = ParseNumber (pLine);
				if (iNest < 0 || iNest >= MAX_NEST) {
					bug ("Fread_obj: bad nest %d.", iNest);
					iNest = 0;
					fNest = FALSE;
				}
				fMatch = TRUE;
			}
			break;

		  case 'R':
			KEY ("Room", room, RoomTable.GetRoom (ParseNumber (pLine)));

		  case 'S':
			KEY2("ShortDescr",	obj->SetShortDescr,	ParseCString (pLine, fp));

			if (! str_cmp (word, "Spell")) {
				int		iValue = ParseNumber (pLine);
				int		sn     = SkillTable.Lookup (ParseWord (pLine));

				if (iValue < 0 || iValue > 5)
					bug ("Fread_obj: bad iValue %d.", iValue);
				else if (sn < 0)
					bug ("Fread_obj: unknown skill.");
				else
					obj->value [iValue] = sn;
				fMatch = TRUE;
				break;
			}
			break;

		  case 'T':
			KEY ("Timer",	obj->timer,		ParseNumber (pLine));
			break;

		  case 'V':
			if (! str_cmp (word, "Values")) {
				int		x1, x2, x3, x4, x5, x6;
				x1 = x2 = x3 = x4 = x5 = x6 = 0;
				sscanf (pLine, "%d %d %d %d %d %d",
					&x1, &x2, &x3, &x4, &x5, &x6);

				obj->value [0] = x1;
				obj->value [1] = x2;
				obj->value [2] = x3;
				obj->value [3] = x4;
				obj->value [4] = x5;
				obj->value [5] = x6;
				fMatch = TRUE;
				break;
			}

			if (! str_cmp (word, "Vnum")) {
				int		vnum = ParseNumber (pLine);
				if ((obj->pIndexData = OIdxTable.GetObj (vnum)) == NULL)
					fVnum = FALSE;
				else {
					fVnum = TRUE;
					obj->cost = obj->GetIndex ()->cost;
					obj->weight = obj->GetIndex ()->weight;
					obj->item_type = obj->GetIndex ()->item_type;
					obj->wear_flags = obj->GetIndex ()->wear_flags;
					obj->m_ExtraFlags = obj->GetIndex ()->m_ExtraFlags;
				}
				fMatch = TRUE;
				break;
			}
			break;

		  case 'W':
			KEY ("WearFlags",	obj->wear_flags,	ParseNumber (pLine));
			KEY ("WearLoc",		obj->wear_loc,		ParseNumber (pLine));
			KEY ("Weight",		obj->weight,		ParseNumber (pLine));
			break;

		}

		if (! fMatch) {
			bug ("Fread_obj: no match: %s", word);
			delete obj;
			return;
		}
	}
}


// Based on last time modified, show when a player was last on	-Thoric
void do_last (CCharacter *ch, char *argument)
{
    char buf [MAX_STRING_LENGTH];
    char arg [MAX_INPUT_LENGTH];
    char name[MAX_INPUT_LENGTH];
    struct stat fst;

    one_argument (argument, arg);
    if (arg[0] == '\0')
    {
	ch->SendText ("Usage: last <playername>\n\r");
	return;
    }
    strcpy (name, capitalize (arg));
    strcpy (buf, FileTable.PlayerName (name));
    if (stat (buf, &fst) != -1)
      sprintf (buf, "%s was last on: %s\r", name,
		NCCP gpDoc->GetTimeString (fst.st_mtime));
    else
      sprintf (buf, "%s was not found.\n\r", name);
   ch->SendText (buf);
}


void write_corpses (CCharacter *ch, const char *name)
{
	FILE	*fp = NULL;

	// Name and ch support so that we dont have to have a char to save their
	// corpses.. (ie: decayed corpses while offline)
	if (ch && ch->IsNpc ()) {
		bug ("Write_corpses: writing NPC corpse.");
		return;
	}
	if (ch)
		name = ch->GetName ();

	CString Cname = FileTable.MakeCorpseName (capitalize (name));

	// Go by vnum, less chance of screwups. -- Altrag
	CObjIndexData	&Idx = *OIdxTable.Find (OBJ_VNUM_CORPSE_PC);
	if (&Idx) {
		POSITION	pos = Idx.m_ObjList.GetHeadPosition ();
		while (pos) {
			CObjData	&Corpse = *Idx.m_ObjList.GetNext (pos);
			if (Corpse.GetInRoom ()
			  && !str_cmp (Corpse.GetShortDescr ()+14, name)) {
				if (! fp) {
					if (! (fp = fopen (Cname, "w"))) {
						bug ("Write_corpses: Cannot open file: %s", Cname);
						return;
					}
				}
				Corpse.Write (fp, 0, OS_CORPSE);
				fwrite_obj (Corpse.GetContentList (), fp, 1, OS_CARRY);
			}
		}
	}
	if (fp) {
		fprintf (fp, "#END\n\n");
		fclose (fp);
	} else {
		remove (FileTable.MakeCorpseName (capitalize (name)));
	}
}


void load_corpses ()
{
	CString		Fname;
	FILE		*fp;
	extern int	falling;

	WIN32_FIND_DATA	Fd;
	HANDLE Dh = FindFirstFile (FileTable.MakeCorpseName ("*"), &Fd);
    if (Dh == INVALID_HANDLE_VALUE) then return;

	gpDoc->LogString ("Loading corpses...", LOG_BOOT);
	falling = 1;	// Arbitrary, must be >0 though.

	do {
		Fname = FileTable.MakeName (SD_CORPSE_DIR, Fd.cFileName);
		gpDoc->m_pLog->Printf (LOG_ALWAYS, "Corpse -> %s\n", NCCP Fname);
		if (! (fp = fopen (Fname, "r"))) {
			perror (Fname);
			continue;
		}
		for (;;) {
			char	letter;
			char	*word, *pLine;

			pLine = fread_line (fp);
			letter = *pLine++;
			if (letter == '*')
				continue;

			if (letter != '#') {
				bug ("Load_corpses: # not found.");
				break;
			}
			word = ParseWord (pLine);
			if (! str_cmp (word, "CORPSE"))
				fread_obj (NULL, fp, OS_CORPSE);
			else if (! str_cmp (word, "OBJECT"))
				fread_obj (NULL, fp, OS_CARRY);
			else if (! str_cmp (word, "END"))
				break;
			else {
				bug ("Load_corpses: bad section.");
				break;
			}
		}
		fclose (fp);
	} while (FindNextFile (Dh, &Fd));

	FindClose (Dh);
	falling = 0;
}