/** VGB: portable GameBoy emulator ***************************/
/**                                                         **/
/**                           GB.c                          **/
/**                                                         **/
/** This file contains the portable part of the GameBoy     **/
/** hardware emulation. See GB.h for #defines related to    **/
/** drivers and GameBoy hardware.                           **/
/**                                                         **/
/** Copyright (C) Marat Fayzullin 1995,1996                 **/
/**     You are not allowed to distribute this software     **/
/**     commercially. Please, notify me, if you make any    **/   
/**     changes to this file.                               **/
/*************************************************************/

#include "GB.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

byte Verbose    = 1;     /* Verboseness level                            */

byte *RAM,*Page[8];      /* RAM and pointers to Z80 address space 8x8kB) */

int  VPeriod    = 6000;  /* Number of Z80 cycles between VBlanks         */
byte UPeriod    = 1;     /* Number of VBlanks per screen update          */
byte LineDelay  = 0;     /* When 1, CMPLINE interrupts are delayed       */
byte CheckCRC   = 1;     /* When 1, VGB checks cartridge CRC on loading  */
byte AutoA      = 0;     /* When 1, autofire emulation for button A      */
byte AutoB      = 0;     /* When 1, autofire emulation for button B      */

char *SaveName  = NULL;  /* .sav file name                               */

char *SndName   = NULL;  /* Soundtrack log file                          */
FILE *SndStream = NULL;

struct                   /* Cheat list to be filled before StartGB()     */
{                        /* is called                                    */
  byte Value,Orig;
  word Address;
} Cheats[MAXCHEAT];
int CheatCount  = 0;     /* Number of cheats in the list                 */

byte SIOBuf;             /* Next byte to output via the serial line      */
byte SIOFlag;            /* Bit0 -> SIO interrupt should be generated    */
                         /* Bit1 -> Data should be sent out              */

byte JoyState;           /* Joystick state: START.SELECT.B.A.D.U.L.R     */

byte IMask;              /* A mask to reset an event bit in IFLAGS       */

byte MBCType;            /* MBC type: 1 for MBC2, 0 for MBC1             */
byte *ROMMap[256];       /* Addresses of ROM banks                       */
byte ROMBank;            /* Number of ROM bank currently used            */
byte ROMMask;            /* Mask for the ROM bank number                 */
int  ROMBanks;           /* Total number of ROM banks                    */
byte *RAMMap[256];       /* Addresses of RAM banks                       */ 
byte RAMBank;            /* Number of RAM bank currently used            */
byte RAMMask;            /* Mask for the RAM bank number                 */
int  RAMBanks;           /* Total number of RAM banks                    */

unsigned long TCount,TStep;  /* Timer counter and increment              */

byte BPal[4];            /* Background palette                           */
byte SPal0[4],SPal1[4];  /* Sprite palettes                              */

byte *ChrGen;            /* Character generator                          */
byte *BgdTab,*WndTab;    /* Background and window character tables       */

byte SprFlag;            /* <>0: sprites were enabled during the frame   */

