Technical Note : SLE0008
Author : Scott Evans
Created/Modified : 11/9/98
Description : Memory cards 
 

This technical note describes how to handle the memory cards. It explains how to create a file, write data to a file and read data from a file. The code examples in this document are from Bouncer 2.

Initialising the memory cards

Before you can do anything with the memory cards you need to know if there are any memory cards plugged in to the Playstation and what state they are in. This can be achieved using the library function TestCard().

The first function we will write will be used to test a memory card to see if it can be used. It might look something like the following. The constants that the function returns would be defined with an enum as follows. They have to match the return values from the TestCard() function.

enum
{
 MC_MISSING,
 MC_PRESENT,
 MC_NEW,
 MC_ERROR,
 MC_UNINITIALISED
};

// Function  : InitMemoryCard()
// Coded by  : Scott Evans
// Created/Modified : 21/7/98
// Description  : Test a memory card

// Parameters  : slot - 0 or 1 to select memory card

// Returns  : result of test

// Notes  : Results are as follows
//
// MC_MISSING - no memory card in the slot
// MC_PRESENT - a memory card was found in the slot
// MC_NEW - a new memory card was found
// MC_ERROR - an error with card
// MC_UNINITIALISED - non formatted memory card
 

long InitMemoryCard(u_byte slot)
{
 long result;

     result=TestCard(slot);

     // Wait for it....
 
     VSync(4);
 
     return(result);
}
 

This is a very simple function. All it does is return the result of the library function TestCard() and you could just use TestCard() instead. I have no idea why the VSync() is needed but the manual mentions waiting after calling the TestCard() function.

The next function will be used to find a memory card that we can use. It looks a little like this.
 

// Function  : FindMemoryCard()
// Coded by  : Scott Evans
// Created/Modified : 21/7/98
// Description  : Finds a valid memory card

// Parameters  : slot - specify a slot to search or search
//                        both

// Returns  : Slot number of available memory card or
//                        MC_SLOT_NOTFOUND

// Notes  : MC_SLOT0 to search slot 0
//     MC_SLOT1 to search slot 1
//     MC_NOSLOT to search both
 

u_byte FindMemoryCard(u_byte slot)
{
    long result;
 
    // Use the slot specified if given
 
    if(slot!=MC_NOSLOT)
    {
     result=InitMemoryCard(slot);
 
      if(result==MC_PRESENT || result==MC_NEW)
       return(slot);
 
        return(MC_SLOT_NOTFOUND);
    }
    else
    {
     // Try slot 0
 
        result=InitMemoryCard(MC_SLOT0);
 
        if(result==MC_PRESENT)
         return(MC_SLOT0);
 
        // Try the other slot
 
        result=InitMemoryCard(MC_SLOT1);
 
        if(result==MC_PRESENT || result==MC_NEW)
         return(MC_SLOT1);
 
        // Failed both slots
 
        return(MC_SLOT_NOTFOUND);
    }
}

Again the constants used in the function would be defined with an enum.

enum
{
 MC_SLOT0,
 MC_SLOT1,
 MC_SLOT_NOTFOUND
};

As you can see you can specify a slot to search or you can try both. The function will return the slot which contains the memory card which was found. If the memory card is not formatted or there was an error then the function will return MC_SLOT_NOTFOUND.

Creating a file

Once we have found ourselves a memory card we can create a file. First we need to construct a file name. Normally you make a file name from the product code of your game which you get from Sony but for Yaroze games it is a little different.

The following function can be used to create a memory card filename which follows the Sony rules for Yaroze memory card files, see the savefile.html file on the Yaroze website.
 

// Function   : CreateMemoryCardFilename()
// Coded by   : Scott Evans
// Created/Modified : 21/7/98
// Description  : Creates a memory card filename

// Parameters  : name - name of file
//      mc_name - memory card filename
//      slot - slot number to use

// Returns   : None

// Notes   : None
 

#define MC_STANDARD_FILENAME "BE-NETYAROZE"

void CreateMemoryCardFilename(u_byte *name,u_byte *mc_name,u_byte slot)
{
 // Create filename
 
     sprintf(mc_name,"bu%d0:%s %s",slot,MC_STANDARD_FILENAME,name);
}
 

Here is an example of how to use this function.

// Maximum size for a memory card filename

