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 its 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.