/*** Following are some known manufacturer codes *************************/
struct { word Code;char *Name; } Companies[] =
{
  { 0x3301,"Nintendo"  },{ 0x7901,"Accolade"  },{ 0xA400,"Konami"    },
  { 0x6701,"Ocean"     },{ 0x5601,"LJN"       },{ 0x9900,"ARC?"      },
  { 0x0101,"Nintendo"  },{ 0x0801,"Capcom"    },{ 0x0100,"Nintendo"  },
  { 0xBB01,"SunSoft"   },{ 0xA401,"Konami"    },{ 0xAF01,"Namcot?"   },
  { 0x4901,"Irem"      },{ 0x9C01,"Imagineer" },{ 0xA600,"Kawada?"   },
  { 0xB101,"Nexoft"    },{ 0x5101,"Acclaim"   },{ 0x6001,"Titus"     },
  { 0xB601,"HAL"       },{ 0x3300,"Nintendo"  },{ 0x0B00,"Coconuts?" },
  { 0x5401,"Gametek"   },{ 0x7F01,"Kemco?"    },{ 0xC001,"Taito"     },
  { 0xEB01,"Atlus"     },{ 0xE800,"Asmik?"    },{ 0xDA00,"Tomy?"     },
  { 0xB100,"ASCII?"    },{ 0xEB00,"Atlus"     },{ 0xC000,"Taito"     },
  { 0x9C00,"Imagineer" },{ 0xC201,"Kemco?"    },{ 0xD101,"Sofel?"    },
  { 0x6101,"Virgin"    },{ 0xBB00,"SunSoft"   },{ 0xCE01,"FCI?"      },
  { 0xB400,"Enix?"     },{ 0xBD01,"Imagesoft" },{ 0x0A01,"Jaleco?"   },
  { 0xDF00,"Altron?"   },{ 0xA700,"Takara?"   },{ 0xEE00,"IGS?"      },
  { 0x8300,"Lozc?"     },{ 0x5001,"Absolute?" },{ 0xDD00,"NCS?"      },
  { 0xE500,"Epoch?"    },{ 0xCB00,"VAP?"      },{ 0x8C00,"Vic Tokai" },
  { 0xC200,"Kemco?"    },{ 0xBF00,"Sammy?"    },
  { 0x1800,"Hudson Soft"    },{ 0xCA01,"Palcom/Ultra" },
  { 0xCA00,"Palcom/Ultra"   },{ 0xC500,"Data East?" },
  { 0xA900,"Technos Japan?" },{ 0xD900,"Banpresto?" },
  { 0x7201,"Broderbund?"    },{ 0x7A01,"Triffix Entertainment?" },
  { 0xE100,"Towachiki?"     },{ 0x9300,"Tsuburava?" },
  { 0xC600,"Tonkin House?"  },{ 0xCE00,"Pony Canyon" },
  { 0x7001,"Infogrames?"    },{ 0x8B01,"Bullet-Proof Software?" },
  { 0x5501,"Park Place?"    },{ 0xEA00,"King Records?" },
  { 0x5D01,"Tradewest?"     },{ 0x6F01,"ElectroBrain?" },
  { 0xAA01,"Broderbund?"    },{ 0xC301,"SquareSoft" },
  { 0x5201,"Activision?"    },{ 0x5A01,"Bitmap Brothers/Mindscape" },
  { 0x5301,"American Sammy" },{ 0x4701,"Spectrum Holobyte" },
  { 0x1801,"Hudson Soft"},{ 0x0000,NULL }
};

void DoWrite(word A,byte V);

byte M_RDMEM(register word A)
{ return(Page[A>>13][A&0x1FFF]); }

void M_WRMEM(register word A,register byte V)
{

  if(A>0xFEFF) DoWrite(A,V);
  else if(A>0x7FFF) Page[A>>13][A&0x1FFF]=V;
       else

  switch(A&0xE000)
  {
    case 0xC000:
    case 0x8000: RAM[A]=V;return;
    case 0xA000: Page[5][A&0x1FFF]=V;return;
    case 0xE000: DoWrite(A,V);return;
    case 0x2000:
      if(MBCType&&((A&0xFF00)!=0x2100)) return;
      V&=ROMMask;
      if(!V) V++;
      if(ROMMask&&(V!=ROMBank))
      {
        ROMBank=V;
        Page[2]=ROMMap[V]? ROMMap[V]:RAM+0x4000;
        Page[3]=Page[2]+0x2000;
        if(Verbose&0x08) printf("ROM: Bank %d selected\n",V);
      }
      return;
    case 0x4000:
      V&=RAMMask;
      if(RAMMask&&!MBCType&&(RAMBank!=V))
      {
        RAMBank=V;
        Page[5]=RAMMap[V]? RAMMap[V]:RAM+0xA000;
        if(Verbose&0x08) printf("RAM: Bank %d selected\n",V);
      }
      return;
    case 0x0000:
    case 0x6000:
      if(Verbose&0x02) printf("Wrote %Xh to ROM at %Xh\n",V,A);
      return;
  }
}