#define MC_MAX_FILENAME_CHARACTERS 20

u_byte filename[MC_MAX_FILENAME_CHARACTERS];

CreateMemoryCardFilename(“B2”,filename,0);

This will create the string “bu00:BE-NETYAROZE B2” in the array filename. It will reference the file BE-NETYAROZE B2 on memory card 1.

This is the filename we must use from now on in our other memory card functions. The following function can be used to create a file on a memory card.

// Function   : CreateFile()
// Coded by   : Scott Evans
// Created/Modified : 21/7/98
// Description  : Creates a file on a memory card

// Parameters  : mc_name - memory card filename
//      size - size of file in blocks

// Returns   : 1 for success, 0 for error

// Notes   : File blocks are 8K, total capcity of card
//          15 blocks (120K)
//
// Filename format is bu<slot>0:BE-NETYAROZE<name>
// Filename should be created with CreateMemoryCardFilename()
 

u_byte CreateFile(u_byte *mc_name,word size)
{
    long fh;
 
    // Try to create the file
 
    if((fh=open(mc_name,O_CREAT|(size<<16)))>0)
    {
      close(fh);
         return(1);
    }
 
    return(0);
}
 

To create a file which uses 1 block (8K) on the memory card we would do the following.

CreateFile(filename,1);

This will create a file which uses 1 block of the memory card.

Once the file has been created it size cannot be changed unless it is deleted and then created at it’s new size. So it is important to know the maximum size of your file at the time of creation.

Before we can start writing any data to the file we need to know the format of the file.
 

File format

The file is made up of a header followed by data. The header contains information about the file needed by the system like the size of the file and a description. The data section is where the user puts the data needed by the game.

The following structure can be used to describe the format of the header for Playstation save game files.

// Size of the CLUT used for animation

#define MC_CLUT_SIZE 16

// Size of images used in animation

#define MC_IMAGE_SIZE 64

// Size of header

#define MC_HEADER0_SIZE (4+64+28+(MC_CLUT_SIZE<<1)+(MC_IMAGE_SIZE*2))
#define MC_HEADER1_SIZE (4+64+28+(MC_CLUT_SIZE<<1)+(MC_IMAGE_SIZE*4))
#define MC_HEADER2_SIZE (4+64+28+(MC_CLUT_SIZE<<1)+(MC_IMAGE_SIZE*6))

// Number of characters in description

#define MC_DESCRIPTION_SIZE 64

// Number of padding bytes

#define MC_PADDING_SIZE  28

// Memory card file header

typedef struct
{
 u_byte magic_no[2];
 u_byte type;
 u_byte no_blocks;
 u_byte name[MC_DESCRIPTION_SIZE];
 u_byte pad[MC_PADDING_SIZE];
 u_word CLUT[MC_CLUT_SIZE];
 u_word image0[MC_IMAGE_SIZE];
 u_word image1[MC_IMAGE_SIZE];
 u_word image2[MC_IMAGE_SIZE];
}MCFILE_HEADER;

The following function can be used to set up a header for a memory card file.

void InitMCFileHeader(MCFILE_HEADER *mch,MC_IMAGE_INFO *mci,
                      u_byte n,u_byte *s)
{
 // Clear out the structure
 
  bzero(mch,sizeof(MCFILE_HEADER));
 
   // Fill out the header
 
   mch->magic_no[0]='S';
   mch->magic_no[1]='C';
   mch->type=mci->type;
   mch->no_blocks=n;
 
 // Put in the description

 memcpy(&mch>name[0],ConvertAsciiToShiftJIS(s),
        MC_DESCRIPTION_SIZE);

 // Pad with spaces

 memset(&mch->pad[0],' ',MC_PADDING_SIZE);

   // Copy the CLUT for the animation
 
   if(mci->clut)
     memcpy((u_byte *)&mch->CLUT[0],
           (u_byte *)mci>clut,MC_CLUT_SIZE<<1);
 
 // Copy the first image
 
 if(mci->image0)
    memcpy((u_byte *)&mch->image0[0],
           (u_byte *)mci>image0,MC_IMAGE_SIZE<<1);
 
  // Copy the second image
 
  if(mci->image1)
    memcpy((u_byte *)&mch->image1[0],
           (u_byte *)mci>image1,MC_IMAGE_SIZE<<1);
 
  // Copy the third image
 
 if(mci->image2)
    memcpy((u_byte *)&mch->image2[0],
           (u_byte *)mci>image2,MC_IMAGE_SIZE<<1);
}

