You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
6.8 KiB
C++
289 lines
6.8 KiB
C++
/******************************************************************************/
|
|
// SIMPLE INPUT RECORD/PLAYBACK
|
|
// (c) 2015 Brian Provinciano
|
|
//
|
|
// You are free to use this code for your own purposes, no strings attached.
|
|
//
|
|
// This is a very basic sample to record and playback button input.
|
|
// It's most useful when activated on startup, deactivated on shutdown for
|
|
// global button recording/playback.
|
|
//
|
|
// For details on more advanced implementations, see my GDC 2015 session:
|
|
// -> Automated Testing and Instant Replays in Retro City Rampage
|
|
// The slides and full video will be available on the GDC Vault at a later date.
|
|
/******************************************************************************/
|
|
|
|
|
|
/******************************************************************************/
|
|
// wrap it so it can be conditionally compiled in.
|
|
// for example, set INPUTREPLAY_CAN_RECORD to 1 to play the game and record the input, set it to 0 when done
|
|
// INPUTREPLAY_CAN_RECORD takes priority over INPUTREPLAY_CAN_PLAYBACK
|
|
|
|
#define INPUTREPLAY_CAN_PLAYBACK 1
|
|
#define INPUTREPLAY_CAN_RECORD 1
|
|
|
|
#define INPUTREPLAY_INCLUDED (INPUTREPLAY_CAN_PLAYBACK || INPUTREPLAY_CAN_RECORD)
|
|
/******************************************************************************/
|
|
|
|
#if INPUTREPLAY_INCLUDED
|
|
|
|
#define INPUT_BUTTONS_TOTAL 32 // up to 32
|
|
#define MAX_REC_LEN 0x8000 // the buffer size for storing RLE compressed button input (x each button)
|
|
|
|
/******************************************************************************/
|
|
typedef struct
|
|
{
|
|
unsigned short *rledata;
|
|
unsigned short rlepos;
|
|
unsigned short datalen;
|
|
unsigned short currentrun;
|
|
} ButtonRec;
|
|
/******************************************************************************/
|
|
|
|
// if INPUTREPLAY_CAN_RECORD, as soon as this class is instanced, it will automatically record when instanced/created.
|
|
// statically creating this as a global will blanket the entire play session
|
|
//
|
|
// if INPUTREPLAY_CAN_PLAYBACK, playback will begin as soon as LoadFile() is used
|
|
//
|
|
class SimpleInputRec
|
|
{
|
|
unsigned int m_buttonstate;
|
|
ButtonRec m_buttons[INPUT_BUTTONS_TOTAL];
|
|
bool m_bRecording;
|
|
|
|
unsigned char* m_data;
|
|
|
|
public:
|
|
SimpleInputRec()
|
|
: m_buttonstate(0)
|
|
, m_data(NULL)
|
|
, m_bRecording(true)
|
|
{
|
|
}
|
|
|
|
~SimpleInputRec()
|
|
{
|
|
if(m_data)
|
|
{
|
|
#if INPUTREPLAY_CAN_RECORD
|
|
WriteToFile();
|
|
#endif
|
|
delete[] m_data;
|
|
}
|
|
}
|
|
|
|
// run each frame before the game uses the live button input.
|
|
// when recording, it saves the live input
|
|
// during playback, it overwrites the live input
|
|
void Update(bool bForce = false);
|
|
|
|
// to start a playback
|
|
#if INPUTREPLAY_CAN_PLAYBACK
|
|
bool LoadFile(KSTR szfilename);
|
|
#endif
|
|
|
|
// to finish recording
|
|
#if INPUTREPLAY_CAN_RECORD
|
|
void WriteToFile();
|
|
#endif
|
|
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
void SimpleInputRec::Update(bool bForce)
|
|
{
|
|
#if INPUTREPLAY_CAN_RECORD
|
|
if(m_bRecording)
|
|
{
|
|
unsigned int newbuttons = nesinput.buttons;
|
|
|
|
// allocate and initialize
|
|
if(!m_data)
|
|
{
|
|
m_data = new unsigned char[INPUT_BUTTONS_TOTAL * MAX_REC_LEN * 2];
|
|
unsigned short* dataptr = (unsigned short*)m_data;
|
|
|
|
for(int i=0; i<INPUT_BUTTONS_TOTAL; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
|
|
btn.rledata = dataptr;
|
|
dataptr += MAX_REC_LEN;
|
|
|
|
btn.rlepos = 0;
|
|
btn.currentrun = 0;
|
|
btn.datalen = MAX_REC_LEN;
|
|
}
|
|
}
|
|
|
|
// write RLE button bit streams
|
|
for(int i=0; i<INPUT_BUTTONS_TOTAL; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
|
|
if(bForce || (newbuttons&(1<<i)) != (m_buttonstate&(1<<i)) || btn.currentrun==0x7FFF)
|
|
{
|
|
if(btn.currentrun)
|
|
{
|
|
int bit = (m_buttonstate>>i)&1;
|
|
btn.rledata[btn.rlepos++] = (bit<<15) | btn.currentrun;
|
|
}
|
|
btn.currentrun = bForce? 0 : 1;
|
|
}
|
|
else
|
|
{
|
|
++btn.currentrun;
|
|
}
|
|
}
|
|
|
|
m_buttonstate = newbuttons;
|
|
}
|
|
#endif
|
|
|
|
#if INPUTREPLAY_CAN_PLAYBACK
|
|
if(!m_bRecording)
|
|
{
|
|
bool bIsRunning = false;
|
|
for(int i=0; i<INPUT_BUTTONS_TOTAL; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
if(btn.rledata)
|
|
{
|
|
bIsRunning = true;
|
|
if(!btn.currentrun && btn.rlepos<btn.datalen)
|
|
{
|
|
unsigned short value = btn.rledata[btn.rlepos++];
|
|
btn.currentrun = value&0x7FFF;
|
|
m_buttonstate &= ~(1<<i);
|
|
m_buttonstate |= ((value>>15)&1)<<i;
|
|
--btn.currentrun;
|
|
}
|
|
else
|
|
{
|
|
if(btn.currentrun)
|
|
{
|
|
--btn.currentrun;
|
|
}
|
|
else if(btn.rlepos==btn.datalen)
|
|
{
|
|
btn.rledata = NULL;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(bIsRunning)
|
|
{
|
|
// TODO: this is where you can overwrite the live button state to the prerecorded one
|
|
systeminput.buttons = m_buttonstate;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/******************************************************************************/
|
|
|
|
#if INPUTREPLAY_CAN_PLAYBACK
|
|
bool SimpleInputRec::LoadFile(KSTR szfilename)
|
|
{
|
|
for(int i=0; i<INPUT_BUTTONS_TOTAL; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
|
|
btn.datalen = 0;
|
|
btn.rledata = NULL;
|
|
btn.rlepos = 0;
|
|
btn.currentrun = 0;
|
|
}
|
|
|
|
delete[] m_data;
|
|
m_bRecording = false;
|
|
|
|
if(fcheckexists(szfilename))
|
|
{
|
|
FILE* f = fopen(szfilename, "wb");
|
|
if(f)
|
|
{
|
|
// WARNING: You'll want to do more error checking, but just to keep it simple...
|
|
|
|
fseek(f,0,SEEK_END);
|
|
unsigned long filelen = ftell(f);
|
|
fseek(f,0,SEEK_SET);
|
|
|
|
m_data = new unsigned char[filelen];
|
|
fread(m_data, 1, filelen, f);
|
|
fclose(f);
|
|
|
|
unsigned char* bufptr = m_data;
|
|
int numbuttons = bufptr[0] | (bufptr[1]<<8);
|
|
bufptr += 2;
|
|
if(numbuttons <= INPUT_BUTTONS_TOTAL)
|
|
{
|
|
for(int i=0; i<numbuttons; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
|
|
btn.datalen = bufptr[0] | (bufptr[1]<<8);
|
|
bufptr += 2;
|
|
}
|
|
|
|
for(int i=0; i<numbuttons; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
if(btn.datalen)
|
|
{
|
|
// WARNING: Endian dependent for simplcicity
|
|
btn.rledata = (unsigned short*)bufptr;
|
|
bufptr += btn.datalen*2;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
/******************************************************************************/
|
|
|
|
#if INPUTREPLAY_CAN_RECORD
|
|
void SimpleInputRec::WriteToFile()
|
|
{
|
|
if(m_data && m_bRecording)
|
|
{
|
|
Update(true);
|
|
|
|
FILE* f = fopen("_autorec.rec","wb");
|
|
if(f)
|
|
{
|
|
fputc(INPUT_BUTTONS_TOTAL, f);
|
|
fputc(0, f);
|
|
|
|
for(int i=0; i<INPUT_BUTTONS_TOTAL; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
|
|
fputc((unsigned char)btn.rlepos, f);
|
|
fputc(btn.rlepos >> 8, f);
|
|
}
|
|
for(int i=0; i<INPUT_BUTTONS_TOTAL; ++i)
|
|
{
|
|
ButtonRec& btn = m_buttons[i];
|
|
|
|
// WARNING: Endian dependent for simplcicity
|
|
fwrite(btn.rledata, 2, btn.rlepos, f);
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/******************************************************************************/
|
|
|
|
#endif // INPUTREPLAY_INCLUDED
|
|
|