From 24a33f940df2cce689ae3e66a4dcecfec5ffa9af Mon Sep 17 00:00:00 2001
From: jsalmon3 <>
Date: Thu, 1 Aug 2002 16:22:02 +0000
Subject: [PATCH] Added initial support for game replay

---
 src/include/commands.h      |   9 +-
 src/network/commands.cpp    | 370 ++++++++++++++++++++++++++---
 src/stratagus/mainloop.cpp  |   3 +
 src/stratagus/stratagus.cpp |   2 +-
 src/ui/menus.cpp            | 447 +++++++++++++++++++++++++++++++++++-
 5 files changed, 793 insertions(+), 38 deletions(-)

diff --git a/src/include/commands.h b/src/include/commands.h
index 64738db5c..9fc709b7d 100644
--- a/src/include/commands.h
+++ b/src/include/commands.h
@@ -32,12 +32,19 @@
 --	Variables
 ----------------------------------------------------------------------------*/
 
-extern int CommandLogEnabled;		/// True, if command log is on
+extern int CommandLogDisabled;		/// True, if command log is off
 
 /*----------------------------------------------------------------------------
 --	Functions
 ----------------------------------------------------------------------------*/
 
+    /// Replay user commands from log each cycle
+extern void ReplayEachCycle(void);
+    /// Load replay
+extern int LoadReplay(char* name);
+    /// End logging
+extern void EndReplayLog(void);
+
 /*
 **	The send command functions sends a command, if needed over the
 **	Network, this is only for user commands. Automatic reactions which
diff --git a/src/network/commands.cpp b/src/network/commands.cpp
index e06dd958a..668bc5a3e 100644
--- a/src/network/commands.cpp
+++ b/src/network/commands.cpp
@@ -44,6 +44,10 @@
 #include "network.h"
 #include "netconnect.h"
 #include "campaign.h"			// for CurrentMapPath
+#include "ccl.h"
+#include "commands.h"
+#include "interface.h"
+#include "iocompat.h"
 
 //----------------------------------------------------------------------------
 //	Declaration
@@ -53,7 +57,12 @@
 //	Variables
 //----------------------------------------------------------------------------
 
-global int CommandLogEnabled;		/// True if command log is on
+global int CommandLogDisabled;		/// True if command log is off
+local int DisabledLog;			/// Disabled log for replay
+local SCM ReplayLog;			/// Replay log
+local FILE* LogFile;			/// Replay log file
+local int NextLogCycle;			/// Next log cycle number
+
 
 //----------------------------------------------------------------------------
 //	Log commands
@@ -79,9 +88,7 @@ global int CommandLogEnabled;		/// True if command log is on
 local void CommandLog(const char* name,const Unit* unit,int flag,
 	unsigned x,unsigned y,const Unit* dest,const char* value,int num)
 {
-    static FILE* logf;
-
-    if( !CommandLogEnabled ) {		// No log wanted
+    if( CommandLogDisabled ) {		// No log wanted
 	return;
     }
 
@@ -89,15 +96,25 @@ local void CommandLog(const char* name,const Unit* unit,int flag,
     //	Create and write header of log file. The player number is added
     //  to the save file name, to test more than one player on one computer.
     //
-    if( !logf ) {
+    if( !LogFile ) {
 	time_t now;
-	char buf[256];
+	char buf[PATH_MAX];
 	char* s;
 	char* s1;
 
-	sprintf(buf,"log_of_freecraft_%d.log",ThisPlayer->Player);
-	logf=fopen(buf,"wb");
-	if( !logf ) {
+#ifdef USE_WIN32
+	strcpy(buf,"logs");
+	mkdir(buf);
+#else
+	sprintf(buf,"%s/%s",getenv("HOME"),FREECRAFT_HOME_PATH);
+	mkdir(buf,0777);
+	strcat(buf,"/logs");
+	mkdir(buf,0777);
+#endif
+
+	sprintf(buf,"%s/log_of_freecraft_%d.log",buf,ThisPlayer->Player);
+	LogFile=fopen(buf,"wb");
+	if( !LogFile ) {
 	    return;
 	}
 
@@ -109,34 +126,35 @@ local void CommandLog(const char* name,const Unit* unit,int flag,
 
 	//
 	//	Parseable header
+	//	TODO: add custom start options (eg high resources, game type, etc)
 	//
-	fprintf(logf,";;;(replay-log\n");
-	fprintf(logf,";;;  'comment\t\"Generated by FreeCraft Version " VERSION "\"\n");
-	fprintf(logf,";;;  'comment\t\"Visit http://FreeCraft.Org for more informations\"\n");
-	fprintf(logf,";;;  'comment\t\"$Id$\"\n");
-	fprintf(logf,";;;  'type\t\"%s\"\n","single-player");
-	fprintf(logf,";;;  'date\t\"%s\"\n",s);
-	fprintf(logf,";;;  'map\t\"%s\"\n",TheMap.Description);
-	fprintf(logf,";;;  'map-id\t%u\n",TheMap.Info->MapUID);
-	fprintf(logf,";;;  'map-path\t\"%s\"\n",CurrentMapPath);
-	fprintf(logf,";;;  'engine\t'(%d %d %d)\n",
+	fprintf(LogFile,"(replay-log\n");
+	fprintf(LogFile,"  'comment\t\"Generated by FreeCraft Version " VERSION "\"\n");
+	fprintf(LogFile,"  'comment\t\"Visit http://FreeCraft.Org for more informations\"\n");
+	fprintf(LogFile,"  'comment\t\"$Id$\"\n");
+	fprintf(LogFile,"  'type\t\"%s\"\n","single-player");
+	fprintf(LogFile,"  'date\t\"%s\"\n",s);
+	fprintf(LogFile,"  'map\t\"%s\"\n",TheMap.Description);
+	fprintf(LogFile,"  'map-id\t%u\n",TheMap.Info->MapUID);
+	fprintf(LogFile,"  'map-path\t\"%s\"\n",CurrentMapPath);
+	fprintf(LogFile,"  'engine\t'(%d %d %d)\n",
 	    FreeCraftMajorVersion,FreeCraftMinorVersion,FreeCraftPatchLevel);
-	fprintf(logf,";;;  'network\t'(%d %d %d)\n",
+	fprintf(LogFile,"  'network\t'(%d %d %d)\n",
 	    NetworkProtocolMajorVersion,
 	    NetworkProtocolMinorVersion,
 	    NetworkProtocolPatchLevel);
-	fprintf(logf,";;;  )\n");
+	fprintf(LogFile,"  )\n");
     }
 
     //
     //	Frame, unit, (type-ident only to be better readable).
     //
     if( unit ) {
-	fprintf(logf,"(log %lu 'U%d '%s '%s '%s",
+	fprintf(LogFile,"(log %lu 'unit %d 'ident '%s 'name '%s 'flag '%s",
 		GameCycle,UnitNumber(unit),unit->Type->Ident,name,
 		flag ? "flush" : "append");
     } else {
-	fprintf(logf,"(log %lu '%s '%s",
+	fprintf(LogFile,"(log %lu 'name '%s 'flag '%s",
 		GameCycle,name, flag ? "flush" : "append");
     }
 
@@ -144,35 +162,325 @@ local void CommandLog(const char* name,const Unit* unit,int flag,
     //	Coordinates given.
     //
     if( x!=-1 || y!=-1 ) {
-	fprintf(logf," '(%d %d)",x,y);
+	fprintf(LogFile," 'pos '(%d %d)",x,y);
     }
     //
     //	Destination given.
     //
     if( dest ) {
-	fprintf(logf," 'U%d",UnitNumber(dest));
+	fprintf(LogFile," 'dest '%d",UnitNumber(dest));
     }
     //
     //	Value given.
     //
     if( value ) {
-	fprintf(logf," '%s",value);
+	fprintf(LogFile," 'value '%s",value);
     }
     //
     //	Number given.
     //
     if( num!=-1 ) {
-	fprintf(logf," %d",num);
+	fprintf(LogFile," 'num %d",num);
     }
     if( unit ) {
-	fprintf(logf,") ;%d:%d %X",unit->Player->Player,unit->Refs,
+	fprintf(LogFile,") ;%d:%d %X",unit->Player->Player,unit->Refs,
 		SyncRandSeed);
     } else {
-	fprintf(logf,") ;-:- %X",SyncRandSeed);
+	fprintf(LogFile,") ;-:- %X",SyncRandSeed);
     }
 
-    fprintf(logf,"\n");
-    fflush(logf);
+    fprintf(LogFile,"\n");
+    fflush(LogFile);
+}
+
+/**
+**
+*/
+local SCM CclLog(SCM list)
+{
+    SCM var;
+
+    var=gh_symbol2scm("*replay_log*");
+    if( gh_null_p(symbol_value(var,NIL)) ) {
+	setvar(var,cons(list,NIL),NIL);
+    } else {
+	SCM tmp;
+	tmp=symbol_value(var,NIL);
+	while( !gh_null_p(gh_cdr(tmp)) ) {
+	    tmp=gh_cdr(tmp);
+	}
+	setcdr(tmp,cons(list,NIL));
+    }
+
+    return SCM_UNSPECIFIED;
+}
+
+/**
+**
+*/
+local SCM CclReplayLog(SCM list)
+{
+    SCM value;
+    SCM sublist;
+    const char* comment;
+    const char* logtype;
+    const char* logdate;
+    const char* map;
+    unsigned int mapid;
+    const char* mappath;
+    int ever1;
+    int ever2;
+    int ever3;
+    int nver1;
+    int nver2;
+    int nver3;
+
+    while( !gh_null_p(list) ) {
+	value=gh_car(list);
+	list=gh_cdr(list);
+
+	if( gh_eq_p(value,gh_symbol2scm("comment")) ) {
+	    comment=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("type")) ) {
+	    logtype=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("date")) ) {
+	    logdate=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("map")) ) {
+	    map=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("map-id")) ) {
+	    mapid=gh_scm2int(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("map-path")) ) {
+	    mappath=get_c_string(gh_car(list));
+	    strcpy(CurrentMapPath,mappath);
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("engine")) ) {
+	    sublist=gh_car(list);
+	    ever1=gh_scm2int(gh_car(sublist));
+	    sublist=gh_cdr(sublist);
+	    ever2=gh_scm2int(gh_car(sublist));
+	    sublist=gh_cdr(sublist);
+	    ever3=gh_scm2int(gh_car(sublist));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("network")) ) {
+	    sublist=gh_car(list);
+	    nver1=gh_scm2int(gh_car(sublist));
+	    sublist=gh_cdr(sublist);
+	    nver2=gh_scm2int(gh_car(sublist));
+	    sublist=gh_cdr(sublist);
+	    nver3=gh_scm2int(gh_car(sublist));
+	    list=gh_cdr(list);
+	}
+    }
+
+    return SCM_UNSPECIFIED;
+}
+
+/**
+**	Load a log file to replay a game
+**
+**	@param name	name of file to load.
+*/
+global int LoadReplay(char* name)
+{
+    gh_new_procedureN("log",CclLog);
+    gh_new_procedureN("replay-log",CclReplayLog);
+    gh_define("*replay_log*",NIL);
+    vload(name,0,1);
+
+    ReplayLog=symbol_value(gh_symbol2scm("*replay_log*"),NIL);
+    NextLogCycle=-1;
+    if( !CommandLogDisabled ) {
+	CommandLogDisabled=1;
+	DisabledLog=1;
+    }
+
+    return 0;
+}
+
+/**
+**	End logging
+*/
+global void EndReplayLog(void)
+{
+    if( LogFile ) {
+	fclose(LogFile);
+	LogFile=NULL;
+    }
+    if( DisabledLog ) {
+	CommandLogDisabled=0;
+	DisabledLog=0;
+    }
+}
+
+/**
+**	Do next replay
+*/
+local void DoNextReplay(void)
+{
+    SCM value;
+    SCM list;
+    int unit;
+    const char* ident;
+    const char* name;
+    const char* flag;
+    int flags;
+    int posx;
+    int posy;
+    int dest;
+    const char* val;
+    int num;
+
+    list=gh_car(ReplayLog);
+
+    NextLogCycle=gh_scm2int(gh_car(list));
+    list=gh_cdr(list);
+
+    if( NextLogCycle!=GameCycle ) {
+	return;
+    }
+
+    NextLogCycle=-1;
+    unit=-1;
+    name=NULL;
+    flags=0;
+    posx=-1;
+    posy=-1;
+    dest=-1;
+    val=NULL;
+    num=-1;
+    while( !gh_null_p(list) ) {
+	value=gh_car(list);
+	list=gh_cdr(list);
+
+	if( gh_eq_p(value,gh_symbol2scm("unit")) ) {
+	    unit=gh_scm2int(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("ident")) ) {
+	    ident=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("name")) ) {
+	    name=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("flag")) ) {
+	    flag=get_c_string(gh_car(list));
+	    if( !strcmp(flag,"flush") ) {
+		flags=1;
+	    }
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("pos")) ) {
+	    SCM sublist;
+	    sublist=gh_car(list);
+	    posx=gh_scm2int(gh_car(sublist));
+	    sublist=gh_cdr(sublist);
+	    posy=gh_scm2int(gh_car(sublist));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("dest")) ) {
+	    dest=gh_scm2int(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("value")) ) {
+	    val=get_c_string(gh_car(list));
+	    list=gh_cdr(list);
+	} else if( gh_eq_p(value,gh_symbol2scm("num")) ) {
+	    num=gh_scm2int(gh_car(list));
+	    list=gh_cdr(list);
+	}
+    }
+
+    if( !strcmp(name,"stop") ) {
+	SendCommandStopUnit(Units[unit]);
+    } else if( !strcmp(name,"stand-ground") ) {
+	SendCommandStandGround(Units[unit],flags);
+    } else if( !strcmp(name,"follow") ) {
+	SendCommandFollow(Units[unit],Units[dest],flags);
+    } else if( !strcmp(name,"move") ) {
+	SendCommandMove(Units[unit],posx,posy,flags);
+    } else if( !strcmp(name,"repair") ) {
+	SendCommandRepair(Units[unit],posx,posy,Units[dest],flags);
+    } else if( !strcmp(name,"attack") ) {
+	SendCommandAttack(Units[unit],posx,posy,Units[dest],flags);
+    } else if( !strcmp(name,"attack-ground") ) {
+	SendCommandAttackGround(Units[unit],posx,posy,flags);
+    } else if( !strcmp(name,"patrol") ) {
+	SendCommandPatrol(Units[unit],posx,posy,flags);
+    } else if( !strcmp(name,"board") ) {
+	SendCommandBoard(Units[unit],posx,posy,Units[dest],flags);
+    } else if( !strcmp(name,"unload") ) {
+	SendCommandUnload(Units[unit],posx,posy,dest!=-1?Units[dest]:NoUnitP,flags);
+    } else if( !strcmp(name,"build") ) {
+	SendCommandBuildBuilding(Units[unit],posx,posy,UnitTypeByIdent(val),flags);
+    } else if( !strcmp(name,"cancel-build") ) {
+	SendCommandCancelBuilding(Units[unit],Units[dest]);
+    } else if( !strcmp(name,"harvest") ) {
+	SendCommandHarvest(Units[unit],posx,posy,flags);
+    } else if( !strcmp(name,"mine") ) {
+	SendCommandMineGold(Units[unit],Units[dest],flags);
+    } else if( !strcmp(name,"haul") ) {
+	SendCommandHaulOil(Units[unit],Units[dest],flags);
+    } else if( !strcmp(name,"return") ) {
+	SendCommandReturnGoods(Units[unit],Units[dest],flags);
+    } else if( !strcmp(name,"train") ) {
+	SendCommandTrainUnit(Units[unit],UnitTypeByIdent(val),flags);
+    } else if( !strcmp(name,"cancel-train") ) {
+	SendCommandCancelTraining(Units[unit],num,UnitTypeByIdent(val));
+    } else if( !strcmp(name,"upgrade-to") ) {
+	SendCommandUpgradeTo(Units[unit],UnitTypeByIdent(val),flags);
+    } else if( !strcmp(name,"cancel-upgrade-to") ) {
+	SendCommandCancelUpgradeTo(Units[unit]);
+    } else if( !strcmp(name,"research") ) {
+	SendCommandResearch(Units[unit],UpgradeByIdent(val),flags);
+    } else if( !strcmp(name,"cancel-research") ) {
+	SendCommandCancelResearch(Units[unit]);
+    } else if( !strcmp(name,"demolish") ) {
+	SendCommandDemolish(Units[unit],posx,posy,Units[dest],flags);
+    } else if( !strcmp(name,"spell-cast") ) {
+	SendCommandSpellCast(Units[unit],posx,posy,Units[dest],num,flags);
+    } else if( !strcmp(name,"diplomacy") ) {
+	int state;
+	if( !strcmp(val,"neutral") ) {
+	    state=DiplomacyNeutral;
+	} else if( !strcmp(val,"allied") ) {
+	    state=DiplomacyAllied;
+	} else if( !strcmp(val,"enemy") ) {
+	    state=DiplomacyEnemy;
+	} else if( !strcmp(val,"crazy") ) {
+	    state=DiplomacyCrazy;
+	} else {
+	    DebugLevel0Fn("Invalid diplomacy command: %s" _C_ val);
+	    state=-1;
+	}
+	SendCommandDiplomacy(posx,state,posy);
+    } else {
+	DebugLevel0Fn("Invalid name: %s" _C_ name);
+    }
+
+    ReplayLog=gh_cdr(ReplayLog);
+}
+
+/**
+**	Replay user commands from log each cycle
+*/
+global void ReplayEachCycle(void)
+{
+    if( gh_null_p(ReplayLog) ) {
+	return;
+    }
+
+    if( NextLogCycle!=-1 && NextLogCycle!=GameCycle ) {
+	return;
+    }
+
+    do {
+	DoNextReplay();
+    } while( !gh_null_p(ReplayLog) && (NextLogCycle==-1 || NextLogCycle==GameCycle) );
+
+    if( gh_null_p(ReplayLog) ) {
+	SetMessage("End of replay");
+    }
 }
 
 //@}