void DoWrite(register word A,register byte V)
{
  static unsigned long TPFreqs[] = { 4096,262144,65536,16384 };
  register byte *P;

  switch(A)
  {
    case 0xFF00: JOYPAD=0xCF|V;
                 if(!(V&0x20)) JOYPAD&=(JoyState>>4)|0xF0;
                 if(!(V&0x10)) JOYPAD&=JoyState|0xF0; 
                 return;
    case 0xFF01: SIOBuf=V;SIOFlag|=0x02;return;
    case 0xFF02: if(V&0x80)
                 {
                   if(SIOFlag&0x02? SIOSend(SIOBuf):SIOReceive(&SIODATA))
                     SIOFlag|=0x01;
                   SIOFlag=(SIOFlag&0xFD);
                 }
                 V|=0x7E; 
                 break;
    case 0xFF07: TStep=(TPFreqs[V&0x03]<<16)/(154*60);
                 V|=0xF8;break;
    case 0xFF0F: V&=0x1F;break;
    case 0xFFFF: V&=0x1F;break;
    case 0xFF46: P=RAM+0xFE00;A=(word)V<<8;
                 for(V=0;V<0xA0;V++) *P++=M_RDMEM(A++);
                 return;
    case 0xFF41: V=(V&0xF8)|(LCDSTAT&0x07);
                 break;
    case 0xFF40: ChrGen=RAM+(V&0x10? 0x8000:0x8800);
                 BgdTab=RAM+(V&0x08? 0x9C00:0x9800);
                 WndTab=RAM+(V&0x40? 0x9C00:0x9800);
                 break;
    case 0xFF44: V=0;break;
    case 0xFF47: BPal[0]=V&0x03;
                 BPal[1]=(V&0x0C)>>2;
                 BPal[2]=(V&0x30)>>4;
                 BPal[3]=(V&0xC0)>>6;
                 break;
    case 0xFF48: SPal0[0]=V&0x03;
                 SPal0[1]=(V&0x0C)>>2;
                 SPal0[2]=(V&0x30)>>4;
                 SPal0[3]=(V&0xC0)>>6;
                 break;
    case 0xFF49: SPal1[0]=V&0x03;
                 SPal1[1]=(V&0x0C)>>2;
                 SPal1[2]=(V&0x30)>>4;
                 SPal1[3]=(V&0xC0)>>6;
                 break;
    default:     if((A>=0xFF10)&&(A<=0xFF26))
                 {
                   if(SndStream)
                   { fputc(A-0xFF10,SndStream);fputc(V,SndStream); }
                   Sound(A-0xFF10,V);
                 }
  }
  RAM[A]=V;
}