The magic number should always be the characters ‘S’ and ‘C’. The type field is used to identify how many textures are used in the animation that you see on the memory card screen (if you boot your Playstation without a Playstation CD). You can use 1, 2 or 3 frames in your animation. The textures have to be 4bit and 16x16 in size.

The size of the file in 8K blocks is specified in the no_blocks field. The pad array is just padding so fill it with spaces. It probably does not matter what you fill this array with since it is just padding but you never know.

Next comes the CLUT data for your animation. It is a 4 bit CLUT so simply copy the CLUT from the textures that you are going to use for the animation into the CLUT array.

After your CLUT comes the data for the textures which are used in the animation. The number of textures used depends on what is specified in the type field. This also effects the size of the header. Just copy your 4 bit TIM data into the arrays image0, image1 and image2 as needed.

The MC_IMAGE_INFO structure just contains pointers to the CLUT and TIM data. It also contains the type (number of images used) and the size of the header.

// Information on the images used in animation

typedef struct
{
 u_long header_size;
 u_word *clut;
 u_word *image0;
 u_word *image1;
 u_word *image2;
 u_byte type;
}MC_IMAGE_INFO;

That is basically all you need to do. The only thing left is the description of the file which also appears on the memory card screen. This string is not simple ASCII it is SHIFT-JIS so we need some functions to convert ASCII to SHIFT-JIS.
 

// Function   : MapAsciiToShiftJIS()
// Coded by   : Scott Evans
// Created/Modified : 31/7/98
// Description  : Maps an ASCII character to a Shift JIS
//      character

// Parameters  : c - ASCII code to convert
// Returns   : Shift JIS character code

// Notes   : None
 

// Shift JIS charaters

#define SJIS_UA  0x8260
#define SJIS_LA  0x8281
#define SJIS_0  0x824f
#define SJIS_SPACE  0x8140

u_word MapAsciiToShiftJIS(u_byte c)
{
 switch(c)
 {
  // Convert upper case letters (A-Z)

  case ‘A’ ... ‘Z’:
   return(SJIS_UA+c-'A');
 
  // Convert lower case letters (a-z)
 
  case ‘a’ ... ‘z’:
   return(SJIS_LA+c-'a');
 
  // Convert numbers (0-9)
 
  case ‘0’ ... ‘9’:
   return(SJIS_0+c-'0');
 
  // Other characters
 
  case ' ':
   return(SJIS_SPACE);
 
  default:
   return(0);
 }
}
 

// Function   : ConvertAsciiToShiftJIS()
// Coded by   : Scott Evans
// Created/Modified : 31/7/98
// Description  : Converts an ASCII string a Shift JIS string

// Parameters  : string - ASCII string to convert
// Returns   : Shift JIS character string

// Notes   : No check is done on length of string
 

u_byte *ConvertAsciiToShiftJIS(u_byte *string)
{
 static u_byte buffer[MC_DESCRIPTION_SIZE];
 u_byte i,j,l;
 u_word sjis;

 // Number of characters to convert

 l=strlen(string);

 // Convert Ascii string

 for(i=0,j=0;i<l;i++)
 {
  sjis=MapAsciiToShiftJIS(string[i]);
  buffer[j++]=(sjis>>8)&0xff;
  buffer[j++]=sjis&0xff;
 }

 buffer[j++]=0;
 buffer[j]=0;

 return(&buffer[0]);
}

One thing worth a mention is these functions only convert A to Z, a to z, 0 to 9 and space. So only use these in your description if using these functions. The maximum size for the description is 32 ASCII characters since for every ASCII character 2 bytes are needed for the equivalent SHIFT-JIS character.

So we now know how to create a file and initialise the header. Next we need to be able to read/write this data to the memory card along with any data which is used for our game.

Reading and writing

Two very simple functions can be used to read and write data. They are as follows.

// Size of one sector

#define MC_SECTOR_SIZE 128
 

// Function   : WriteBlock()
// Coded by   : Scott Evans
// Created/Modified : 21/7/98
// Description  : Writes a block of bytes to memory card