diff --git a/src/stratagus/mainloop.cpp b/src/stratagus/mainloop.cpp
index d82260303..5b3ece5c1 100644
--- a/src/stratagus/mainloop.cpp
+++ b/src/stratagus/mainloop.cpp
@@ -58,6 +58,7 @@
 #include "campaign.h"
 #include "sound_server.h"
 #include "settings.h"
+#include "commands.h"
 
 #if defined(USE_SDLCD) || defined(USE_LIBCDA)
 #include "sound_server.h"
@@ -740,6 +741,7 @@ global void GameMainLoop(void)
 		fprintf(stderr,"FIXME: *** round robin ***\n");
 		fprintf(stderr,"FIXME: *** round robin ***\n");
 	    }
+	    ReplayEachCycle();
 	    NetworkCommands();		// Get network commands
 	    UnitActions();		// handle units
 	    MissileActions();		// handle missiles
@@ -865,6 +867,7 @@ global void GameMainLoop(void)
     //	Game over
     //
     NetworkQuit();
+    EndReplayLog();
     if( GameResult==GameDefeat ) {
 	fprintf(stderr,"You have lost!\n");
 	SetStatusLine("You have lost!");
diff --git a/src/stratagus/stratagus.cpp b/src/stratagus/stratagus.cpp
index bea096d5c..b66983857 100644
--- a/src/stratagus/stratagus.cpp
+++ b/src/stratagus/stratagus.cpp
@@ -1339,7 +1339,7 @@ global int main(int argc,char** argv)
 		AiCostFactor=atoi(optarg);
 		continue;
 	    case 'l':
-		CommandLogEnabled=1;
+		CommandLogDisabled=1;
 		continue;
 	    case 'P':
 		NetworkPort=atoi(optarg);
diff --git a/src/ui/menus.cpp b/src/ui/menus.cpp
index 0390f47c1..4a0e3f20f 100644
--- a/src/ui/menus.cpp
+++ b/src/ui/menus.cpp
@@ -62,6 +62,7 @@
 #include "sound.h"
 #include "ccl.h"
 #include "editor.h"
+#include "commands.h"
 
 #if defined(USE_SDLCD) || defined(USE_SDLA)
 #include "SDL.h"
@@ -279,6 +280,18 @@ local void EditorSaveConfirmOk(void);
 local void EditorSaveConfirmCancel(void);
 local void EditorQuitMenu(void);
 
+local void ReplayGameMenu(void);
+local void ReplayGameInit(Menuitem *mi);
+local void ReplayGameLBInit(Menuitem *mi);
+local void ReplayGameLBExit(Menuitem *mi);
+local int ReplayGameRDFilter(char *pathbuf, FileList *fl);
+local void ReplayGameLBAction(Menuitem *mi, int i);
+local unsigned char *ReplayGameLBRetrieve(Menuitem *mi, int i);
+local void ReplayGameVSAction(Menuitem *mi, int i);
+local void ReplayGameFolder(void);
+local void ReplayGameOk(void);
+local void ReplayGameCancel(void);
+
 /*----------------------------------------------------------------------------
 --	Variables
 ----------------------------------------------------------------------------*/
@@ -580,6 +593,18 @@ global void InitMenuFuncHash(void) {
     HASHADD(EditorSaveConfirmInit,"editor-save-confirm-init");
     HASHADD(EditorSaveConfirmOk,"editor-save-confirm-ok");
     HASHADD(EditorSaveConfirmCancel,"editor-save-confirm-cancel");
+
+// Replay game
+    HASHADD(ReplayGameMenu,"replay-game-menu");
+    HASHADD(ReplayGameInit,"replay-game-init");
+    HASHADD(ReplayGameLBInit,"replay-game-lb-init");
+    HASHADD(ReplayGameLBExit,"replay-game-lb-exit");
+    HASHADD(ReplayGameLBAction,"replay-game-lb-action");
+    HASHADD(ReplayGameLBRetrieve,"replay-game-lb-retrieve");
+    HASHADD(ReplayGameVSAction,"replay-game-vs-action");
+    HASHADD(ReplayGameFolder,"replay-game-folder");
+    HASHADD(ReplayGameOk,"replay-game-ok");
+    HASHADD(ReplayGameCancel,"replay-game-cancel");
 }
 
 /*----------------------------------------------------------------------------
@@ -905,7 +930,8 @@ local void SaveVSAction(Menuitem *mi, int i)
 		}
 
 		DebugCheck(mi->d.listbox.startline < 0);
-		DebugCheck(mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
+		DebugCheck(mi->d.listbox.noptions > 0 &&
+		    mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
 
 		SaveLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
 		MustRedraw |= RedrawMenu;
@@ -1178,7 +1204,8 @@ local void LoadVSAction(Menuitem *mi, int i)
 		}
 
 		DebugCheck(mi->d.listbox.startline < 0);
-		DebugCheck(mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
+		DebugCheck(mi->d.listbox.noptions > 0 &&
+		    mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
 
 		LoadLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
 		MustRedraw |= RedrawMenu;
@@ -2842,7 +2869,8 @@ local void ScenSelectVSAction(Menuitem *mi, int i)
 		}
 
 		DebugCheck(mi->d.listbox.startline < 0);
-		DebugCheck(mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
+		DebugCheck(mi->d.listbox.noptions > 0 &&
+		    mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
 
 		ScenSelectLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
 		MustRedraw |= RedrawMenu;
@@ -4668,7 +4696,8 @@ local void EditorMainLoadVSAction(Menuitem *mi, int i)
 		}
 
 		DebugCheck(mi->d.listbox.startline < 0);
-		DebugCheck(mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
+		DebugCheck(mi->d.listbox.noptions > 0 &&
+		    mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
 
 		EditorMainLoadLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
 		MustRedraw |= RedrawMenu;
@@ -5344,7 +5373,8 @@ local void EditorSaveVSAction(Menuitem *mi, int i)
 		}
 
 		DebugCheck(mi->d.listbox.startline < 0);
-		DebugCheck(mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
+		DebugCheck(mi->d.listbox.noptions > 0 &&
+		    mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
 
 		EditorSaveLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
 		MustRedraw |= RedrawMenu;
@@ -5416,6 +5446,413 @@ local void EditorEndMenu(void)
     MustRedraw = RedrawMenu;
 }
 
+/**
+**	Replay game menu
+*/
+local void ReplayGameMenu(void)
+{
+    char buf[PATH_MAX];
+
+#ifdef USE_WIN32
+    strcpy(buf,"logs");
+    mkdir(buf);
+
+    sprintf(ScenSelectPath, "logs/");
+#else
+    sprintf(buf,"%s/%s",getenv("HOME"),FREECRAFT_HOME_PATH);
+    mkdir(buf,0777);
+    strcat(buf,"/logs");
+    mkdir(buf,0777);
+
+    sprintf(ScenSelectPath,"%s/%s/logs/", getenv("HOME"), FREECRAFT_HOME_PATH);
+#endif
+    *ScenSelectDisplayPath = '\0';
+
+    VideoLockScreen();
+    VideoClearScreen();
+    MenusSetBackground();
+    VideoUnlockScreen();
+    Invalidate();
+
+    GuiGameStarted = 0;
+    ProcessMenu("menu-replay-game",1);
+    if (GuiGameStarted) {
+	GameMenuReturn();
+    }
+}
+
+/**
+**	Replay game menu init callback
+*/
+local void ReplayGameInit(Menuitem *mi)
+{
+    DebugCheck(!*ScenSelectPath);
+    mi->menu->items[5].flags =
+	*ScenSelectDisplayPath ? 0 : MenuButtonDisabled;
+    mi->menu->items[5].d.button.text = ScenSelectDisplayPath;
+    DebugLevel0Fn("Start path: %s\n" _C_ ScenSelectPath);
+}
+
+/**
+**	Replay game listbox init callback
+*/
+local void ReplayGameLBInit(Menuitem *mi)
+{
+    int i;
+
+    ReplayGameLBExit(mi);
+    i = mi->d.listbox.noptions = ReadDataDirectory(ScenSelectPath, ReplayGameRDFilter,
+	(FileList **)&(mi->d.listbox.options));
+
+    if (i == 0) {
+	mi->menu->items[3].d.button.text = "OK";
+	mi->menu->items[3].flags |= MenuButtonDisabled;
+    } else {
+	ReplayGameLBAction(mi, 0);
+	mi->menu->items[3].flags &= ~MenuButtonDisabled;
+	if (i > 5) {
+	    mi[1].flags &= ~MenuButtonDisabled;
+	}
+    }
+}
+
+/**
+**	Replay game listbox exit callback
+*/
+local void ReplayGameLBExit(Menuitem *mi)
+{
+    FileList *fl;
+
+    if (mi->d.listbox.noptions) {
+	fl = mi->d.listbox.options;
+	free(fl);
+	mi->d.listbox.options = NULL;
+	mi->d.listbox.noptions = 0;
+	mi[1].flags |= MenuButtonDisabled;
+    }
+}
+
+/**
+**	Replay game read directory filter
+*/
+local int ReplayGameRDFilter(char *pathbuf, FileList *fl)
+{
+    char *suf;
+    char *np;
+    char *cp;
+    char *lcp;
+#ifdef USE_ZZIPLIB
+    int sz;
+    ZZIP_FILE *zzf;
+#endif
+
+    suf = ".log";
+    np = strrchr(pathbuf, '/');
+    if (np) {
+	np++;
+    } else {
+	np = pathbuf;
+    }
+    cp = np;
+    cp--;
+    fl->type = -1;
+#ifdef USE_ZZIPLIB
+    if ((zzf = zzip_open(pathbuf, O_RDONLY|O_BINARY))) {
+	sz = zzip_file_real(zzf);
+	zzip_close(zzf);
+	if (!sz) {
+	    goto usezzf;
+	}
+    }
+#endif
+    do {
+	lcp = cp++;
+	cp = strcasestr(cp, suf);
+    } while (cp != NULL);
+    if (lcp >= np) {
+	cp = lcp + strlen(suf);
+#ifdef USE_ZLIB
+	if (strcmp(cp, ".gz") == 0) {
+	    *cp = 0;
+	}
+#endif
+#ifdef USE_BZ2LIB
+	if (strcmp(cp, ".bz2") == 0) {
+	    *cp = 0;
+	}
+#endif
+	if (*cp == 0) {
+#ifdef USE_ZZIPLIB
+usezzf:
+#endif
+	    if (strcasestr(pathbuf, ".log")) {
+		fl->type = 1;
+		fl->name = strdup(np);
+		return 1;
+	    }
+	}
+    }
+    return 0;
+}
+
+/**
+**	Replay game listbox action
+*/
+local void ReplayGameLBAction(Menuitem *mi, int i)
+{
+    FileList *fl;
+
+    DebugCheck(i<0);
+    if (i < mi->d.listbox.noptions) {
+	fl = mi->d.listbox.options;
+	if (fl[i].type) {
+	    mi->menu->items[3].d.button.text = "OK";
+	} else {
+	    mi->menu->items[3].d.button.text = "Open";
+	}
+	if (mi->d.listbox.noptions > 5) {
+	    mi[1].d.vslider.percent = (i * 100) / (mi->d.listbox.noptions - 1);
+	    mi[1].d.hslider.percent = (i * 100) / (mi->d.listbox.noptions - 1);
+	}
+    }
+}
+
+/**
+**	Replay game listbox retrieve
+*/
+local unsigned char *ReplayGameLBRetrieve(Menuitem *mi, int i)
+{
+    FileList *fl;
+    static char buffer[1024];
+
+    if (i < mi->d.listbox.noptions) {
+	fl = mi->d.listbox.options;
+	if (fl[i].type) {
+	    strcpy(buffer, "   ");
+	} else {
+	    strcpy(buffer, "\260 ");
+	}
+	strcat(buffer, fl[i].name);
+	return buffer;
+    }
+    return NULL;
+}
+
+/**
+**	Replay game vertical scroll action
+*/
+local void ReplayGameVSAction(Menuitem *mi, int i)
+{
+    int op;
+    int d1;
+    int d2;
+
+    mi--;
+    switch (i) {
+	case 0:		// click - down
+	case 2:		// key - down
+	    if (mi[1].d.vslider.cflags&MI_CFLAGS_DOWN) {
+		if (mi->d.listbox.curopt+mi->d.listbox.startline+1 < mi->d.listbox.noptions) {
+		    mi->d.listbox.curopt++;
+		    if (mi->d.listbox.curopt >= mi->d.listbox.nlines) {
+			mi->d.listbox.curopt--;
+			mi->d.listbox.startline++;
+		    }
+		    MustRedraw |= RedrawMenu;
+		}
+	    } else if (mi[1].d.vslider.cflags&MI_CFLAGS_UP) {
+		if (mi->d.listbox.curopt+mi->d.listbox.startline > 0) {
+		    mi->d.listbox.curopt--;
+		    if (mi->d.listbox.curopt < 0) {
+			mi->d.listbox.curopt++;
+			mi->d.listbox.startline--;
+		    }
+		    MustRedraw |= RedrawMenu;
+		}
+	    }
+	    ReplayGameLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
+	    if (i == 2) {
+		mi[1].d.vslider.cflags &= ~(MI_CFLAGS_DOWN|MI_CFLAGS_UP);
+	    }
+	    break;
+	case 1:		// mouse - move
+	    if (mi[1].d.vslider.cflags&MI_CFLAGS_KNOB && (mi[1].flags&MenuButtonClicked)) {
+		if (mi[1].d.vslider.curper > mi[1].d.vslider.percent) {
+		    if (mi->d.listbox.curopt+mi->d.listbox.startline+1 < mi->d.listbox.noptions) {
+			for (;;) {
+			    op = ((mi->d.listbox.curopt + mi->d.listbox.startline + 1) * 100) /
+				 (mi->d.listbox.noptions - 1);
+			    d1 = mi[1].d.vslider.curper - mi[1].d.vslider.percent;
+			    d2 = op - mi[1].d.vslider.curper;
+			    if (d2 >= d1)
+				break;
+			    mi->d.listbox.curopt++;
+			    if (mi->d.listbox.curopt >= mi->d.listbox.nlines) {
+				mi->d.listbox.curopt--;
+				mi->d.listbox.startline++;
+			    }
+			    if (mi->d.listbox.curopt+mi->d.listbox.startline+1 == mi->d.listbox.noptions)
+				break;
+			}
+		    }
+		} else if (mi[1].d.vslider.curper < mi[1].d.vslider.percent) {
+		    if (mi->d.listbox.curopt+mi->d.listbox.startline > 0) {
+			for (;;) {
+			    op = ((mi->d.listbox.curopt + mi->d.listbox.startline - 1) * 100) /
+				     (mi->d.listbox.noptions - 1);
+			    d1 = mi[1].d.vslider.percent - mi[1].d.vslider.curper;
+			    d2 = mi[1].d.vslider.curper - op;
+			    if (d2 >= d1)
+				break;
+			    mi->d.listbox.curopt--;
+			    if (mi->d.listbox.curopt < 0) {
+				mi->d.listbox.curopt++;
+				mi->d.listbox.startline--;
+			    }
+			    if (mi->d.listbox.curopt+mi->d.listbox.startline == 0)
+				break;
+			}
+		    }
+		}
+
+		DebugCheck(mi->d.listbox.startline < 0);
+		DebugCheck(mi->d.listbox.noptions > 0 &&
+		    mi->d.listbox.startline+mi->d.listbox.curopt >= mi->d.listbox.noptions);
+
+		ReplayGameLBAction(mi, mi->d.listbox.curopt + mi->d.listbox.startline);
+		MustRedraw |= RedrawMenu;
+	    }
+	    break;
+	default:
+	    break;
+    }
+}
+
+/**
+**	Replay game folder button callback
+*/
+local void ReplayGameFolder(void)
+{
+    Menu *menu;
+    Menuitem *mi;
+    char *cp;
+
+    menu = FindMenu("menu-replay-game");
+    mi = &menu->items[1];
+
+    if (ScenSelectDisplayPath[0]) {
+	cp = strrchr(ScenSelectDisplayPath, '/');
+	if (cp) {
+	    *cp = 0;
+	} else {
+	    ScenSelectDisplayPath[0] = 0;
+	    menu->items[5].flags |= MenuButtonDisabled;
+	    menu->items[5].d.button.text = NULL;
+	}
+	cp = strrchr(ScenSelectPath, '/');
+	if (cp) {
+	    *cp = 0;
+	    ReplayGameLBInit(mi);
+	    mi->d.listbox.cursel = -1;
+	    mi->d.listbox.startline = 0;
+	    mi->d.listbox.curopt = 0;
+	    mi[1].d.vslider.percent = 0;
+	    mi[1].d.hslider.percent = 0;
+	    MustRedraw |= RedrawMenu;
+	}
+    }
+}
+
+/**
+**	Replay game ok button callback
+*/
+local void ReplayGameOk(void)
+{
+    Menu *menu;
+    Menuitem *mi;
+    FileList *fl;
+    int i;
+
+    menu = FindMenu("menu-replay-game");
+    mi = &menu->items[1];
+    i = mi->d.listbox.curopt + mi->d.listbox.startline;
+    if (i < mi->d.listbox.noptions) {
+	fl = mi->d.listbox.options;
+	if (fl[i].type == 0) {
+	    strcat(ScenSelectPath, "/");
+	    strcat(ScenSelectPath, fl[i].name);
+	    if (menu->items[5].flags&MenuButtonDisabled) {
+		menu->items[5].flags &= ~MenuButtonDisabled;
+		menu->items[5].d.button.text = ScenSelectDisplayPath;
+	    } else {
+		strcat(ScenSelectDisplayPath, "/");
+	    }
+	    strcat(ScenSelectDisplayPath, fl[i].name);
+	    ReplayGameLBInit(mi);
+	    mi->d.listbox.cursel = -1;
+	    mi->d.listbox.startline = 0;
+	    mi->d.listbox.curopt = 0;
+	    mi[1].d.vslider.percent = 0;
+	    mi[1].d.hslider.percent = 0;
+	    MustRedraw |= RedrawMenu;
+	} else {
+	    strcpy(ScenSelectFileName, fl[i].name);	// Final map name
+
+	    if (ScenSelectPath[0]) {
+		strcat(ScenSelectPath, "/");
+		strcat(ScenSelectPath, ScenSelectFileName);
+	    } else {
+		strcpy(ScenSelectPath, ScenSelectFileName);
+	    }
+
+	    LoadReplay(ScenSelectPath);
+
+	    for (i = 0; i < MAX_OBJECTIVES; i++) {
+		if (GameIntro.Objectives[i]) {
+		    free(GameIntro.Objectives[i]);
+		    GameIntro.Objectives[i] = NULL;
+		}
+	    }
+	    GameIntro.Objectives[0] = strdup(DefaultObjective);
+
+	    GuiGameStarted = 1;
+	    EndMenu();
+	}
+    }
+}
+
+/**
+**	Replay game cancel button callback
+*/
+local void ReplayGameCancel(void)
+{
+    char* s;
+
+    //
+    //  Use last selected map.
+    //
+    DebugLevel0Fn("Map   path: %s\n" _C_ CurrentMapPath);
+    strcpy(ScenSelectPath, FreeCraftLibPath);
+    if (*ScenSelectPath) {
+	strcat(ScenSelectPath, "/");
+    }
+    strcat(ScenSelectPath, CurrentMapPath);
+    if ((s = strrchr(ScenSelectPath, '/'))) {
+	strcpy(ScenSelectFileName, s + 1);
+	*s = '\0';
+    }
+    strcpy(ScenSelectDisplayPath, CurrentMapPath);
+    if ((s = strrchr(ScenSelectDisplayPath, '/'))) {
+	*s = '\0';
+    } else {
+	*ScenSelectDisplayPath = '\0';
+    }
+
+    DebugLevel0Fn("Start path: %s\n" _C_ ScenSelectPath);
+
+    EndMenu();
+}
+
 /*----------------------------------------------------------------------------
 --	Init functions
 ----------------------------------------------------------------------------*/