int StartGB(char *CartName)
{
  static char *CartTypes[] =
  {
    "ROM ONLY","ROM+MBC1","ROM+MBC1+RAM",
    "ROM+MBC1+RAM+BATTERY","UNKNOWN",
    "ROM+MBC2","ROM+MBC2+BATTERY"
  };

  int Checksum,I,J,*T;
  FILE *F;
  char *P,S[50];
  word A;
  reg R;

  /*** STARTUP CODE starts here: ***/
  T=(int *)"\01\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
#ifdef LSB_FIRST
  if(*T!=1)
  {
    puts("********** This machine is high-endian. *********");
    puts("Take #define LSB_FIRST out and compile VGB again.");
    return(0);
  }
#else
  if(*T==1)
  {
    puts("********* This machine is low-endian. *********");
    puts("Insert #define LSB_FIRST and compile VGB again.");
    return(0);
  }
#endif

  RAM=NULL;SaveName=NULL;SndStream=NULL;
  for(I=0;I<256;I++) RAMMap[I]=ROMMap[I]=NULL;

  if(Verbose) printf("Allocating 64kB for address space...");
  if(!(RAM=malloc(0x10000))) { if(Verbose) puts("FAILED");return(0); }
  memset(RAM,NORAM,0x10000);
  for(I=0;I<8;I++) Page[I]=RAM+I*0x2000;

  if(Verbose) printf("OK\nOpening %s...",CartName);
  if(!(F=fopen(CartName,"rb")))
  { if(Verbose) puts("FAILED");return(0); }

  if(Verbose) printf("reading...");
  if(fread(RAM,1,0x4000,F)!=0x4000)
  { if(Verbose) puts("FAILED");return(0); }

  ROMMap[0]=RAM;
  ROMBanks=2<<RAM[0x0148];
  RAMBanks=(RAM[0x0149]&0x03)==3? 4:0;
  Checksum=((word)RAM[0x014E]<<8)+RAM[0x014F];
  MBCType=RAM[0x0147]>3;

  P=NULL;
  if((RAM[0x0147]==4)||(RAM[0x0147]>6)) P="Unknown ROM type";
  if(P)
  {
    printf("\nError loading cartridge: %s\n",P);
    fclose(F);return(0);
  }

  if(Verbose)
  {
    strncpy(S,RAM+0x0134,16);S[16]='\0';
    printf("OK\n  Name: %s\n",S);
    printf("  Type: %s\n",CartTypes[RAM[0x0147]]);
    printf("  ROM Size: %dx16kB\n",ROMBanks);
    J=(RAM[0x0149]&0x03)*2;J=J? (1<<(J-1)):0;
    printf("  RAM Size: %dkB\n",J);

    J=((word)RAM[0x014B]<<8)+RAM[0x014A];
    for(I=0,P=NULL;!P&&Companies[I].Name;I++)
      if(J==Companies[I].Code) P=Companies[I].Name;
    printf("  Manufacturer ID: %Xh",J);
    printf(" [%s]\n",P? P:"?");

    printf("  Version Number: %Xh\n",RAM[0x014C]);
    printf("  Complement Check: %Xh\n",RAM[0x014D]);
    printf("  Checksum: %Xh\n",Checksum);
    J=((word)RAM[0x0103]<<8)+RAM[0x0102];
    printf("  Start Address: %Xh\n",J);
  }

  Checksum+=RAM[0x014E]+RAM[0x014F];
  for(I=0;I<0x4000;I++) Checksum-=RAM[I];

  if(Verbose) printf("Loading %dx16kB ROM banks:\n.",ROMBanks);
  for(I=1;I<ROMBanks;I++)
    if(ROMMap[I]=malloc(0x4000))
      if(fread(ROMMap[I],1,0x4000,F)==0x4000)
      {
        for(J=0;J<0x4000;J++) Checksum-=ROMMap[I][J];
        if(Verbose) putchar('.');
      }
      else { if(Verbose) puts("READ FAILED");break; }
    else { if(Verbose) puts("MALLOC FAILED");break; }

  fclose(F);if(I<ROMBanks) return(0);

  if(CheckCRC&&(Checksum&0xFFFF))
  { puts("\nError loading cartridge: Checksum is wrong");return(0); }

  if(Verbose) puts("OK");

  if(RAMBanks&&!MBCType)
  {
    if(Verbose) printf("Allocating %dx8kB RAM banks...",RAMBanks);
    for(I=0;I<RAMBanks;I++)
      if(RAMMap[I]=malloc(0x2000))
        memset(RAMMap[I],0,0x2000);
      else
      { if(Verbose) puts("FAILED");return(0); }
    if(Verbose) puts("OK");
  }

  if((RAM[0x0147]!=3)&&(RAM[0x0147]!=6)) SaveName=NULL;
  else
    if(SaveName=malloc(strlen(CartName)+10))
    {
      strcpy(SaveName,CartName);
      if(P=strrchr(SaveName,'.')) strcpy(P,".sav");
      else strcat(SaveName,".sav");

      if(Verbose) printf("Opening %s...",SaveName);
      if(F=fopen(SaveName,"rb"))
      {
        if(Verbose) printf("reading...");
        J=RAM[0x0147]==3? 0x2000:0x0200;
        J=(fread(RAMMap[0]? RAMMap[0]:Page[5],1,J,F)==J);
        if(Verbose) puts(J? "OK":"FAILED");
        fclose(F);
      }
      else if(Verbose) puts("FAILED");
    }

  if(CheatCount>0)
  {
    if(Verbose) puts("Patching cheats into the ROM code:");
    for(J=0;J<CheatCount;J++)
    {
      A=Cheats[J].Address;
      if(Verbose)
        printf("  at %Xh: %Xh -> %Xh\n",A,Cheats[J].Orig,Cheats[J].Value);
      if(A<0x4000)
      { if(ROMMap[0][A]==Cheats[J].Orig) ROMMap[0][A]=Cheats[J].Value; }
      else
        for(I=0,A-=0x4000;I<ROMBanks;I++)
          if(ROMMap[I][A]==Cheats[J].Orig) ROMMap[I][A]=Cheats[J].Value;
    }
  }

  if(SndName)
  {
    if(Verbose) printf("Logging soundtrack to %s...",SndName);
    SndStream=fopen(SndName,"wb");
    if(Verbose) puts(SndStream? "OK":"FAILED");
    if(SndStream) fprintf(SndStream,"GameBoy Soundtrack File 1.0\032");
  }

  if(ROMBanks<3) ROMMask=0;
  else { for(I=1;I<ROMBanks;I<<=1);ROMMask=I-1;ROMBank=1; }
  if(!RAMMap[0]) RAMMask=0;
  else { for(I=1;I<RAMBanks;I<<=1);RAMMask=I-1;RAMBank=0; }

  if(RAMMap[0]) Page[5]=RAMMap[0];
  if(ROMMap[1]) Page[2]=ROMMap[1];
  Page[3]=Page[2]+0x2000;

  IPeriod=VPeriod/154/11;
  TStep=32768;TCount=0;

  ChrGen=RAM+0x8800;BgdTab=WndTab=RAM+0x9800;
  LCDCONT=0x81;LCDSTAT=0x00;
  CURLINE=0x00;CMPLINE=0xFF;
  IFLAGS=ISWITCH=0x00;
  TIMECNT=TIMEMOD=0x01;
  TIMEFRQ=0xF8;
  SIODATA=0x00;
  SIOCONT=0x7E;
  SIOBuf=SIOFlag=0x00;
  SprFlag=0;
  JoyState=0xFF;

  for(I=0;I<4;I++) SPal0[I]=SPal1[I]=BPal[I]=I;
  BGRDPAL=SPR0PAL=SPR1PAL=0xE4;

  if(Verbose) printf("RUNNING ROM CODE...\n");
  ResetZ80(&R);A=Z80(R);

  if(Verbose) printf("EXITED at PC = %04Xh.\n",A); 
  return(1);
}

void TrashGB(void)
{
  FILE *F;
  int I;

  if(SaveName)
  {
    if(Verbose) printf("\nOpening %s...",SaveName);   
    if(F=fopen(SaveName,"wb"))
    { 
      if(Verbose) printf("writing...");
      I=RAM[0x0147]==3? 0x2000:0x0200;
      I=(fwrite(RAMMap[0]? RAMMap[0]:Page[5],1,I,F)==I);
      if(Verbose) puts(I? "OK":"FAILED");
      fclose(F);
    }
    else if(Verbose) puts("FAILED");
    free(SaveName);
  }

  if(RAM) free(RAM);
  for(I=1;ROMMap[I];I++) free(ROMMap[I]);
  for(I=0;RAMMap[I];I++) free(RAMMap[I]);
  if(SndName&&SndStream) fclose(SndStream);
}

word Interrupt(void)
{
  static byte LCDStates[11] = { 2,2,3,3,3,3,0,0,0,0,0 };
  static byte LCount=0;
  static byte UCount=1;
  static byte ACount=0;
  register byte J,I;

  DIVREG++;LCount=(LCount+1)%11;
  if(CURLINE<144) LCDSTAT=(LCDSTAT&0xFC)|LCDStates[LCount];

  switch(LCount)
  {
    case 0:
      /* Proceeding with line refresh */
      break;
    case 2:
      /* Generating VBlank interrupt */
      if(CURLINE==144)
        if((ISWITCH&VBL_IFLAG)&&(LCDCONT&0x80))
        { IFLAGS=IMask=VBL_IFLAG;return(0x0040); }
      return(0xFFFF);
    case 7:
      /* Generating HBlank interrupt */
      if((LCDSTAT&0x08)&&(CURLINE<144))
        if((ISWITCH&LCD_IFLAG)&&(LCDCONT&0x80))
        { IFLAGS=IMask=LCD_IFLAG;return(0x0048); }
      return(0xFFFF);
    default:
      /* Only LCD state had to be changed */
      return(0xFFFF);
  }

  CURLINE=(CURLINE+1)%154;
  if(!UCount&&(CURLINE<144)) RefreshLine(CURLINE);
  SprFlag|=LCDCONT&0x02;

  /* Checking for line coincidence */
  J=LineDelay? (CURLINE+1)%154:CURLINE;
  if(J!=CMPLINE) { LCDSTAT&=0xFB;J=0x00; }
  else           { LCDSTAT|=0x04;J=0x40; }

  /* If end of frame reached... */
  if(CURLINE==144)
  {
    /* Set VBlank state */
    LCDSTAT=(LCDSTAT&0xFC)|0x01;J|=0x10;

    /* Write interrupt timestamp into sound log */
    if(SndStream) fputc(0xFF,SndStream);

    /* Refresh screen if needed */
    if(UCount) UCount--;
    else
    {
      if(SprFlag&&(LCDCONT&0x80)) RefreshSprites();
      RefreshScreen();SprFlag=0;UCount=UPeriod;
    }

    /* Generate serial IO interrupt */
    if(SIOFlag&0x01)
    {
      SIOFlag&=0xFE;SIOCONT&=0x7F;
      if(ISWITCH&SIO_IFLAG) IFLAGS|=SIO_IFLAG;
    }

    /* Update joystick */
    JoyState=Joystick();

    /* Autofire emulation */
    ACount=(ACount+1)&0x07;
    if(ACount>3)
    {
      if(AutoA) JoyState|=0x10;
      if(AutoB) JoyState|=0x20;
    }

    /* Assign value to JOYPAD */
    I=JOYPAD|0xCF;
    if(!(I&0x10)) JOYPAD=I&(JoyState|0xF0);
    if(!(I&0x20)) JOYPAD=I&((JoyState>>4)|0xF0);
  }

  /* Generating LCD controller interrupt */
  if((J&LCDSTAT)&&(ISWITCH&LCD_IFLAG)&&(LCDCONT&0x80)) IFLAGS|=LCD_IFLAG;

  /* Generating timer interrupt */
  if(TIMEFRQ&0x04)
  {
    TCount+=TStep;
    if(TCount&0xFFFF0000)
    {
      unsigned long L;
      L=TIMECNT+(TCount>>16);
      TCount&=0x0000FFFF;
      if(L&0xFFFFFF00)
      {
        TIMECNT=TIMEMOD;
        if(ISWITCH&TIM_IFLAG) IFLAGS|=TIM_IFLAG;
      }
      else TIMECNT=L;
    }
  }

  /* Determining interrupt address */
  if(J=IFLAGS)
  {
    if(J&EXT_IFLAG) { IMask=EXT_IFLAG;return(0x0060); }
    if(J&SIO_IFLAG) { IMask=SIO_IFLAG;return(0x0058); }
    if(J&TIM_IFLAG) { IMask=TIM_IFLAG;return(0x0050); }
    if(J&LCD_IFLAG) { IMask=LCD_IFLAG;return(0x0048); }
    if(J&VBL_IFLAG) { IMask=VBL_IFLAG;return(0x0040); }
  }

  /* No interrupt */
  return(0xFFFF);
} 