// Parameters  : mc_name - memory card filename
//      buffer - pointer to data to write
//      n - number of bytes to write

// Returns   : Number of bytes written or -1 for error

// Notes   : The memory card filename is created by
//          CreateFile()
 

long WriteBlock(u_byte *mc_name,u_byte *buffer,long n)
{
 long fh,no_bytes;
     word i,nsectors;

     // Open the file
 
     if((fh=open(mc_name,O_WRONLY))>=0)
     {
  // Calculate number of sectors to write

  nsectors=n/MC_SECTOR_SIZE;

      // Write the data
 
  for(no_bytes=i=0;i<nsectors;i++)
  {
          no_bytes+=write(fh,buffer,MC_SECTOR_SIZE);
   buffer+=MC_SECTOR_SIZE;
  }
 
         // Close the file
 
         close(fh);
 
  return(no_bytes);
     }
 
  return(fh);
}
 

// Function   : ReadBlock()
// Coded by   : Scott Evans
// Created/Modified : 21/7/98
// Description  : Reads a block of bytes from the memory card

// Parameters  : mc_name - memory card filename
//      buffer - pointer to data storage area
//      n - number of bytes to read
// Returns   : Number of bytes read or -1 for error

// Notes   : The memory card filename is created by
//          CreateFile()
 

long ReadBlock(u_byte *mc_name,u_byte *buffer,long n)
{
 long fh,no_bytes;
 word i,nsectors;

 // Open the file
 
     if((fh=open(mc_name,O_RDONLY))>=0)
     {
     // Calculate number of sectors to write

  nsectors=n/MC_SECTOR_SIZE;

      // Write the data
 
  for(no_bytes=i=0;i<nsectors;i++)
  {
          no_bytes+=read(fh,buffer,MC_SECTOR_SIZE);
   buffer+=MC_SECTOR_SIZE;
  }
 
  // Close the file
 
         close(fh);
 
  return(no_bytes);
     }
 
 return(fh);
}

The only thing to make a note of here is that data needs to be written to memory cards in 128 byte blocks. So make sure the number of bytes passed to the functions is a multiple of 128 or you might not read/write the correct number of bytes.

Deleting files is very easy since there is a library function to do this, delete(). Just use the name of the file to delete as the parameter, remember to use the filename created with CreateMemoryCardFilename().

There is also a library function to format a memory card. The function is format() and takes the file system to format as its only parameter. So to format the memory card in slot 1 just call format like this.

Format(“bu00:”);

If you wanted to format the other memory card the parameter would be “bu10:”.

Right then we have all the building blocks to create and manipulate memory card files so lets put them all together and do something useful with them.

The two functions below are taken as they are from Bouncer 2. They are used to save and load game information.

// Function   : SaveGame()
// Coded by   : Scott Evans
// Created/Modified : 21/7/98
// Description  : Saves game data to memory card

// Parameters  : slot - memory card to use
// Returns   : 1 for success, 0 for error

// Notes   : None
 

