Maelstrom: Added first pass at a replay system (it works!) :)

https://github.com/libsdl-org/Maelstrom/commit/8564da98ce28fa3a36e5d511e9888f83f7b017ee

From 8564da98ce28fa3a36e5d511e9888f83f7b017ee Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 20 Nov 2011 03:06:54 -0500
Subject: [PATCH] Added first pass at a replay system (it works!) :)

---
 game/Maelstrom_Globals.h |   2 +
 game/Makefile.am         |   2 +
 game/Makefile.in         |   5 +-
 game/game.cpp            |  12 +++-
 game/gameinfo.cpp        |  17 ++++++
 game/gameinfo.h          |   2 +
 game/packet.h            |  16 ++++--
 game/replay.cpp          | 115 +++++++++++++++++++++++++++++++++++++++
 game/replay.h            |  71 ++++++++++++++++++++++++
 9 files changed, 234 insertions(+), 8 deletions(-)
 create mode 100644 game/replay.cpp
 create mode 100644 game/replay.h

diff --git a/game/Maelstrom_Globals.h b/game/Maelstrom_Globals.h
index a44d2195..8bfd4fcb 100644
--- a/game/Maelstrom_Globals.h
+++ b/game/Maelstrom_Globals.h
@@ -38,6 +38,7 @@
 
 #include "myerror.h"
 #include "fastrand.h"
+#include "replay.h"
 #include "scores.h"
 #include "controls.h"
 #include "gameinfo.h"
@@ -135,6 +136,7 @@ extern int	gWave;
 class Object;
 extern Object  *gEnemySprite;
 extern int	gWhenEnemy;
+extern Replay   gReplay;
 
 // in controls.cpp :
 extern Controls	controls;
diff --git a/game/Makefile.am b/game/Makefile.am
index 1b8bd0e4..89a513a7 100644
--- a/game/Makefile.am
+++ b/game/Makefile.am
@@ -43,6 +43,8 @@ libgame_a_SOURCES =		\
 	protocol.h		\
 	rect.cpp		\
 	rect.h			\
+	replay.cpp		\
+	replay.h		\
 	scores.cpp		\
 	scores.h		\
 	shinobi.h
diff --git a/game/Makefile.in b/game/Makefile.in
index 09650f80..6d03b94c 100644
--- a/game/Makefile.in
+++ b/game/Makefile.in
@@ -53,7 +53,7 @@ am_libgame_a_OBJECTS = MacDialog.$(OBJEXT) MaelstromUI.$(OBJEXT) \
 	load.$(OBJEXT) lobby.$(OBJEXT) main.$(OBJEXT) make.$(OBJEXT) \
 	myerror.$(OBJEXT) netplay.$(OBJEXT) object.$(OBJEXT) \
 	objects.$(OBJEXT) player.$(OBJEXT) rect.$(OBJEXT) \
-	scores.$(OBJEXT)
+	replay.$(OBJEXT) scores.$(OBJEXT)
 libgame_a_OBJECTS = $(am_libgame_a_OBJECTS)
 DEFAULT_INCLUDES = -I.@am__isrc@
 depcomp = $(SHELL) $(top_srcdir)/build-scripts/depcomp
@@ -220,6 +220,8 @@ libgame_a_SOURCES = \
 	protocol.h		\
 	rect.cpp		\
 	rect.h			\
+	replay.cpp		\
+	replay.h		\
 	scores.cpp		\
 	scores.h		\
 	shinobi.h
@@ -289,6 +291,7 @@ distclean-compile:
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/objects.Po@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/player.Po@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rect.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/replay.Po@am__quote@
 @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/scores.Po@am__quote@
 
 .cpp.o:
diff --git a/game/game.cpp b/game/game.cpp
index 6c1a6b50..7518e1f4 100644
--- a/game/game.cpp
+++ b/game/game.cpp
@@ -31,6 +31,7 @@
 
 // Global variables set in this file...
 GameInfo gGameInfo;
+Replay  gReplay;
 int	gScore;
 int	gGameOn;
 int	gPaused;
