Technical Note : SLE0008 Author : Scott Evans Created/Modified : 5/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. enum { // 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. 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. // 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 bu0:BE-NETYAROZE // 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 a 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 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 SC. 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. 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>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 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=0) { // Calculate number of sectors to write nsectors=n/MC_SECTOR_SIZE; // Write the data for(no_bytes=i=0;iheader_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>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 to memory card has never been easier!! This is a first draft and as a result could do with a few more details here and there but most of it is done. If you have any questions, comments or have any code that does a better job I would be very interested to hear from you.