Created
May 22, 2018 13:14
-
-
Save tankorsmash/88fe7773fe8ea4f8e4c2d4abfaef352d to your computer and use it in GitHub Desktop.
SIMPLE INPUT RECORD/PLAYBACK (c) 2015 Brian Provinciano
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/******************************************************************************/ | |
// 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment