BBS Game Dev
Intro Setup Assests DDPlus Data Core Tips


Core Skeleton Example
An example program with save game features, creating a new player, loading the player, and maintenance routines.

What does the Player Record look like?
How do you handle loading the player?
How do you run maintenance for the game?
How do you create a new player?
How do you set the game up with saving?


What does the Player Record look like?
All global variables are kept in VAR.PAS. All the people playing the game are stored in TotalData(TP). The current player in-game is PlayerData (CP). CP is loaded from TP when the game starts, and stores all the integer information for the current character. TP will store 50 players in total.
unit vars;
interface

const
ect...

type
 TotalData = record
             handle: string[64];
             name: string[16];
             turns: string[2];
             ect...
             end;

 PlayerData = record
              handle: string[64];
              name: string[16];
              turns: integer;
              ect...
              end;
var
 ect...
 TP: array[1..50] of TotalData;
 CP: PlayerData;



How do you handle loading the player?
All the load routines are located in LOADRTN.PAS, loading the player, monsters, and game config information. loadTP loads all the players into the TP record - a global that stores all character information. This routine returns the total players loaded:
function loadTP:integer;
 var
  ect...
 begin

{ clear the array for player REMOVE }
  pIndex:=1;
  while pIndex <= 50 do
  begin
   TP[pIndex].handle:= '';
   TP[pIndex].name:= '';
   ect...
   inc(pIndex);
  end;

 { load TP: Total Players from the Player File }
 gIndex:=1;
 pIndex:=1;
 assign(P,'SKEL.PLR');
 reset(P);

 while not EOF(P) do
  begin
   readln(P,s);
   if s<>'' then
    begin
     last:=0;
     pos:=last+1;
     while (pos<=length(s)) and (s[pos]<>',') do
      inc(pos);
     TP[pIndex].handle := copy(s,last+1,(pos-last-1));

     last:=pos;
     pos:=last+1;
     while (pos<=length(s)) and (s[pos]<>',') do
      inc(pos);
     TP[pIndex].name := copy(s,last+1,(pos-last-1));

     inc(pIndex);
    end;
  end;
  { Set Global Index for Total Players }
  gIndex:= pIndex-1;
  close(P);

  { Return Total Loaded }
  loadTP:=pIndex;
 end;
The player file (SKL.PLR) is comma delimited. This routine starts grabbing strings between the commas. Later, we'll convert those strings into integers.




How do you run maintenance for the game?
The maintenance routine is in MNTCTRL.PAS. checkMaintenance is called when the game is run. It reads the last time maintenance was run by pulling the date from the config file (SKL.CFG). Then, compare it against today's date.
procedure checkMaintenance;
var
 ect...
begin
 assign(F,'SKEL.CFG');
 reset(F);
 readln(F,s);
 close(F);

 date:= Copy(s,0,8);
 ect...

 Val(date,dateVal,code);
 GetDate(Year,Month,Day,Hour);
 currentDate := L0(Year)+L0(Month)+L0(Day);
 Val(currentDate,currentDateVal,code);

 if currentDateVal > dateVal then
  runMaintenance(currentDate,turns,days,pBattle,moonPhaseString,GV.topPlayer);

end;

If today's date is greater than the value stored, runMaintenance is executed. This compares the current date against the player's last log-on date. Going through all the players in the game.
procedure runMaintenance(currentDate,turns,days,pBattle,moonPhaseString,topPlayer:string);
var
 ect...