int AddCheat(char *Cheat)
{
  static char Digits[]="0123456789ABCDEF";
  int X1,X2;

  if(CheatCount>=MAXCHEAT) return(0);
  else
  {
    if((Cheat[3]!='-')||(Cheat[7]!='-')) return(0);
    X1=strchr(Digits,toupper(Cheat[0]))-Digits;
    X2=strchr(Digits,toupper(Cheat[1]))-Digits;
    if((X1<0)||(X2<0)) return(0);
    else Cheats[CheatCount].Value=X1*16+X2;
    X1=strchr(Digits,toupper(Cheat[4]))-Digits;
    X2=strchr(Digits,toupper(Cheat[5]))-Digits;
    if((X1<0)||(X2<0)) return(0);
    else Cheats[CheatCount].Address=X1*16+X2;
    X1=strchr(Digits,toupper(Cheat[6]))-Digits;
    X2=strchr(Digits,toupper(Cheat[2]))-Digits;
    if((X1<0)||(X2<0)) return(0);
    else Cheats[CheatCount].Address+=256*((15-X1)*16+X2);
    X1=strchr(Digits,toupper(Cheat[10]))-Digits;
    X2=strchr(Digits,toupper(Cheat[8]))-Digits;
    if((X1<0)||(X2<0)) return(0);
    X1=~(16*X2+X1);
    X1=((X1>>2)&0x3F)|((X1<<6)&0xC0);
    Cheats[CheatCount].Orig=X1^0x45;

    if(Cheats[CheatCount].Address>=0x8000) return(0);
    CheatCount++;return(1);
  }
}