@@ -88,6 +89,9 @@ static bool SetupPlayers(void)
 
 void NewGame(void)
 {
+	/* Start the replay */
+	gReplay.HandleNewGame();
+
 	/* Start up the random number generator */
 	SeedRandom(gGameInfo.seed);
 
@@ -238,11 +242,15 @@ GamePanelDelegate::OnTick()
 	/* -- Read in keyboard input for our ship */
 	HandleEvents(0);
 
-	if ( m_showingBonus ) {
+	if ( m_showingBonus || !gGameOn ) {
 		return;
 	}
 
 	/* -- Send Sync! signal to all players, and handle keyboard. */
+	if (!gReplay.HandlePlayback()) {
+		gGameOn = 0;
+		return;
+	}
 	if ( SyncNetwork() < 0 ) {
 		if ( gPaused & ~0x1 ) {
 			/* One of the other players is minimized and may not
@@ -254,6 +262,8 @@ GamePanelDelegate::OnTick()
 		gGameOn = 0;
 		return;
 	}
+	gReplay.HandleRecording();
+
 	OBJ_LOOP(i, MAX_PLAYERS) {
 		if (!gPlayers[i]->IsValid()) {
 			continue;
diff --git a/game/gameinfo.cpp b/game/gameinfo.cpp
index 2059b22a..3d9d6905 100644
--- a/game/gameinfo.cpp
+++ b/game/gameinfo.cpp
@@ -254,6 +254,23 @@ GameInfo::WriteToPacket(DynamicPacket &packet)
 	}
 }
 
+void
+GameInfo::PrepareForReplay()
+{
+	// Clean up the game info for privacy and so replay works
+	gameID = localID;
+
+	SDL_zero(nodes);
+	numNodes = 0;
+
+	for (int i = 0; i < MAX_PLAYERS; ++i) {
+		if (IsValidPlayer(i)) {
+			players[i].nodeID = localID;
+			players[i].controlMask = CONTROL_REPLAY;
+		}
+	}
+}
+
 bool
 GameInfo::HasNode(Uint32 nodeID) const
 {
diff --git a/game/gameinfo.h b/game/gameinfo.h
index 51d08cd9..42f04c88 100644
--- a/game/gameinfo.h
+++ b/game/gameinfo.h
@@ -115,6 +115,8 @@ class GameInfo
 	bool ReadFromPacket(DynamicPacket &packet);
 	void WriteToPacket(DynamicPacket &packet);
 
+	void PrepareForReplay();
+
 	int GetNumNodes() const {
 		return numNodes;
 	}
diff --git a/game/packet.h b/game/packet.h
index 1c27e08b..c358aa59 100644
--- a/game/packet.h
+++ b/game/packet.h
@@ -69,6 +69,9 @@ class DynamicPacket : public UDPpacket
 	int Tell() {
 		return pos;
 	}
+	int Size() {
+		return len;
+	}
 
 	void Write(Uint8 value) {
 		Grow(sizeof(value));
@@ -94,16 +97,17 @@ class DynamicPacket : public UDPpacket
 			amount = 255;
 		}
 		Write((Uint8)amount);
-		Grow(amount);
-		SDL_memcpy(&data[pos], value, amount);
-		pos += amount;
+		Write(value, amount);
 	}
 	void Write(DynamicPacket &packet) {
 		size_t amount = packet.len - packet.pos;
-		Grow(amount);
-		SDL_memcpy(&data[pos], &packet.data[packet.pos], amount);
+		Write(&packet.data[packet.pos], amount);
 		packet.pos += amount;
-		pos += amount;
+	}
+	void Write(const void *value, size_t size) {
+		Grow(size);
+		SDL_memcpy(&data[pos], value, size);
+		pos += size;
 	}
 
 	bool Read(Uint8 &value) {
diff --git a/game/replay.cpp b/game/replay.cpp
new file mode 100644
index 00000000..855de2fd
--- /dev/null
+++ b/game/replay.cpp
@@ -0,0 +1,115 @@
+/*
+    Maelstrom: Open Source version of the classic game by Ambrosia Software
+    Copyright (C) 1997-2011  Sam Lantinga
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+    Sam Lantinga
+    slouken@libsdl.org
+*/
+
+#include "Maelstrom_Globals.h"
+#include "netplay.h"
+#include "replay.h"
+
+
+Replay::Replay()
+{
+	m_mode = REPLAY_IDLE;
+}
+
+void
+Replay::SetMode(REPLAY_MODE mode)
+{
+	m_mode = mode;
+}
+
+bool
+Replay::Load(const char *file)
+{
+	assert(!"Not yet implemented");
+	return false;
+}
+
+bool
+Replay::Save(const char *file)
+{
+	assert(!"Not yet implemented");
+	return false;
+}
+
+void
+Replay::HandleNewGame()
+{
+	if (m_mode == REPLAY_RECORDING) {
+		m_game.CopyFrom(gGameInfo);
+		m_game.PrepareForReplay();
+	} else if (m_mode == REPLAY_PLAYBACK) {
+		m_game.PrepareForReplay();
+		gGameInfo.localID = m_game.localID;
+		gGameInfo.CopyFrom(m_game);
+	}
+	m_data.Seek(0);
+}
+
+bool
+Replay::HandlePlayback()
+{
+	if (m_mode != REPLAY_PLAYBACK) {
+		return true;
+	}
+
+	// Check to make sure we haven't gotten a consistency error
+	Uint32 seed;
+	if (!m_data.Read(seed)) {
+		// That's it, end of recording
+		return false;
+	}
+	if (seed != GetRandSeed()) {
+		error("Error!! \a Frame consistency problem, aborting!!\r\n");
+		return false;
+	}
+
+	// Add the input for this frame
+	Uint16 size;
+	Uint8 value;
+	if (!m_data.Read(size) || (m_data.Size() < (int)size)) {
+		error("Error in replay, missing data\r\n");
+		return false;
+	}
+	while (size--) {
+		m_data.Read(value);
+		QueueInput(value);
+	}
+	return true;
+}
+
+void
+Replay::HandleRecording()
+{
+	if (m_mode != REPLAY_RECORDING) {
+		return;
+	}
+
+	// Get the input for this frame
+	Uint8 *data;
+	int len = GetSyncBuf(&data);
+
+	// Add it to our data buffer
+	m_data.Write(GetRandSeed());
+	assert(len <= 0xFFFF);
+	m_data.Write((Uint16)len);
+	m_data.Write(data, len);
+}
diff --git a/game/replay.h b/game/replay.h
new file mode 100644
index 00000000..eb948e6d
--- /dev/null
+++ b/game/replay.h
@@ -0,0 +1,71 @@
+/*
+    Maelstrom: Open Source version of the classic game by Ambrosia Software
+    Copyright (C) 1997-2011  Sam Lantinga
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+    Sam Lantinga
+    slouken@libsdl.org
+*/
+
+#ifndef _replay_h
+#define _replay_h
+
+#include "gameinfo.h"
+#include "packet.h"
+
+// You should increment this every time game code changes in ways that would
+// affect the random number sequence for a game.
+//
+// Examples of this would be changing the game play area, game info structure,
+// game logic, etc.
+//
+#define REPLAY_VERSION	1
+
+enum REPLAY_MODE {
+	REPLAY_IDLE,
+	REPLAY_RECORDING,
+	REPLAY_PLAYBACK
+};
+
+class Replay
+{
+public:
+	Replay();
+
+	// You should never change this while the game is running
+	void SetMode(REPLAY_MODE mode);
+
+	bool IsPlaying() {
+		return m_mode == REPLAY_PLAYBACK;
+	}
+	bool IsRecording() {
+		return m_mode == REPLAY_RECORDING;
+	}
+
+	bool Load(const char *file);
+	bool Save(const char *file);
+
+	void HandleNewGame();
+	bool HandlePlayback();
+	void HandleRecording();
+
+protected:
+	REPLAY_MODE m_mode;
+	GameInfo m_game;
+	DynamicPacket m_data;
+};
+
+#endif // _replay_h