begin
 ect...
 for i:=1 to pIndex-1 do
  begin
   days_total:= 0;
   pYearString:= copy(TP[i].totaldays,0,2);
   pMonthString:= copy(TP[i].totaldays,3,2);
   pDayString:= copy(TP[i].totaldays,5,2);

   { Player Date }
   Val(pDayString,pDay,code);
   Val(pMonthString,pMonth,code);
   Val(pYearString,pYear,code);

   { Calculates Current Time against Players Last Login }
   if cYear = pYear then
    begin
     if cMonth = pMonth then
      begin
       days_total:= cDay-pDay;
      end
     else
      begin
       for fdi:= pMonth+1 to cMonth-1 do
        begin
         days_total:= days_total + month_days[fdi];
        end;
       days_total:= days_total + (cDay+(month_days[pMonth]-pDay));
      end;
    end
   else
    begin
     for fdi:= 1 to cMonth-1 do
      begin
       days_total:= days_total+month_days[fdi];
      end;
     for fdi:= pMonth+1 to 12 do
      begin
       days_total:= days_total+month_days[fdi];
      end;
     if cYear - pYear >= 0 then
      begin
       days_total:= days_total + ((cYear - pYear - 1)*365) +
                    (month_days[pMonth] - pDay + cDay);
      end;
    end;

   if TP[i].totaldays<>'REMOVE' then
    begin
     { If The Player Hasn't Been On In X Days:(SKEL.CFG) REMOVE }
     Val(days,daysVal,code);
     if days_total >= daysVal then TP[i].totaldays:= 'REMOVE';
    end;
  end; { end For statement }
  ect...
end;

Next, we have to resurrect any character who died the day before. The status value is checked. This value stores the death date of the player. So if it's greater than 10, the player has died. Below 10 is used to know if the player is alive.
  { Clears Death If Player Died The Day Before }
  Val(returnShortDate,tempDate,code);
   for i:=1 to pIndex-1 do
    begin
     { Restore Special Attack Points To Max }
     TP[i].spattack:=TP[i].spmax;
     Val(TP[i].status,tempStatus,code);
     if tempStatus > 10 then
      begin
       { Set the status to Rezzed so the game knows rez player on log in }
       Str(deathRezzed,outputString);
       if tempStatus < tempDate then TP[i].deathType:= outputString;
      end;
    end;

Before we can close out maintenance, we need to write the player file back to disk. But we skip over the player that has REMOVE in the totaldays element.
{ Write out Player Save File for characters that are not REMOVED }
  assign(P,'SKEL.PLR');
  rewrite(P);
  for i:=1 to pIndex-1 do
   begin
    { Heals the player during maintenance }
    TP[i].hitpoints:=TP[i].maxhp;

    if TP[i].totaldays <> 'REMOVE' then
     writeln(P,TP[i].handle+','
     +TP[i].name+','
     { not the same as COMMFUN }
     { directly writes turns & pbattles to the player save file }
     +turns+','
     +pBattle+','
     +TP[i].totaldays+','
     ect...
     +TP[i].invPotion+',999');
   end;
  close(P);



How do you create a new player?
CRTENEW.PAS handles creating a new player. At the core of it, you need to test if the name has already been taken. The first part happens here in CRTENEW.PAS.
begin
  swrite('Enter your character''s name: ');
  sgoto_xy(37,12);
  set_color(GV.colorSpeech,0);
  sread(newName);
  ect...
  if testInput = true then
   begin
    testName:= checkName(newName);
    if testName=true then
     begin
      correctName:= true;
     end
    else
     begin
      sgoto_xy(8,16);
      set_color(9,0);
      swriteln('Name already taken.');
      sgoto_xy(28,16);
      sread_char(Ch);
    end
   end
  else
   begin
    ect...
   end;
  end;

The second test happens in COMMFUN.PAS under checkName. But the names are converted to uppercase before testing.
function convertUpperCase(pName: string): string;
var
 l: integer;
 pos: integer;
 tempCh: char;
 outputString: string;
 upperString: string;
begin
 l:= length(pName);
 pos:= 1;
 upperString:= '';
 while pos <= l do
 begin
  tempCh:= UpCase(pName[pos]);
  insert(tempCh,upperString,pos);
  inc(pos);
 end;
 convertUpperCase:= upperString;
end;

function checkName(newName: string): boolean;
var
 i: integer;
 outputString: string;
begin
 i:= 1;
 checkName:= true;
 while i <= gIndex  do
 begin
  Str(i,outputString);
  if convertUpperCase(newName) = convertUpperCase(TP[i].name) then checkName:= false;
  inc(i);
 end;
end;



How do you set the game up with saving?
SKELETON.PAS starts off with the pointer to the saveGame routine:
var
 ect...
begin;
 ect...
 saveexitproc:= exitproc;
 exitproc:= @myexit1;

Which will execute ERROLOG.PAS running the MyExit1 routine that calls TrapExit:
{$F+} Procedure MyExit1; {$F-}
 VAR SaveExitProc: POINTER;
 Begin;
  TrapExit;
  SaveExitProc:=Exitproc;
 End;

In ERROLOG.PAS, it calls TrapExit which calls the saveGame routine.
PROCEDURE TrapExit;

CONST
{Replace Skeleton Game Framework with the name of your program}
 ProductName='Skeleton Game Framework';
 ect...
 {Replace the next line with the name of YOUR save procedure}
 {This ones save my game information should something go wrong}
 {I have left it so you see what I did, although it IS commented out}

 {SaveGame(Player,PlayerFile,TempP,Country,CountryFile,Map1,MapFile);}
 saveGame;

saveGame routine from COMMFUN.PAS:
procedure saveGame;
 var
  F: text;
  i: integer;
  outputString: string;
 begin
  if charLoc<>0 then
   begin
    { Copy the Current Player back into the Total Player record }
    TP[charLoc].handle:= CP.handle;
    TP[charLoc].name:= CP.name;
    Str(CP.turns,outputString);
    TP[charLoc].turns:= outputString;
    Str(CP.pbattle,outputString);
    TP[charLoc].pbattle:= outputString;
    { Store current date on logoff. Used in Maintenance }
    TP[charLoc].totaldays:= returnShortDate;
    ect...
    Str(CP.invPotion,outputString);
    TP[charLoc].invPotion:= outputString;

    assign(F,'SKEL.PLR');
    rewrite(F);

    for i:=1 to gIndex do
    begin
     writeln(F,TP[i].handle+','
     +TP[i].name+','
     +TP[i].turns+','
     +TP[i].pBattle+','
     +TP[i].totaldays+','
     +TP[i].sex+','
     ect...
     +TP[i].invPotion+',999');

    end;
    close(F);
   end;
 end;
Upon exit, the shortdate is stored in TP[].totaldays. This is checked during maintenance, and the character is deleted if the date is over 180 days. This value is set in SKL.CFG.











Intro Setup Assests DDPlus Data Core Tips