u_byte SaveGame(u_byte slot)
{
 long no_bytes,filesize;
 MC_IMAGE_INFO *mci;

     // Find a memory card that can be used
 
     if(FindMemoryCard(slot)!=MC_SLOT_NOTFOUND)
     {
      CreateMemoryCardFilename("B2",ls_filename,slot);

         // Delete the file if it already exists
         DeleteFile(ls_filename);

         // Create a new file
 
         if(CreateFile(ls_filename,LS_FILE_BLOCKS))
         {
          // Get information about textures

   mci=GetMCImageInfo(mc_anim,0,1,2);
 
          // Set up the file header
 
   InitMCFileHeader((MCFILE_HEADER*)&ls_buffer[0],
   mci,LS_FILE_BLOCKS,"Bouncer 2 Save Game");

   filesize=mci->header_size;

         // Put in a validation string

   memcpy((u_byte*)&ls_buffer[filesize],&validation_string[0],
                 strlen(validation_string)+1);
 
   filesize+=strlen(validation_string)+1;
 
   // Make sure block starts on a 4 byte boundary

   filesize=((filesize>>2)<<2)+4;

         // Copy the highscore table data
 
   memcpy((u_byte *)&ls_buffer[filesize],
          (u_byte*)&highscore_table[0],
           sizeof(HIGHSCORE)*HIGHSCORE_TABLE_SIZE);

         filesize+=sizeof(HIGHSCORE)*HIGHSCORE_TABLE_SIZE;

   // Make sure our next block starts on a 4 byte boundary

   filesize=((filesize>>2)<<2)+4;

   // Copy the game data

   lsgd.level_no=gd.last_level;
   lsgd.difficulty=gd.difficulty;

   // Screen position

   lsgd.sx=pd.screen_xoff;
   lsgd.sy=pd.screen_yoff;

   // Cheat modes

   lsgd.cheats_on=gd.cheats_on;

   // Number of lives and bombs

   lsgd.lives=gd.no_lives;
   lsgd.bombs=gd.no_bombs;

   memcpy((u_byte *)&ls_buffer[filesize],
                (u_byte *)&lsgd,sizeof(LSGAME_DATA));
   filesize+=sizeof(LSGAME_DATA);

   // Make sure data size is a multiple of
   // MC_SECTOR_SIZE

      filesize=((filesize/MC_SECTOR_SIZE)*MC_SECTOR_SIZE)+
      MC_SECTOR_SIZE;

   // Write the data to the memory card

   no_bytes=WriteBlock(ls_filename,&ls_buffer[0],filesize);

   if(no_bytes==-1 || no_bytes<filesize)
    return(0);

   return(1);
        }
 
        return(0);
    }
 
 return(0);
}
 

// Function   : LoadGame()
// Coded by   : Scott Evans
// Created/Modified : 21/7/98
// Description  : Loads game data from memory card

// Parameters  : slot - memory card to use

// Returns   : 1 for success, 0 for error

// Notes    : None
 

u_byte LoadGame(u_byte slot)
{
 long no_bytes,filesize,n;

 if(FindMemoryCard(slot)!=MC_SLOT_NOTFOUND)
 {
  // Try loading from the memory card

  CreateMemoryCardFilename("B2",ls_filename,slot);

  // Make sure data size is a multiple of MC_SECTOR_SIZE

  n=MC_HEADER2_SIZE+strlen(validation_string)+1+
    (sizeof(HIGHSCORE)*HIGHSCORE_TABLE_SIZE)+sizeof(LSGAME_DATA);

  filesize=((n/MC_SECTOR_SIZE)*MC_SECTOR_SIZE)+
    MC_SECTOR_SIZE;

  // Read the game data

  no_bytes=ReadBlock(ls_filename,&ls_buffer[0],filesize);

  // Was all the data loaded

  if(no_bytes==filesize)
  {
   filesize=MC_HEADER2_SIZE;

   // Check that it is a Bouncer 2 save game

   if(!strcmp(validation_string,&ls_buffer[filesize]))
   {
    filesize+=strlen(&ls_buffer[filesize])+1;

    // Make next block of data starts on a 4 byte boundary

    filesize=((filesize>>2)<<2)+4;

    // Copy the highscore data
 
    memcpy((u_byte *)&highscore_table[0],
          (u_byte*)&ls_buffer[filesize],
                sizeof(HIGHSCORE)*HIGHSCORE_TABLE_SIZE);

    // Move on to next block of data

    filesize+=sizeof(HIGHSCORE)*HIGHSCORE_TABLE_SIZE;
 
    // Make next block of data starts on a 4 byte boundary

    filesize=((filesize>>2)<<2)+4;

    // Get the game data

    memcpy((u_byte *)&lsgd,
               (u_byte *)&ls_buffer[filesize],
           sizeof(LSGAME_DATA));

    // Set the game data

    gd.start_level=lsgd.level_no;
    gd.last_level=gd.start_level;
    gd.difficulty=lsgd.difficulty;
    gd.cheats_on=lsgd.cheats_on;
    gd.no_lives=lsgd.lives;
    gd.no_bombs=lsgd.bombs;

    return(1);
   }

   return(0);
  }

  return(0);
 }

 return(0);
}

So there you have it. Loading and saving your games could not be easier! It is worth mentioning that the SaveGame() and LoadGame() functions have some limitations. Such as if the memory card is full the functions will simply fail and report a memory card error. The same is true if the memory card is not formatted. It should really give you the choice to format a non formatted memory card and give you a choice of which file to delete when the memory card is full. This is something to put in for the next version.
 

This document in word format.
This document in text format.