////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 RWS Inc, All Rights Reserved
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of version 2 of the GNU General Public License as published by
// the Free Software Foundation
//
// 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.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
// play.cpp
// Project: Nostril (aka Postal)
//
// This module deals with the high-level aspects of setting up and running the
// game.
//
// History:
//		11/19/96 MJR	Started.
//
//		A huge number of changes occurred, and then the entire module was
//		reorganized, to the point where the previous history was no longer
//		relevant.  This history was was purged on 8/3/97 -- if you need to
//		refer back to it, simply go back before this date in SourceSafe.
//
//		08/03/97 MJR	A total reorganization occurs.
//
//		08/05/97	JMI	Changed uses of CRealm::m_bMultiplayer to
//							CRealm::m_flags.bMultiplayer.
//
//		08/06/97 MJR	Fixed bug when going to next level or restarting.
//
//		08/06/97	JMI	Now Play_VerifyQuitMenuChoice() plays the appropriate sound
//							as to whether there was a selection change or an item was
//							chosen.
//							Also, changed uses of InitLocalInput() to ClearLocalInput().
//
//		08/08/97 MJR	Moved background/foreground callbacks to game.cpp.
//
//		08/08/97 MJR	Fixed multiplayer go-to-next-level bug.
//							Got the abort message working properly.
//
//		08/08/97	JMI	CPlayRealm::EndRealm() now only updates players stockpiles
//							if we are not restarting the current level.  This way, in
//							single player mode, when we restart the level, you don't
//							get a combo of the ammo you had when you died and the warp
//							but rather a combo of the ammo you had when you entered the
//							level and the warp.
//
//		08/08/97	JMI	After a realm play when 'Just one realm' was specified,
//							the 'Game Over' flag would get set regardless of whether
//							the player had chosen to restart the realm.  Fixed.
//
//		08/09/97	JMI	CoreLoopRender() and CoreLoopUserInput() were checking
//							m_bCheckForAbortKey without first checking if we're in
//							network mode.  This flag is not used in non-net mode so we
//							must check before using it.
//
//		08/09/97	JRD	Changed play to call the new toolbar render, and modified the
//							score render to include the background bitmap.
//
//		08/11/97	JMI	Changed two occurrences of sRealNum to info->m_sRealNum.
//							sRealmNum is the passed in start realm and info->m_sRealNum
//							is the current realm.
//
//		08/11/97 MJR	Fixed a bug where time wasn't being updated properly (and
//							thereby was at least one reason for sync problems.)
//
//		08/12/97	JMI	Now that cheats require an input event, we only pass it to
//							GetLocalInput() in singel player mode.  Since the two ways
//							of getting input for are so different, it makes it difficult
//							to hack cheats into multiplayer mode.
//
//		08/13/97 MJR	Cleaned up use of info flags to try to simplify and
//							make sure no race conditions exist.
//
//							Fixed bug when trying to resume paused game (wasn't
//							filtering out modifier keys -- now it does).
//
//		08/13/97	JMI	Fixed positioning macros so they are nearly constant (i.e,
//							changes in the g_pimScreenBuf could cause it to be non-
//							constant).
//							Fixed portions of the code that updated the realm status
//							using the INFO_STATUS_* macros.
//							Moved the initial drawing of the toolbar into
//							CPlayStatus::StartRealm().
//							Now utilizes the return value from ToolbarRender() to de-
//							termine whether to update that area of the display.
//
//		08/14/97	JMI	Took 'again' out of "Hit <pause> key again to resume"
//							paused message.
//							Also, RespondToMenuRequest() now clears all events before
//							starting menu.
//							Made XRay All key a toggle.
//							Changed name of difficulty parameter to Play() from
//							bDifficulty to sDifficulty.
//							Now uses sDifficulty paramter to Play().
//							Added sDifficulty paramter to
//							Play_GetRealmSectionAndEntry().
//							Converted ms_bQuitVerified to ms_menuaction and added two
//							actions:  MenuActionQuit and MenuActionSaveGame.
//							Now passes difficulty to Game_SavePlayersGame() which is
//							now called from RespondToMenuRequest().
//
//		08/14/97	JMI	Converted Play_VerifyQuitMenuChoice() to returning true to
//							accept or false to deny.
//
//		08/17/97	JMI	Now disables postal organ option from within the game.
//
//		08/17/97 MJR	Now loads abort gui from g_resmgrShell.
//
//		08/18/97	JMI	Was still clearing KEY_RESTART as a left over from when we
//							would use KEY_RESTART to flag restarting a level in single
//							player (nowadays uses INPUT_REVIVE).
//							Also, was able to get rid of INPUT_JUMP which was left over
//							from when we converted to INPUT_REVIVE but play.cpp was
//							under different construction.
//
//		08/18/97	JMI	Now turns on XRay all when the local dude dies.
//
//		08/18/97	JMI	Added variable that, when true, allows advancing to the next
//							level without meeting the level goal.
//							Also, now in multiplayer mode, the server can advance the
//							level without meeting the level goal.
//
//		08/19/97 MJR	Added supoprt for new MP parameters.
//
//		08/20/97	JMI	Now responds to INPUT_CHEAT_29 by advancing the level if
//							NOT a sales demo.
//
//		08/20/97 BRH	In the Play function, I used the flags passed in to
//							determine and set the scoring mode in the realm.
//
//		08/21/97	JMI	Now keeps the global savable stockpile up to date.
//
//		08/21/97	JMI	Changed call to Update() to UpdateSystem() and occurrences
//							of rspUpdateDisplay() to UpdateDisplay().
//
//		08/22/97	JMI	Changed calls to UpdateDisplay() back to rspUpdateDisplay()
//							since we no longer need UpdateDisplay() now that we are
//							using rspLock/Unlock* functions properly.
//							Also, now locks the composite buffer before accessing it
//							and unlocks it before updating the screen.  This required
//							breaking CoreLoopRender() into CoreLoopRender() and
//							CoreLoopDraw().
//
//		08/23/97	JMI	Now 'Save' menu option is disabled in multiplayer mode.
//
//		08/24/97	JMI	Added a timeout to the abortion of playing samples just in
//							case there's a bug or a sound driver problem (no need to
//							to taunt infinite loopage).
//
//		08/24/97	JMI	Moved code to stop all samples into a function so we could
//							call it in two places.
//							Now used before starting the load b/c playing samples sound
//							too shitty during loads.
//
//		08/24/97	JMI	Check for INPUT_CHEAT_29 was incorrectly using
//							INPUT_CHEAT_29 as a mask instead of INPUT_WEAPONS_MASK so
//							other cheats that included all the same mask bits could
//							cause 29 to be activated (there was only one, of course,
//							INPUT_CHEAT_30).
//
//		08/25/97	JMI	Now uses toolbar initialized score font colors for debug
//							display info text.
//
//		08/26/97 BRH	Added special cases for the final ending demo level.
//							Now when it is determined that the player won, it sets
//							the global g_bLastLevelDemo so that the ending demo will
//							be shown after the final game level which is the air
//							force base.  Also made a few special cases so that the
//							Cutscene shown is the one loaded from the RealmEnd section
//							of the realms.ini file, and that the toolbars are not
//							shown during the final level demo.
//
//		08/26/97	JMI	Moved m_bXRayAll to CPlayInfo so it could be accessed from
//							anywhere.
//							Fixed problem where, when you come back to life in MP mode
//							or via cheat, the XRay would stay on even if the user
//							setting was off.
//
//		08/27/97	JMI	Changed PAUSED_FONT_HEIGHT to 48 (was 50).  Apparently, we
//							cannot use a size that is larger than the largest cached
//							font size.  So all font sizes for the Smash font must be
//							less than or equal to 48.
//
//		08/27/97 MJR	Updated to use new union name in NetMsg.
//							Now sets dude ID for all players in MP mode.
//							Now sends and receives special peer data.
//
//		08/28/97 MJR	Merged CPlayClient and CPlayServer into CPlayNet.
//
//		xx/xx/97 MJR	HUGE CHANGES to incorporate new network scheme.
//
//							==========================================================
//		09/05/97 MJR	MERGED ALL THE CHANGES FROM THE SEPARATE BRANCH OF PLAY.CPP
//							WHICH IS WHERE THE FOLLOWING CHANGES CAME FROM
//							==========================================================
//
//		08/30/97 BRH	Fixed paths for installer.  The levels were still trying
//							to load from the HD path but they should load from the CD
//							path.
//
//		08/30/97	JMI	If the player hits space to restart, we check if the goal
//							was met and, if so, show the high score dialogs.
//
//		09/02/97	JMI	Now Purges all resources from g_resmgrGame, Samples, and
//							Res on certain systems.
//
//		09/03/97	JMI	I realized that the last change would cause an
//							unnecessarily long load for restarting a realm so now it
//							only does the purging (on the MAC) if we're not restarting
//							the realm.
//
//		09/03/97	JMI	Changed the check for the end of the demo to use IsDead()
//							instead of State_Dead for determining whether the dude is
//							dead.  Also, now checks InputIsDemoOver().
//
//		09/03/97	JMI	Now checks to make sure we're in SP mode before pausing
//							while in the background.
//
//		09/04/97 BRH	Play no longer sets the full path to the realm file to
//							load.  It is done in Realm::Load instead so that we can
//							try several paths.  This way the realms can be loaded
//							from the HD path, or if not there, loaded from the CD
//							path.  Then if someone wants to insert their level, or
//							we want to provide an updated level, they can copy it
//							to the mirror path on their HD and it will attempt to
//							load that one first.
//
//							==========================================================
//							Finished merging separate branches of PLAY.CPP.
//							==========================================================
//
//		09/06/97 MJR	Fixed bug in SetupDudes() that caused crash in single
//							player mode.
//
//		09/06/97 MJR	Now allows menu to be used in MP mode.
//							Cleaned up how local user quits are handled in MP mode.
//							Properly uses abort gui thing.
//
//		09/07/97	JMI	Now displays the high scores at the end of each MP level.
//							Also, now defaults to 99 (instead of 10) kills when neither
//							a time or a kill limit is specified.
//
//		09/07/97 MJR	Fixed bug that prevented end-of-game sequence from working.
//							Now ignores keyboard input during end-of-game sequence.
//
//		09/08/97 MJR	Centered net prog gui thingy.
//
//		09/11/97	JMI	Added support for ENABLE_PLAY_SPECIFIC_REALMS_ONLY which
//							only allows you to play a realm whose name is jumbled in
//							ms_szSingleRealmPostFix[].
//
//		09/12/97 MJR	In MP game, if a realm can't be loaded, we either abort
//							the game if we're the server or we drop out of the game
//							if we're a client.
//
//							Also removed the ASSERT() from Cinfo->GameOver(), which
//							used to not get called in MP mode, but now does due to
//							our sudden use of "just one realm" mode in cases where
//							the server only has one realm available.
//
//		09/16/97 MJR	Removed the JUMBLE stuff, which was made obsolete when we
//							switched to embedding the realm files in the executable.
//
//		09/29/97	JMI	Now updates areas of the display that were blanked by
//							ScaleFilm() (called from CPlayRealm::CoreLoopRender() ) in
//							CPlayInfo::UpdateBlankedAreas() (called from
//							CPlayRealm::CoreLoopDraw() ).  Since, when ScaleFilm() is
//							called, we are inside a rspLock/UnlockBuffer() pair, we
//							cannot call rspUpdateDisplay() there.
//
//		10/30/97	JMI	Used to use a flag to indicate whether CInfo::m_rc* needed
//							to be updated.  Now we simply check whether m_rc*.sW & sH
//							are greater than 0 so we need to make sure they're
//							initialized to zero.  It didn't show up on the PC b/c Blue
//							does not allow negative widths/heights to be drawn but on
//							the Mac it seems to cause a rather bizarre mess.
//
//		11/19/97	JMI	The m_bDrawFrame flag was not being set to false when
//							bDoFrame (in CPlayRealm::CoreLoopRender() ) was false.  The
//							result was that while a net game was idle of input, the
//							display was still being updated.  Once this was changed and
//							m_bDrawFrame was moved into CPlayInfo (so all CPlayXxxx's
//							could utilize it), the idle looping increased in speed by
//							approximately 10 times on my machine.  The next logical
//							step would be to use this flag to reduce the number of
//							calls to ToolBarRender() and ScoreUpdateDisplay().  There's
//							a possible order problem with simply checking m_bDrawFrame
//							since it is set to false or true in
//							CPlayRealm::CoreLoopRender() and ToolBarRender() and
//							ScoreUpdateDisplay() are called in
//							CPlayStatus::CoreLoopRender().
//
//		11/20/97	JMI	Added net chat and dirty rects.  Now most things don't have to
//							bother implementing an CoreLoopRender() just for the sake of
//							updating an area they already processed.  Now, in
//							CoreLoopRender(), just do a pinfo->m_drl.Add(x, y, w, h) of the
//							area dirtied and it will be combined with everyone else's area
//							and updated to the screen (usually in one chunk if the film
//							size has not been altered).
//							More testing needs to be done, though.  Playing against all
//							P200s, the game ran fine.  But with a P120, it ran poorly.
//							We only tried once though...not sure there's really a
//							problem (also the P120 was the only machine with Win95...).
//
//		11/20/97	JMI	Added bCoopLevels & bCoopMode parameters to
//							Play_GetRealmInfo() and Play_GetRealmSectionAndEntry()
//							calls.
//							Also, added sCoopLevels & sCoopMode to Play() call.
//							Also, fixed a bug in Play_GetRealmInfo() where it would
//							write one byte off the end of the pszTitle parameter.
//
//		11/25/97	JMI	Changed the chats' .GUIs to be loaded from the HD
//							instead of from the VD so we can guarantee the new assets
//							get loaded (since they'll use their old Postal disc, we
//							cannot load the .GUIs from the CD).
//
//		06/04/98 BRH	Set the cutscene mode to simple mode if this is a spawn
//							build, since the spawn version only has 1 default cutscene
//							bitmap, it has to use this for all cutscenes.
//
//		10/07/99	JMI	Changed play loop to get the number of single player levels
//							from the INI.  Previously, it was 16.
//
////////////////////////////////////////////////////////////////////////////////
#define PLAY_CPP

#include "RSPiX.h"
#include "main.h"
#include "input.h"
#include "game.h"
#include "update.h"
#include "realm.h"
#include "camera.h"
#include "grip.h"
#include "thing.h"
#include "dude.h"
#include "hood.h"
#include "input.h"
#include "menus.h"
#include "SampleMaster.h"
#include "reality.h"
#include "cutscene.h"
#include "play.h"
#include "warp.h"
#include "scene.h"
#include "score.h"
#include "person.h"
#include "toolbar.h"
#include "title.h"
#include "credits.h"
//#define RSP_PROFILE_ON
//#include "ORANGE/Debug/profile.h"
#include "MenuTrans.h"
#include "pvr.h"
#include "aica.h"

////////////////////////////////////////////////////////////////////////////////
// Externs.
////////////////////////////////////////////////////////////////////////////////
extern PVR::cRenderMgr *g_pvrRenderer;
extern CAicaSoundPack *g_AicaSounds;

////////////////////////////////////////////////////////////////////////////////
// Macros/types/etc.
////////////////////////////////////////////////////////////////////////////////

#define DEMO_FRAMES_PER_SECOND 15
#define DEMO_TIME_PER_FRAME (1000 / DEMO_FRAMES_PER_SECOND)
#define DEMO_MAX_SEQUENTIAL_SKIPPED_FRAMES 1
#define DEMO_MAX_LAG (DEMO_TIME_PER_FRAME / 2)
#define DEMO_MAX_DEAD_TIME 5000

#define DEMO_MULTIALPHA_FILE "2d/school.mlp"

#define DISP_INFO_INTERVAL 1000 // NEVER EVER MAKE THIS LESS THAN 1!!!!
#define DISP_INFO_FONT_HEIGHT 15

// Scaling values
#define FILM_INCDEC_SCALE 0.05
#define FILM_MAX_SCALE 1.00
#define FILM_MIN_SCALE 0.30

#define INFO_STATUS_RECT_X ((g_GameSettings.m_sViewW - 640) / 2)
#define INFO_STATUS_RECT_Y (g_GameSettings.m_sFilmY - (INFO_STATUS_RECT_H + 3))
#define INFO_STATUS_RECT_W (640 - INFO_STATUS_RECT_X)
#define INFO_STATUS_RECT_H DISP_INFO_FONT_HEIGHT

#define DUDE_STATUS_RECT_X 0
#define DUDE_STATUS_RECT_Y (g_GameSettings.m_sFilmY + g_GameSettings.m_sViewH)
#define DUDE_STATUS_RECT_W (640 - DUDE_STATUS_RECT_X)
#define DUDE_STATUS_RECT_H (480 - DUDE_STATUS_RECT_Y)

#define REALM_STATUS_RECT_X 0
#define REALM_STATUS_RECT_Y 0
#define REALM_STATUS_RECT_W (g_GameSettings.m_sFilmX + g_GameSettings.m_sViewW - REALM_STATUS_RECT_X)
#define REALM_STATUS_RECT_H 40


// No less than this even after scaling.
#define MIN_GRIP_ZONE_RADIUS 30

// Grip movement parameters
#define GRIP_MIN_MOVE_X 1
#define GRIP_MIN_MOVE_Y 1
#define GRIP_MAX_MOVE_X 8
#define GRIP_MAX_MOVE_Y 8
#define GRIP_ALIGN_X 1
#define GRIP_ALIGN_Y 1

// Time for black screen between cutscene and game screen
#define BLACK_HOLD_TIME 250

// Default message in case app's time stamp is not available.  MUST be 25 characters or less!!!
#define DEFAULT_APP_TIMESTAMP "No time stamp available"

#define DEBUG_STR " Debug"
#define RELEASE_STR " Release"
#define TRACENASSERT_STR " Trace & Assert"

// Number of kills limit if they specified no kills limit and no time limit.
#define KILLS_LIMIT_DEFAULT 0

// Default value for "final frame" in network mode (6.8 years at 10fps)
#define DEFAULT_FINAL_FRAME LONG_MAX

#if WITH_STEAMWORKS
extern bool EnableSteamCloud;
#define SAVEGAME_DIR (EnableSteamCloud ? "steamcloud" : "savegame")
#else
#define SAVEGAME_DIR ("savegame")
#endif

#define SAVEGAME_EXT "gme"

#define ABORT_GUI_FILE "menu/abort.gui"
#define CHAT_GUI "res/shell/chat.gui"
#define CHAT_IN_GUI "res/shell/chatin.gui"

#define PAUSED_FONT_HEIGHT 48
#define PAUSED_FONT_SHADOW_X 5					  // In pixels.
#define PAUSED_FONT_SHADOW_Y PAUSED_FONT_SHADOW_X // In pixels.
#define PAUSED_BASE_PAL_INDEX 64
#define PAUSED_FONT_SHADOW_COLOR_R 0
#define PAUSED_FONT_SHADOW_COLOR_G 0
#define PAUSED_FONT_SHADOW_COLOR_B 0
#define PAUSED_FONT_COLOR_R 0
#define PAUSED_FONT_COLOR_G 15
#define PAUSED_FONT_COLOR_B 255

#define PAUSED_MSG_FONT_HEIGHT 29
#define PAUSED_MSG_FONT_SHADOW_X 3						  // In pixels.
#define PAUSED_MSG_FONT_SHADOW_Y PAUSED_MSG_FONT_SHADOW_X // In pixels.
#define PAUSED_MSG_FONT_SHADOW_COLOR_R PAUSED_FONT_SHADOW_COLOR_R
#define PAUSED_MSG_FONT_SHADOW_COLOR_G PAUSED_FONT_SHADOW_COLOR_G
#define PAUSED_MSG_FONT_SHADOW_COLOR_B PAUSED_FONT_SHADOW_COLOR_B
#define PAUSED_MSG_FONT_COLOR_R PAUSED_FONT_COLOR_R
#define PAUSED_MSG_FONT_COLOR_G PAUSED_FONT_COLOR_G
#define PAUSED_MSG_FONT_COLOR_B PAUSED_FONT_COLOR_B

#define TIME_OUT_FOR_ABORT_SOUNDS 3000 // In ms.

#define MP_HIGH_SCORES_MAX_TIME 7000 // In ms.

#define NUM_CHATS 4
#define CHAT_DELAY 5000 // In ms.

#define CHAT_IN_LENGTH 46

// The number of levels in each category.
#define REALM_NUM 16
#define ADDON_NUM (REALM_NUM + 4)
#define JADDON_NUM (ADDON_NUM + 2)

#define NUM_ELEMENTS(a) (sizeof(a) / sizeof(a[0]))
#define FONT_HEIGHT 12 // "Best" for ComicB.

////////////////////////////////////////////////////////////////////////////////
// Types.
////////////////////////////////////////////////////////////////////////////////

// Game states
typedef enum
{
	// These are defined in a SPECIFIC ORDER!!!  We sometimes check for specific
	// values, but other times we check for less than or greater than a value!!!
	// Note that you can think of the values as a PROGRESSION of states.
	Game_Ok,		  // Base state, must be 0
	Game_RedoRealm,	  // Redo the current realm
	Game_NextRealm,	  // Go to the next realm
	Game_GameOver,	  // Game is over
	Game_GameAborted, // Game is over because user aborted it
} GameState;

// Menu actions
typedef enum
{
	MenuActionNone,
	MenuActionQuit,		// Quit game.
	MenuActionRestart, 
	MenuActionEndMenu	// End the menu
} MenuAction;

////////////////////////////////////////////////////////////////////////////////
// Variables/data
////////////////////////////////////////////////////////////////////////////////

// Quit flag used by menu callbacks
static MenuAction ms_menuaction = MenuActionNone;

// Number used in filename for snapshots
static int32_t ms_lCurPicture = 0;

#ifdef SALES_DEMO
// When true, one can advance to the next level without meeting the goal.
extern bool g_bEnableLevelAdvanceWithoutGoal = false;
#endif

extern SampleMaster::SoundInstance g_siFinalScene;		  // should be in game
extern SampleMaster::SoundInstance g_siFinalSceneCredits; // should be in game
SampleMaster::SoundInstance g_siFinalScene;				  // should be in game
SampleMaster::SoundInstance g_siFinalSceneCredits;		  // should be in game

bool g_bEndLevelKey = false;
bool g_bKillLocalDudes = false;

//#ifdef MOBILE
extern bool demoCompat; //Try to make demos not go out of sync
//#endif
////////////////////////////////////////////////////////////////////////////////
// Function prototypes
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
//
// Info needed by virtually everything in play
//
////////////////////////////////////////////////////////////////////////////////
class CPlayInfo
{
	friend int16_t Play(				 // Returns 0 if successfull, non-zero otherwise		
		INPUT_MODE inputMode,			 // In:  Input mode
		const int16_t sRealmNum,		 // In:  Realm number to start on or -1 to use specified realm file
		const char *pszRealmFile,		 // In:  Realm file to play (ignored if sRealmNum >= 0)
		const bool bJustOneRealm,		 // In:  Play just this one realm (ignored if sRealmNum < 0)
		const bool bGauntlet,			 // In:  Play challenge levels gauntlet - as selected on menu
		const int16_t bAddOn,			 // In:  Play new single player Add On levels
		const int8_t sDifficulty,		 // In:  Difficulty level
		const bool bRejuvenate,			 // In:  Whether to allow players to rejuvenate (MP only)
		const int16_t sTimeLimit,		 // In:  Time limit for MP games (0 or negative if none)
		const int16_t sKillLimit,		 // In:  Kill limit for MP games (0 or negative if none)
		const int16_t sCoopLevels,		 // In:  Zero for deathmatch levels, non-zero for cooperative levels.
		const int16_t sCoopMode,		 // In:  Zero for deathmatch mode, non-zero for cooperative mode.
		const int16_t sFrameTime,		 // In:  Milliseconds per frame (MP only)
		RFile *pfileDemoModeDebugMovie); // In:  File for loading/saving demo mode debug movie

	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------

	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:

	int16_t m_sRealmNum;			  // Realm number
	char m_szRealm[RSP_MAX_PATH + 1]; // Realm file
	bool m_bJustOneRealm;			  // Play just this one realm (ignored if sRealmNum < 0)

	CRealm *m_prealm;
	CCamera *m_pcamera;
	CGrip *m_pgrip;

	bool m_bGauntlet;	   // Play challenge levels gauntlet
	int16_t m_bAddOn;	   // Play new Add On levels
	bool m_bRejuvenate;	   // Whether to allow players to rejuvenate (MP only)
	int16_t m_sTimeLimit;  // Time limit for MP games (0 or negative if none)
	int16_t m_sKillLimit;  // Kill limit for MP games (0 or negative if none)
	int16_t m_sCoopLevels; // Zero for deathmatch levels, non-zero for cooperative levels.

	int16_t m_sFrameTime; // Milliseconds per frame (MP only)

	RFile *m_pfileDemoModeDebugMovie; // File for loading/saving demo mode debug movie

	GameState m_gamestate;

	bool m_bPurgeSaks; // Purge the SAKS if true

public:	
	U8 m_ucNumLocalDudes;
	U16 m_idLocalDudes[4];	 // Local dudes IDs
	U16 m_idGripTarget;	  // Grip target's ID
	bool m_bDoRealmFrame; // Whether to do a realm frame
	int32_t m_lSumUpdateDisplayTimes;
	bool m_bXRayAll; // X Ray all status.
	bool m_bInMenu;	 // Whether we're in the menu
	bool m_bInHighScoreTable;
	
	bool m_bUserQuitMP;	 // Whether local user wants to quit MP game
	bool m_bNextRealmMP; // Whether local user wants next level of MP game
	bool m_bBadRealmMP;	 // Whether MP realm was unable to load
	bool m_bChatting;	 // true, when typing in chat messages.
						 // false, otherwise.
	bool m_bDrawFrame;	 // true, if we need to draw a frame.

	//todo?
	U8 m_iMenuInTransition;
	bool m_bDFrawFrameToTexture = false;

	//RDirtyRects m_drl;	 // Any areas of the composite buffer that is
						 // altered should be added to this list so it can
						 // be updated on CoreLoopDraw().

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayInfo(void)
	{

		m_sRealmNum = 0;
		m_szRealm[0] = 0;
		m_bJustOneRealm = false;

		m_prealm = new CRealm;
		m_pcamera = new CCamera;
		m_pgrip = new CGrip;

		m_bGauntlet = false;
		m_bAddOn = 0;
		m_bRejuvenate = false;
		m_sTimeLimit = 0;
		m_sKillLimit = 0;
		m_sCoopLevels = 0;

		m_sFrameTime = 0;

		m_pfileDemoModeDebugMovie = 0;

		m_gamestate = Game_Ok;
		
		m_ucNumLocalDudes = 1;
		m_idLocalDudes[0] = CIdBank::IdNil;
		m_idLocalDudes[1] = CIdBank::IdNil;
		m_idLocalDudes[2] = CIdBank::IdNil;
		m_idLocalDudes[3] = CIdBank::IdNil;

		m_idGripTarget = CIdBank::IdNil;
		m_bDoRealmFrame = false;
		m_lSumUpdateDisplayTimes = 0;
		m_bXRayAll = false;	  // Always default to no XRay all.
		m_bPurgeSaks = false; // Assume no purging
		m_bInMenu = false;
		m_bInHighScoreTable = false;
		m_iMenuInTransition = 0;
		m_bUserQuitMP = false;
		m_bNextRealmMP = false;
		m_bBadRealmMP = false;

		m_bChatting = false;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	~CPlayInfo()
	{
		delete m_prealm;
		delete m_pcamera;
		delete m_pgrip;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Simple wrappers that allow "read-only" access to member variables
	////////////////////////////////////////////////////////////////////////////////	
	int16_t RealmNum(void) { return m_sRealmNum; }
	const char *RealmName(void) { return m_szRealm; }
	bool JustOneRealm(void) { return m_bJustOneRealm; }
	CRealm *Realm(void) { return m_prealm; }
	CCamera *Camera(void) { return m_pcamera; }
	CGrip *Grip(void) { return m_pgrip; }
	bool Gauntlet(void) { return m_bGauntlet; }
	int16_t AddOn(void) { return m_bAddOn; }
	bool Rejuvenate(void) { return m_bRejuvenate; }
	int16_t TimeLimit(void) { return m_sTimeLimit > 0 ? m_sTimeLimit : 0; }
	int16_t KillLimit(void) { return m_sKillLimit > 0 ? m_sKillLimit : 0; }
	int16_t CoopLevels(void) { return m_sCoopLevels; }
	int16_t FrameTime(void) { return m_sFrameTime; }
	RFile *DemoModeDebugMovie(void) { return m_pfileDemoModeDebugMovie; }

	////////////////////////////////////////////////////////////////////////////////
	// Change the frame time (MP only)
	////////////////////////////////////////////////////////////////////////////////
	void SetFrameTime(
		int16_t sFrameTime)
	{
		m_sFrameTime = sFrameTime;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Set the SAK purge flag.
	////////////////////////////////////////////////////////////////////////////////
	void SetPurgeSaks(void)
	{
		m_bPurgeSaks = true;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Clear the SAK purge flag.
	////////////////////////////////////////////////////////////////////////////////
	void ClearPurgeSaks(void)
	{
		m_bPurgeSaks = false;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Query the SAK purge flag status.
	////////////////////////////////////////////////////////////////////////////////
	bool PurgeSaks(void)
	{
		return m_bPurgeSaks;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Get pointer to local dude if one exists, otherwise returns 0.
	////////////////////////////////////////////////////////////////////////////////
	CDude *LocalDudePointer(int8_t cDudeNum = 0)
	{
		CDude *pdudeLocal;
		if (m_prealm->m_idbank.GetThingByID((CThing **)&pdudeLocal, m_idLocalDudes[cDudeNum]) != 0)
			m_idLocalDudes[cDudeNum] = CIdBank::IdNil;

		return pdudeLocal;
	}

	// todo
	void KillLocalDudes(void)
	{
		if (m_ucNumLocalDudes > 1)
		{
			for(int8_t index = 0; index < MAX_DUDES; index++)
			{
				CDude *pDude = LocalDudePointer(index);
				if (pDude)
				{
					pDude->SetState(CThing3d::State_Suicide);
				}
			}
		}
		else
		{
			LocalDudePointer()->SetState(CThing3d::State_Suicide);
		}
	}


	////////////////////////////////////////////////////////////////////////////////
	// Query the game mode
	////////////////////////////////////////////////////////////////////////////////
	bool IsMP(void)
	{
		return false;
	}

	bool IsServer(void)
	{
		return false;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Set the game state
	////////////////////////////////////////////////////////////////////////////////
	void SetGameState_Ok(void)
	{
		m_gamestate = Game_Ok;
	}

	void SetGameState_RestartRealm(void)
	{
		// This should NEVER occur in MP mode
		ASSERT(!IsMP());
		m_gamestate = Game_RedoRealm;
	}

	void SetGameState_NextRealm(
		bool bServerToldMe = false)
	{
		m_gamestate = Game_NextRealm;
	}

	void SetGameState_GameOver(
		bool bServerToldMe = false)
	{
		// This should NEVER occur in MP mode
		// 09/12/97 MJR -- This USED TO BE TRUE, but now that we re-enabled the
		// use of the "just one realm" mode in MP in the case where the server
		// only has a single realm available, we need this again in MP mode,
		// so I commented it out.
		//	ASSERT(!IsMP());
		m_gamestate = Game_GameOver;
	}

	void SetGameState_GameAborted(
		bool bServerToldMe = false)
	{
		m_gamestate = Game_GameAborted;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Query the game state
	////////////////////////////////////////////////////////////////////////////////
	bool IsRealmDone(void)
	{
		return (m_gamestate >= Game_RedoRealm) ? true : false;
	}

	bool IsRestartingRealm(void)
	{
		return (m_gamestate == Game_RedoRealm) ? true : false;
	}

	bool IsNextRealm(void)
	{
		return (m_gamestate == Game_NextRealm) ? true : false;
	}

	bool IsGameOver(void)
	{
		return (m_gamestate >= Game_GameOver) ? true : false;
	}

	bool IsGameAborted(void)
	{
		return (m_gamestate == Game_GameAborted) ? true : false;
	}
};

////////////////////////////////////////////////////////////////////////////////
//
// Base class for all "Play Modules"
//
////////////////////////////////////////////////////////////////////////////////
class CPlay
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlay(void)
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	virtual ~CPlay()
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare game
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t PrepareGame( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo)		 // I/O: Play info
	{
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Determine if game is ready
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t IsGameReady( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo,		 // I/O: Play info
		bool *pbGameReady)		 // Out: Whether game is ready
	{
		*pbGameReady = true;
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start game
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t StartGame( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo)	   // I/O: Play info
	{
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start cutscene
	////////////////////////////////////////////////////////////////////////////////
	virtual void StartCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare realm
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t PrepareRealm( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo)		  // I/O: Play info
	{
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Determine if realm is ready
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t IsRealmReady( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo,		  // I/O: Play info
		bool *pbRealmReady)		  // Out: Whether realm is ready
	{
		*pbRealmReady = true;
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Do cutscene
	////////////////////////////////////////////////////////////////////////////////
	virtual void DoCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// End cutscene
	////////////////////////////////////////////////////////////////////////////////
	virtual void EndCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t StartRealm( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo)		// I/O: Play info
	{
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop user input
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopUserInput(
		CPlayInfo *pinfo, // I/O: Play info
		RInputEvent *pie) // I/O: Input event
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop update
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopUpdate(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render -- create and update images to the composite buffer but do
	// NOT update the screen.
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopRender(
		CPlayInfo *pinfo, bool bCanDraw) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render on top -- create and update images to the composite buffer
	// on top of things rendered in CoreLoopRender() but do NOT update the screen.
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopRenderOnTop(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop draw -- Draw CoreLoopRender[OnTop]() results to the screen.
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopDraw(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Determine if core loop is done
	////////////////////////////////////////////////////////////////////////////////
	virtual bool IsCoreLoopDone( // Returns true if done, false otherwise
		CPlayInfo *pinfo)		 // I/O: Play info
	{
		return pinfo->IsRealmDone();
	}

	////////////////////////////////////////////////////////////////////////////////
	// End realm
	////////////////////////////////////////////////////////////////////////////////
	virtual void EndRealm(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Unprepare game
	////////////////////////////////////////////////////////////////////////////////
	virtual void UnprepareGame(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm error handler
	////////////////////////////////////////////////////////////////////////////////
	virtual void StartRealmErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Is realm ready error handler
	////////////////////////////////////////////////////////////////////////////////
	virtual void IsRealmReadyErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare realm error handler
	////////////////////////////////////////////////////////////////////////////////
	virtual void PrepareRealmErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start game error handler
	////////////////////////////////////////////////////////////////////////////////
	virtual void StartGameErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Is game ready error handler
	////////////////////////////////////////////////////////////////////////////////
	virtual void IsGameReadyErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare game error handler
	////////////////////////////////////////////////////////////////////////////////
	virtual void PrepareGameErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}
};

////////////////////////////////////////////////////////////////////////////////
//
// Group of "Play Modules"
//
////////////////////////////////////////////////////////////////////////////////
class CPlayGroup
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
public:
	typedef RFList<CPlay *> Plays;

	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	Plays m_Plays; // List of play modules

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayGroup(void)
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	~CPlayGroup()
	{
		m_Plays.Reset();
	}

	////////////////////////////////////////////////////////////////////////////////
	// Add a module
	////////////////////////////////////////////////////////////////////////////////
	void AddModule(
		CPlay *pPlay)
	{
		m_Plays.InsertHead(pPlay);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Remove a module
	////////////////////////////////////////////////////////////////////////////////
	void RemoveModule(
		CPlay *pPlay)
	{
		// Search for the specific pointer and remove it.  This is inefficient, but
		// but it happens so rarely that it doesn't matter.  The alternative was for
		// AddModule() to return a Plays::Pointer, which the caller would pass to
		// this function so it could remove the object without searching for it.
		// This sound good, but then CPlay would have to know what a Plays::Pointer
		// is so it could hold on to it.  Unfortunately, the pointer is defined by
		// this class, and this class must follow the definition of CPlay, so you
		// end up with a classic which-comes-first-the-chicken-or-the-egg problem.
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
		{
			if (m_Plays.GetData(p) == pPlay)
			{
				m_Plays.Remove(p);
				break;
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare game
	////////////////////////////////////////////////////////////////////////////////
	int16_t PrepareGame(  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		int16_t sResult = 0;
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			sResult |= m_Plays.GetData(p)->PrepareGame(pinfo);
		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Determine if game is ready
	////////////////////////////////////////////////////////////////////////////////
	int16_t IsGameReady(   // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo,  // I/O: Play info
		bool *pbGameReady) // Out: Whether game is ready
	{
		int16_t sResult = 0;
		*pbGameReady = true;
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
		{
			bool bGameReady = false;
			sResult |= m_Plays.GetData(p)->IsGameReady(pinfo, &bGameReady);
			*pbGameReady &= bGameReady;
		}
		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start game
	////////////////////////////////////////////////////////////////////////////////
	int16_t StartGame(	  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		int16_t sResult = 0;
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			sResult |= m_Plays.GetData(p)->StartGame(pinfo);
		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start cutscene
	////////////////////////////////////////////////////////////////////////////////
	void StartCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{

		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->StartCutscene(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare realm
	////////////////////////////////////////////////////////////////////////////////
	int16_t PrepareRealm( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		int16_t sResult = 0;

		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
		{
			sResult |= m_Plays.GetData(p)->PrepareRealm(pinfo);
		}

		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Determine if realm is ready
	////////////////////////////////////////////////////////////////////////////////
	int16_t IsRealmReady(	// Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo,	// I/O: Play info
		bool *pbRealmReady) // Out: Whether realm is ready
	{
		int16_t sResult = 0;
		*pbRealmReady = true;
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
		{
			bool bRealmReady = false;
			sResult |= m_Plays.GetData(p)->IsRealmReady(pinfo, &bRealmReady);
			*pbRealmReady &= bRealmReady;
		}
		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Do cutscene
	////////////////////////////////////////////////////////////////////////////////
	void DoCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->DoCutscene(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// End cutscene
	////////////////////////////////////////////////////////////////////////////////
	void EndCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->EndCutscene(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm
	////////////////////////////////////////////////////////////////////////////////
	int16_t StartRealm(	  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		int16_t sResult = 0;
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			sResult |= m_Plays.GetData(p)->StartRealm(pinfo);
		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop user input
	////////////////////////////////////////////////////////////////////////////////
	void CoreLoopUserInput(
		CPlayInfo *pinfo, // I/O: Play info
		RInputEvent *pie) // I/O: Input event
	{

		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->CoreLoopUserInput(pinfo, pie);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop update
	////////////////////////////////////////////////////////////////////////////////
	void CoreLoopUpdate(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->CoreLoopUpdate(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render -- create and update images to the composite buffer but do
	// NOT update the screen.
	////////////////////////////////////////////////////////////////////////////////
	void CoreLoopRender(
		CPlayInfo *pinfo, bool bCanDraw) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->CoreLoopRender(pinfo, bCanDraw);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render on top -- create and update images to the composite buffer
	// on top of things rendered in CoreLoopRender() but do NOT update the screen.
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopRenderOnTop(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->CoreLoopRenderOnTop(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop draw -- Draw CoreLoopRender[OnTop]() results to the screen.
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopDraw(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->CoreLoopDraw(pinfo);		
	}

	////////////////////////////////////////////////////////////////////////////////
	// Determine if core loop is done
	////////////////////////////////////////////////////////////////////////////////
	bool IsCoreLoopDone(  // Returns true if done, false otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		bool bDone = true;
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			bDone &= m_Plays.GetData(p)->IsCoreLoopDone(pinfo);
		return bDone;
	}

	////////////////////////////////////////////////////////////////////////////////
	// End realm
	////////////////////////////////////////////////////////////////////////////////
	void EndRealm(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->EndRealm(pinfo);

		// Any dirty rects left over, we don't care about.
		//pinfo->m_drl.Empty();
	}

	////////////////////////////////////////////////////////////////////////////////
	// Unprepare game
	////////////////////////////////////////////////////////////////////////////////
	void UnprepareGame(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->UnprepareGame(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm error handler
	////////////////////////////////////////////////////////////////////////////////
	void StartRealmErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->StartRealmErr(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Is realm ready error handler
	////////////////////////////////////////////////////////////////////////////////
	void IsRealmReadyErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->IsRealmReadyErr(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare realm error handler
	////////////////////////////////////////////////////////////////////////////////
	void PrepareRealmErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->PrepareRealmErr(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start game error handler
	////////////////////////////////////////////////////////////////////////////////
	void StartGameErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->StartGameErr(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Is game ready error handler
	////////////////////////////////////////////////////////////////////////////////
	void IsGameReadyErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->IsGameReadyErr(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare game error handler
	////////////////////////////////////////////////////////////////////////////////
	void PrepareGameErr(
		CPlayInfo *pinfo) // I/O: Play info
	{
		for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
			m_Plays.GetData(p)->PrepareGameErr(pinfo);
	}
};

////////////////////////////////////////////////////////////////////////////////
//
// Display API
//
////////////////////////////////////////////////////////////////////////////////
class CPlayStatus : public CPlay
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	int32_t m_lLastFrameTime;
	int32_t m_lSumFrameTimes;
	int32_t m_lNumFrames;
	int32_t m_lLastIterationTime;
	/* 12/3/97 AJC */	
	int32_t m_lFramePerSecond;
	int32_t m_lPrevSeqTime;
	/* 12/3/97 AJC */
	int32_t m_lSumIterationTimes;
	int32_t m_lNumIterations;
	RRect m_rectDude;
	RRect m_rectRealm;
	RRect m_rectInfo;
	char m_szFileDescriptor[256];
	RPrint m_print;
	//PVR::cGuiTextString m_guiText;
	bool m_bFirstUpdate;
	bool m_bUpdateDude;	 // true, if dude status area was updated.
	bool m_bUpdateRealm; // true, if realm status area was updated.

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayStatus(void)
		: m_bUpdateRealm(true) // valgrind fix.  --ryan.
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	~CPlayStatus()
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	int16_t StartRealm(	  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		if (!pinfo->m_bBadRealmMP)
		{
			// Initialize times and counts.
			m_lLastFrameTime = rspGetMilliseconds();
			m_lLastIterationTime = m_lLastFrameTime;
			m_lSumFrameTimes = 0;
			m_lNumFrames = 0;
			pinfo->m_lSumUpdateDisplayTimes = 0;

			/*** 12/3/97 AJC ***/			
			m_lFramePerSecond = 0;		
			m_lPrevSeqTime = rspGetMilliseconds();
			/*** 12/3/97 AJC ***/

			m_lSumIterationTimes = 0;
			m_lNumIterations = 0;

			// Get app's descriptor.
			Play_GetApplicationDescriptor(m_szFileDescriptor, sizeof(m_szFileDescriptor));

			// Setup rects for various status areas
			m_rectDude.sX = DUDE_STATUS_RECT_X;
			m_rectDude.sY = DUDE_STATUS_RECT_Y;
			m_rectDude.sW = DUDE_STATUS_RECT_W;
			m_rectDude.sH = DUDE_STATUS_RECT_H;

			m_rectRealm.sX = REALM_STATUS_RECT_X;
			m_rectRealm.sY = REALM_STATUS_RECT_Y;
			m_rectRealm.sW = REALM_STATUS_RECT_W;
			m_rectRealm.sH = REALM_STATUS_RECT_H;

			m_rectInfo.sX = INFO_STATUS_RECT_X;
			m_rectInfo.sY = INFO_STATUS_RECT_Y;
			m_rectInfo.sW = INFO_STATUS_RECT_W;
			m_rectInfo.sH = INFO_STATUS_RECT_H;

			// We must lock the buffer before accessing it.
			//rspLockBuffer();

			// Init the tool bar
			ToolBarInit(pinfo->Realm()->m_phood);

			// Setup print	utilizing some values initialized by ToolBarInit().
			//m_print.SetDestGuiTextString(&m_guiText);
			m_print.m_bColourFixed = false; // todo??
			m_print.SetFont(DISP_INFO_FONT_HEIGHT, &g_fontBig);
			m_print.SetColor(gsStatusFontForeIndex, gsStatusFontBackIndex, gsStatusFontShadowIndex);
			m_print.SetEffectAbs(RPrint::SHADOW_X, 1);
			m_print.SetEffectAbs(RPrint::SHADOW_Y, 1);

			// Render it now so it appears in tandem with the top bar: unless
			// its the last demo level for the game where we don't want the
			// bars to let the player know that they are not under control.
			if (!g_bLastLevelDemo)
			{
				g_pvrRenderer->m_pGuiTextString->Flush();
				g_pvrRenderer->FlushBatchedDrawQueues();
			}

			// Unlock once done.
			rspUnlockBuffer();

			// todo: is this the weird glitch after ending cutscene?
			g_pvrRenderer->RenderBatchedFrame();		
		}
		return 0;
	}

	// todo? make virtual?
	void ResetCamera() // I/O: Play info
	{
		m_rectDude.sX = DUDE_STATUS_RECT_X;
		m_rectDude.sY = DUDE_STATUS_RECT_Y;
		m_rectDude.sW = DUDE_STATUS_RECT_W;
		m_rectDude.sH = DUDE_STATUS_RECT_H;

		m_rectRealm.sX = REALM_STATUS_RECT_X;
		m_rectRealm.sY = REALM_STATUS_RECT_Y;
		m_rectRealm.sW = REALM_STATUS_RECT_W;
		m_rectRealm.sH = REALM_STATUS_RECT_H;

		m_rectInfo.sX = INFO_STATUS_RECT_X;
		m_rectInfo.sY = INFO_STATUS_RECT_Y;
		m_rectInfo.sW = INFO_STATUS_RECT_W;
		m_rectInfo.sH = INFO_STATUS_RECT_H;
	}


	////////////////////////////////////////////////////////////////////////////////
	// Core loop update
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void CoreLoopUpdate(
		CPlayInfo *pinfo) // I/O: Play info
	{
		if (!pinfo->m_bBadRealmMP)
		{			
			//==============================================================================
			// Check for death stuff
			//==============================================================================

			if (g_sActivePlayers > PLAYER_1)
			{
				bool b_AllDead = true;

				for (int8_t index = 0; index < MAX_DUDES; index++)
				{
					CDude *pdudeLocal = pinfo->LocalDudePointer(index);					
					if (pdudeLocal != NULL)
					{
						if (!pdudeLocal->IsDead())
						{
							b_AllDead = false;							
						}					
						else
						{
							g_cLastPlayerToDie = index;
						}						
					}
				}

				if (b_AllDead)
				{			
					g_bAllCoopPlayersDead = true;							
				}
			}			
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render on top -- create and update images to the composite buffer
	// but do NOT update the screen.
	// NOTE that we use CoreLoopRenderOnTop() not b/c these go on top but instead as
	// a cheat to make sure that the m_bDrawFrame flag has already been set.
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void CoreLoopRenderOnTop(
		CPlayInfo *pinfo) // I/O: Play info
	{
		
		if (!pinfo->m_bBadRealmMP)
		{

			// If display info is enabled . . .
			if (g_GameSettings.m_sDisplayInfo)
			{
				// Add this iteration's elapsed time to total (sum).
				m_lSumIterationTimes += (rspGetMilliseconds() - m_lLastIterationTime);
				// Increment number of iterations.
				m_lNumIterations++;
			}

			m_lLastIterationTime = rspGetMilliseconds();

			// No need for this unless we're going to draw . . .
			if (pinfo->m_bDrawFrame)
			{
				bool bUpdateRealm = false;

				// If its the last level then don't draw the top and bottom, for
				// a letterbox look that lets the player know they don't have
				// control for this demo
				if (!g_bLastLevelDemo)
				{
					// Draw top bar.
					PVR::cTexture *tex = g_pvrRenderer->m_TextureRepository.Get(PVR_TEXTURE_TOPBAR);

					// todo: Cleanup all below
					uint32_t colour = 0xFFFFFFFF;
					if (g_pvrRenderer->m_fGlobalRScale != 1.f || g_pvrRenderer->m_fGlobalGScale != 1.f || g_pvrRenderer->m_fGlobalBScale != 1.f)
					{
						colour = PACK_ARGB8888(255, uint8_t(255.f * g_pvrRenderer->m_fGlobalRScale) , uint8_t(255.f * g_pvrRenderer->m_fGlobalGScale), uint8_t(255.f * g_pvrRenderer->m_fGlobalBScale));
					}
	
					if (g_GameSettings.m_cHudMode == 0)
					{
						// Draw topbar
						g_pvrRenderer->DrawQuadDirect(
							tex, 0,
							0.0, 0.0,
							1.0, 0.156,
							0, 0,
							256, 40, 34, 1.0f, colour);

						g_pvrRenderer->DrawQuadDirect(
							tex, 0,
							0.0, 0.156,
							1.0, 0.312,
							256, 0,
							256, 40, 34, 1.0f, colour);

						g_pvrRenderer->DrawQuadDirect(
							tex, 0,
							0.0, 0.312,
							0.496, 0.468,
							512, 0,
							128, 40, 34, 1.0f, colour);
					

						// Draw single-player toolbar
						if (g_sActivePlayers == PLAYER_1)
						{
							ToolBarRender(
							pinfo->Realm()->m_phood,
							m_rectDude.sX,
							m_rectDude.sY,
							pinfo->LocalDudePointer(), FALSE);
						}
						// Draw co-op toolbar
						else
						{							
							ToolBarRender(
							pinfo->Realm()->m_phood,
							m_rectDude.sX,
							m_rectDude.sY,
							pinfo->LocalDudePointer(), TRUE, 5);

							if (g_sActivePlayers & PLAYER_2)
							{
								ToolBarRender(
								pinfo->Realm()->m_phood,
								m_rectDude.sX,
								m_rectDude.sY,
								pinfo->LocalDudePointer(1), TRUE, 5);
							}						

							if (g_sActivePlayers & PLAYER_3)
							{
								ToolBarRender(
								pinfo->Realm()->m_phood,
								m_rectDude.sX,
								m_rectDude.sY,
								pinfo->LocalDudePointer(2), TRUE, 5);
							}		

							if (g_sActivePlayers & PLAYER_4)
							{
								ToolBarRender(
								pinfo->Realm()->m_phood,
								m_rectDude.sX,
								m_rectDude.sY,
								pinfo->LocalDudePointer(3), TRUE, 5);
							}	
						}	

					}
																					
					// Update the realm (or Score) status
					bUpdateRealm = ScoreUpdateDisplayPVR(
						&m_rectRealm,
						pinfo->Realm(),						
						REALM_STATUS_RECT_X,
						REALM_STATUS_RECT_Y,
						pinfo->Realm()->m_phood);
				}
				else
				{					
					int16_t sOffsetY = ((g_GameSettings.m_cCutSceneUnderscan == true)  ? -40 : 0);
					g_pvrRenderer->DrawLetterBoxOverlayDirect(40, 40, 0, 0);
					if (sOffsetY < 0)
					{
						g_pvrRenderer->DrawQuadDirect(uint32_t(PACK_ARGB8888(255, 0, 0, 0)),
							0, 
							416,
							640, 
							34,
							PVR::cRenderMgr::LayerHud
						);
					}					
				}

				// If display info is enabled, draw the info
				if (g_GameSettings.m_sDisplayInfo)
				{
					// Add this frame's elapsed time to total (sum)
					m_lSumFrameTimes += (rspGetMilliseconds() - m_lLastFrameTime);

					// Increment number of frames
					m_lNumFrames++;

					// Just update whenever the realm is updating so we can cash in on their
					// update display all.
					// If were not in MP then calculate the FPS. If we are in MP, FPS is calculated elsewhere *SPA
					if (!pinfo->IsMP())
						m_lFramePerSecond = (1000 * m_lNumFrames) / m_lSumFrameTimes;

					if (m_bUpdateRealm)
					{
						m_print.print(
							m_rectInfo.sX, m_rectInfo.sY,
							"FPS: %ld Video H/W Update: %ld%% %s",
							m_lFramePerSecond,
							(pinfo->m_lSumUpdateDisplayTimes * 100) / m_lSumFrameTimes,
							m_szFileDescriptor);

						// Reset.
						m_lSumFrameTimes = 0;
						m_lNumFrames = 0;
						pinfo->m_lSumUpdateDisplayTimes = 0;
					}
				}

				// todo: tighten?			
			//	m_guiText.Print(0, 0);
			//	m_guiText.Flush();				

				// By resetting this here, we ignore any time spent in the above code
				m_lLastFrameTime = rspGetMilliseconds();
			}
		}
	}
};

// todo: remove?
#if 1 //PLATFORM_UNIX
#include <sys/stat.h>
static void EnumSaveGamesSlots(Menu *menu)
{
	char gamename[RSP_MAX_PATH];
	int Max = (sizeof(menu->ami) / sizeof(menu->ami[0])) - 1;
	if (Max > MAX_SAVE_SLOTS)
		Max = MAX_SAVE_SLOTS;

	for (int i = 0; i < Max; i++)
	{
		snprintf(gamename, sizeof(gamename), "%s/%d.gme", SAVEGAME_DIR, i);
		const char *fname = FindCorrectFile(gamename, "w");
		struct stat statbuf;

		char timebuf[32];
		const char *str = "unknown";
		if (stat(fname, &statbuf) == -1)
			str = "available";
		else
		{
			struct tm *tm;
			if ((tm = localtime((const time_t *)&statbuf.st_mtime)) != NULL)
			{
				strftime(timebuf, sizeof(timebuf), "%m/%d/%y %H:%M", tm);
				str = timebuf;
			}
		}

#ifdef MOBILE
		snprintf(gamename, sizeof(gamename), "%d - [%s]", i, str);
		menu->ami[i].pszText = strdup(gamename);
#else
		snprintf(gamename, sizeof(gamename), "%s/%d.gme [%s]", SAVEGAME_DIR, i, str);
		menu->ami[i].pszText = strdup(gamename);
#endif

		menu->ami[i].sEnabled = (menu->ami[i].pszText != NULL);
		if (!menu->ami[i].sEnabled)
			break;
	}
}
#endif

#ifdef MOBILE
static bool continueIsRestart; //This is to make the continue button actually restart the level
#endif
////////////////////////////////////////////////////////////////////////////////
//
// Input API
//
////////////////////////////////////////////////////////////////////////////////
class CPlayInput : public CPlay
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	int32_t m_lDemoDeadTime; // Time dude has been dead for
	U8 *m_pau8KeyStatus;	 // Key status array
	//	REdit *m_peditChatIn;	 // Outgoing chat.

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayInput(void)
	{
		// todo??
		/*
		// We don't care if this fails.
		m_peditChatIn = (REdit *)RGuiItem::LoadInstantiate(FullPathCD(CHAT_IN_GUI));
		if (m_peditChatIn)
		{
			// Limit to chat length minus some room for our name.
			m_peditChatIn->m_sMaxText = MIN(CHAT_IN_LENGTH, Net::MaxChatSize - 1);

			// Recreate in the correct spot and dimensions . . .
			if (m_peditChatIn->Create(
					DUDE_STATUS_RECT_X,
					DUDE_STATUS_RECT_Y - m_peditChatIn->m_im.m_sHeight,
					DUDE_STATUS_RECT_W,
					m_peditChatIn->m_im.m_sHeight,
					8) == 0)
			{
				m_peditChatIn->SetVisible(FALSE);
			}
			else
			{
				delete m_peditChatIn;
				m_peditChatIn = NULL;
			}
		}
#ifdef MOBILE
		continueIsRestart = false;
#endif*/
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	~CPlayInput()
	{
		//delete m_peditChatIn;
		//m_peditChatIn = NULL;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm
	////////////////////////////////////////////////////////////////////////////////
	virtual int16_t StartRealm( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo)		// I/O: Play info
	{
		// Reset time he's been dead ('cause he isn't dead yet)
		m_lDemoDeadTime = -1;

		// Get the key status array
		m_pau8KeyStatus = NULL; // rspGetKeyStatusArray();

		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// End realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void EndRealm(
		CPlayInfo *pinfo) // I/O: Play info
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop user input
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopUserInput(
		CPlayInfo *pinfo, // I/O: Play info
		RInputEvent *pie) // I/O: Input event
	{

		// If we're in the menu, then just do that
		if (pinfo->m_bInMenu)
		{
			DoMenu(pinfo, pie);			
		}
		else
		{
			// Get useful pointers						
			CRealm *prealm = pinfo->Realm();

			//==============================================================================
			// A system-specific quit ends the game (MP mode is handled elsewhere)
			//==============================================================================
			if (rspGetQuitStatus())
			{
				if (pinfo->IsMP())
					pinfo->m_bUserQuitMP = true;
				else
					pinfo->SetGameState_GameAborted();
			}

			if (!pinfo->m_bBadRealmMP)
			{
				//==============================================================================
				// Handle all the local input stuff.
				//==============================================================================				
				g_bEndLevelKey = false;
	
				// If not in special end-of-the-game-demo-mode...
				if (!g_bLastLevelDemo)
				{
					// If playing back a demo and there's any input, then ignore the input and end the demo					
					if ((GetInputMode() == INPUT_MODE_PLAYBACK) && (pie->type == RInputEvent::Key))
					{
						pinfo->SetGameState_GameAborted();
					}
					else
					{						
						// Handle pause button or device disconnect											
						bool bSuspendRealm = false;											
						for (int8_t index = 0; index < MAX_DUDES; index++)						
						{						
							if (g_sActivePlayers & g_sPlayerFlags[index])
							{
								CDude *pdudeLocal = pinfo->LocalDudePointer(index);															
								MAPLE::cMapleController *pController = pdudeLocal->m_pController;
								CInputSettings *pInputSettings = &g_InputSettings[pController->m_cControlSchemeId];																		
								int8_t cStatus = pController->Status();							
								
								if (cStatus == MAPLE::cMapleController::ControllerDisconnected && pController->m_ulDisconnectedCycles > 30)
								{																									
									bSuspendRealm = true;
									break;
								}
								
								MapleInputState mis;
								GetMapleInputStateNoUpdate(&mis, index);
								if (mis.ButtonState[pInputSettings->m_sJoyStartButton] == MapleInputState::Press)
								{																																				
									if (pinfo->m_bInHighScoreTable)
									{									
										bSuspendRealm = false;
										pinfo->m_bInHighScoreTable = false;																		
									}
									else
									{
										bSuspendRealm = true;
									}
									break;
								}								
							}							
						}

						if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr)
						{							
							if (MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Disconnected && MAPLE::cMapleVmu::m_pActiveVmu->m_ulDisconnectedCycles > 30)
							{
								bSuspendRealm = true;	
							}							
						}
						
						if (bSuspendRealm)
						{								
							cMapleJump::m_bRumbleOff = true;
							if (!pinfo->Realm()->IsSuspended())
							{
								pinfo->Realm()->Suspend();	
								// Propagate rumble off 								
								rspGetJoy(0)->m_pJumpPack->RumbleUpdate();
								if (g_sActivePlayers & PLAYER_2)
								{
									rspGetJoy(1)->m_pJumpPack->RumbleUpdate();
								}
				
								if (g_sActivePlayers & PLAYER_3)
								{
									rspGetJoy(2)->m_pJumpPack->RumbleUpdate();
								}

								if (g_sActivePlayers & PLAYER_4)
								{
									rspGetJoy(3)->m_pJumpPack->RumbleUpdate();
								}								
							}								
						}
																						
						// If "live" and not MP . . .
						if ((GetInputMode() == INPUT_MODE_LIVE) && !pinfo->IsMP())
						{
							bool bRestart = false;
							for (int8_t index = 0; index < MAX_DUDES; index++)						
							{						
								if (g_sActivePlayers & g_sPlayerFlags[index])
								{
									// If revive key pressed . . .
									CDude *pdudeLocal = pinfo->LocalDudePointer(index);
									if ((GetInput(pdudeLocal->m_sDudeNum) & INPUT_REVIVE) && !pinfo->Realm()->IsSuspended())
									{																
										// If there's a local dude . . .
										if (pdudeLocal)
										{
											// If he's dead . . .
											if (pdudeLocal->IsDead() == true && g_bAllCoopPlayersDead == true)
											{
												// Restart the realm
												bRestart = true;
												break;
											}
										}
										else
										{
											// Restart the realm
											bRestart = true;
											break;
										}
									}
								}
							}
							
							if (bRestart)
							{
								// Restart the realm
								pinfo->SetGameState_RestartRealm();

								// Check the goal (since the user pressed revive, we'll
								// say the 'end the level' key was pressed.
								if (prealm->IsEndOfLevelGoalMet(true))
								{
									// The goal was met, show the dialog(s).
									//	ScoreDisplayHighScores(prealm);
								}
							}							
						}
					}
				}

				//==============================================================================
				// Check if end of level goal has been met.  Depending on the game type, this
				// may require that the user pressed the end-of-level key.
				//==============================================================================								
				
				// todo: magic numbers, clean up etc
				if (prealm->IsEndOfLevelGoalPrompt() && (pinfo->Realm()->m_sExitX != 0 || pinfo->Realm()->m_sExitY != 0))				
				{
						// Apply bounce
						static bool bDirection = 1;					
						static float currBounce = 1.f;
						int16_t fontSize = 30;

						const float incrementPerMilliSecond = 0.0008;
						const float decrementPerMilliSecond = 0.001;						

						if (!bDirection)
						{
							currBounce += float(prealm->m_lElapsedTime) * incrementPerMilliSecond;
							if (currBounce >= 1.166f)
							{
								bDirection = 1;
								currBounce = 1.166f;
							}
						}
						else
						{
							currBounce -= float(prealm->m_lElapsedTime) * decrementPerMilliSecond;
							if (currBounce <= 1.f)
							{
								bDirection = 0;
								currBounce = 1.f;
							}
						}

						bool bLeftOfExit = (pinfo->Camera()->m_sSceneViewX + 320 <= pinfo->Realm()->m_sExitX);
						fontSize = int16_t(float(fontSize) * currBounce);
						RPrint print;
						// Create text layer
						print.SetFont(fontSize, &g_fontPostal);
						print.SetColorRGB(192, 0, 0);									
						print.SetEffectAbs(RPrint::SHADOW_X, 1);
						print.SetEffectAbs(RPrint::SHADOW_Y, 1);
						print.SetWordWrap(TRUE);
						print.SetColumn(0, 0, 300, 200);
																																																								
						// Clamp to screen dimensions.														
						if (!prealm->IsSuspended())
						{
							print.offsetX = pinfo->Realm()->m_sExitX - pinfo->Camera()->m_sScene2FilmX;
							print.offsetY = (pinfo->Realm()->m_sExitY - pinfo->Camera()->m_sScene2FilmY) - 50;

							int16_t sOffsetClampMinX = 10;
							int16_t sOffsetClampMinY = 40;
							int16_t sOffsetClampMaxX = 540;
							int16_t sOffsetClampMaxY = 400;							

							// todo: magic numbers
							if (bLeftOfExit)
							{
													
								switch (g_GameSettings.m_cActiveLanguage)
								{
									case 0:
										print.offsetX -= 90;
										sOffsetClampMaxX = 540;
									break;
									case 1:
										print.offsetX -= 140;																
										sOffsetClampMaxX = 475;
									break;
									case 2:
										print.offsetX -= 120;																
										sOffsetClampMaxX = 505;
									break;
									case 3:
										print.offsetX -= 105;
										sOffsetClampMaxX = 520;
									break;
									case 4:
										print.offsetX -= 140;																
										sOffsetClampMaxX = 485;
									break;
									case 5:
										print.offsetX -= 85;
										sOffsetClampMaxX = 550;
									break;
								}
							}

							if (print.offsetY < sOffsetClampMinY)
								print.offsetY = sOffsetClampMinY;
							if(print.offsetX > sOffsetClampMaxX)
								print.offsetX = sOffsetClampMaxX;

							if (print.offsetX < sOffsetClampMinX)
								print.offsetX = sOffsetClampMinX;
							if (print.offsetY > sOffsetClampMaxY)
								print.offsetY = sOffsetClampMaxY;

							if (pinfo->Camera()->m_sSceneViewX + 320 <= pinfo->Realm()->m_sExitX)
							{
								print.printPVR(0, 0, g_pszExitR[g_GameSettings.m_cActiveLanguage]);
							}
							else
							{
								print.printPVR(0, 0, g_pszExitL[g_GameSettings.m_cActiveLanguage]);
							}	
						}
															
						for (int8_t index = 0; index < MAX_DUDES; index++)
						{
							float dSquareDistance	= 0.0;
							float dX;
							float dY;

							CDude *pdude = pinfo->LocalDudePointer(index);
							if (pdude)							
							{									
								dX = float(pdude->m_sprite.m_sX2 - pinfo->Realm()->m_sExitX);
								dY = float(pdude->m_sprite.m_sY2 - pinfo->Realm()->m_sExitY);
								dSquareDistance = rspSqrtf((dX * dX) + (dY * dY));									
								if (MATH_fabs(dSquareDistance) < 75.f)
								{
									cMapleJump::m_bRumbleOff = true;
									g_bEndLevelKey = true;

									// Propogate rumble off 
									rspGetJoy(0)->m_pJumpPack->RumbleUpdate();
									if (g_sActivePlayers & PLAYER_2)
									{
										rspGetJoy(1)->m_pJumpPack->RumbleUpdate();
									}
				
									if (g_sActivePlayers & PLAYER_3)
									{
										rspGetJoy(2)->m_pJumpPack->RumbleUpdate();
									}

									if (g_sActivePlayers & PLAYER_4)
									{
										rspGetJoy(3)->m_pJumpPack->RumbleUpdate();
									}				
								}
							}							
						}						
					}
					
					if (pinfo->Gauntlet() || g_sActivePlayers > PLAYER_1)
					{
						if (prealm->IsEndOfLevelGoalMet(g_bEndLevelKey) && !prealm->IsSuspended())
						{														
							prealm->Suspend();						
						}
					}
					else
					{
						if (prealm->IsEndOfLevelGoalMet(g_bEndLevelKey))
						{
							// Set so we'll go to the next realm
							pinfo->SetGameState_NextRealm();							
						}
					}
																			
				
				SetInput(0, GetLocalInput(prealm, rspUpdateJoy(0), pie));

				if (g_sActivePlayers & PLAYER_2)
				{
					SetInput(pinfo->LocalDudePointer(1)->m_sDudeNum, GetLocalInput(prealm, rspUpdateJoy(1), pie));
				}
				
				if (g_sActivePlayers & PLAYER_3)
				{
					SetInput(pinfo->LocalDudePointer(2)->m_sDudeNum, GetLocalInput(prealm, rspUpdateJoy(2), pie));
				}

				if (g_sActivePlayers & PLAYER_4)
				{
					SetInput(pinfo->LocalDudePointer(3)->m_sDudeNum, GetLocalInput(prealm, rspUpdateJoy(3), pie));
				}				

				//==============================================================================
				// Demo mode stuff
				//==============================================================================

				if (GetInputMode() != INPUT_MODE_LIVE)
				{
					CDude* pdudeLocal = pinfo->LocalDudePointer();
					// If the local dude dies, we wait a short period of time and then end the game.
					if (pdudeLocal != NULL)
					{
						if (pdudeLocal->IsDead() == true)
						{
							// If this is the first time here, reset the timer
							if (m_lDemoDeadTime < 0)
								m_lDemoDeadTime = prealm->m_time.GetGameTime();

							// If he's been dead long enough, end the game
							if (prealm->m_time.GetGameTime() > m_lDemoDeadTime + DEMO_MAX_DEAD_TIME)
								pinfo->SetGameState_GameOver();
						}
					}
					else
					{
						// If the dude goes away, end the game right away
						pinfo->SetGameState_GameOver();
					}

					// If we're in Playback mode . . .
					if (GetInputMode() == INPUT_MODE_PLAYBACK)
					{
						// If the input is done . . .
						if (InputIsDemoOver() == true)
						{
							// Draw one last frame to a texture for the martini effect.
							// We end the game after this final frame is drawn.
							pinfo->m_bDFrawFrameToTexture = true;
						}						
					}

					// Govern the speed of the loop
					while (prealm->m_time.GetRealTime() - prealm->m_time.GetGameTime() < 0);
				}
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// Unprepare game
	////////////////////////////////////////////////////////////////////////////////
	virtual void UnprepareGame(
		CPlayInfo *pinfo) // I/O: Play info
	{
		// If we're still on the menu, end it now
		if (pinfo->m_bInMenu)
			StopMenu(pinfo);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render -- create and update images to the composite buffer but do
	// NOT update the screen.
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void CoreLoopRenderOnTop(
		CPlayInfo *pinfo) // I/O: Play info
	{
		// todo remove		
	}

	////////////////////////////////////////////////////////////////////////////////
	//
	// Start the menu
	//
	////////////////////////////////////////////////////////////////////////////////
	void StartMenu(
		CPlayInfo *pinfo, Menu *pmenu) // I/O: Play info
	{
		// Disable 'Play Options' on 'Options' menu.
		// menuOptions.ami[5].sEnabled	= FALSE;
		// Disable 'Organ' on 'Audio Options' menu.
		menuAudioOptions.ami[1].sEnabled = FALSE;
		// Disable 'Language' on 'Audio Options' menu.
		menuAudioOptions.ami[2].sEnabled = FALSE;
		// Disable 'Save' IF in multiplayer.
		//menuClientGame.ami[1].sEnabled = (pinfo->IsMP() == true) ? FALSE : TRUE;

		// Start the menu		
		if (::StartMenu(pmenu, &g_resmgrShell) == 0)
		{				
			// Set flag to indicate we're in the menu
			pinfo->m_bInMenu = true;
			pinfo->m_iMenuInTransition = 0;
		}
		else
		{
			// Clean up
			StopMenu(pinfo);
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	//
	// Do one iteration of the menu (must have called StartMenu() previously!!!)
	//
	////////////////////////////////////////////////////////////////////////////////
	void DoMenu(
		CPlayInfo *pinfo, // I/O: Play info
		RInputEvent *pie) // I/O: Input event
	{
		// Make sure StartMenu() was called
		ASSERT(pinfo->m_bInMenu);

		// Run the menu
		ms_menuaction = MenuActionNone;

		DoMenuInput(pie);

		switch (ms_menuaction)
		{
		// Nothing in particular.
		case MenuActionNone:					
			break;

		case MenuActionRestart:
			StopMenu(pinfo);
			g_bKillLocalDudes = true;			
		break;
		
		// User quit choice.
		case MenuActionQuit:
			StopMenu(pinfo);
			// User hit "Quit" in the menu; end the game.
			if (pinfo->IsMP())
				pinfo->m_bUserQuitMP = true;
			else
				pinfo->SetGameState_GameAborted();
			break;		
		case MenuActionEndMenu:
			StopMenu(pinfo);
			break;
		default:			
			TRACE("RespondToMenuRequest(): Unhandled action %d.\n", ms_menuaction);
			break;
		}
		
		ms_menuaction = MenuActionNone;		
	}

	////////////////////////////////////////////////////////////////////////////////
	//
	// End the menu
	//
	////////////////////////////////////////////////////////////////////////////////
	void StopMenu(
		CPlayInfo *pinfo) // I/O: Play info
	{
		CDude *pdudeLocal = pinfo->LocalDudePointer();

		// End the menu
		::StopMenu();

		if (g_pvrRenderer->m_szDrawMode == PVR::cRenderMgr::Batched)
		{
			// Update the display reflecting the erasure of the menu.
			rspUpdateDisplay();
		}
	
		// Set the local dude's color in case the user changed it on the menu.  In MP
		// mode we have to ignore this because we currently don't support the messages that
		// would be necessary to tell all the other players about the color change.
		if (g_sActivePlayers == PLAYER_1)
		{
			CDude *pdudeLocal = pinfo->LocalDudePointer();
			if (pdudeLocal)
				pdudeLocal->m_sTextureIndex = MAX((int16_t)0, MIN((int16_t)(CDude::MaxTextures - 1), g_GameSettings.m_sPlayerColorIndex));
		}

		// Re-enable 'Organ' on 'Audio Options' menu.
		menuAudioOptions.ami[1].sEnabled = TRUE;
		// Re-enable 'Language' on 'Audio Options' menu.
		menuAudioOptions.ami[2].sEnabled = TRUE;

		// Fade colors back in
		if (g_pvrRenderer->m_szDrawMode == PVR::cRenderMgr::Batched)
		{
			PalTranOff();
		}
		
		// Clear the local input.
		ClearLocalInput();

		// Restore camera's screen access.
		pinfo->Camera()->SetViewSize(
			g_GameSettings.m_sViewW * g_GameSettings.m_dGameFilmScale,
			g_GameSettings.m_sViewH * g_GameSettings.m_dGameFilmScale);
		
		// Clear flag
		pinfo->m_bInMenu = false;
#ifdef MOBILE
		AndroidSetScreenMode(TOUCH_SCREEN_GAME);
#endif
	}
};

////////////////////////////////////////////////////////////////////////////////
//
// Realm API
//
////////////////////////////////////////////////////////////////////////////////
class CPlayRealm : public CPlay
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
private:
	typedef struct
	{
		CStockPile stockpile;
		CDude::WeaponType weapon;
	} LevelPersist;

	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	bool m_bMakeDemoMovie_WaitForClick;			  // Flag used when making demo movies
	double m_dCurrentFilmScale;
	int16_t m_sCurrentGripZoneRadius;
	int32_t m_lNumSeqSkippedFrames;

public:
	// todo:?
	LevelPersist m_alevelpersist[16]; // Index by CDude::m_sDudeNum.

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayRealm(void)
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	~CPlayRealm()
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare game
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	int16_t PrepareGame(  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		// Note whether multiplayer.
		pinfo->Realm()->m_flags.bMultiplayer = pinfo->IsMP();

		// Array of LevelPersist to carry players' ammo, health, kevlar, current
		// weapon, etc. from level to level.  Using CDudes in this manner was
		// another idea, but when I tried carrying them from level to level, many,
		// many possibilities for error were revealed.  For example, various realm
		// dependent things in the CDude and his base classes are cleaned up by
		// those respective classes in the destructor (e.g., his smash and his
		// sprite and, in some cases, a child object).
		// Initialize to appropriate values.
		int16_t sDudeIndex;
		// todo:
		for (sDudeIndex = 0; sDudeIndex < 16; sDudeIndex++)
		{
			// Clear stockpile.
			m_alevelpersist[sDudeIndex].stockpile.Zero();

			// Make machine gun the default weapon.
			m_alevelpersist[sDudeIndex].weapon = CDude::SemiAutomatic;
		}

		// Debug demo mode stuff (always active -- takes essentially no time unless enabled from game.cpp)
		m_bMakeDemoMovie_WaitForClick = true;

		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	int16_t PrepareRealm( // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		int16_t sResult = 0;
		CRealm *prealm = pinfo->Realm();

		// Clear realm (in case there's any crap left over from last realm)
		prealm->Clear();

		// If we're supposed to purge the SAKs . . .
		if (pinfo->PurgeSaks() == true)
		{
			// We must do this after the Clear() to make sure all the objects
			// have been deleted so that they'll release their resources.
			// Also, we must, of course, do this before the Load().
			g_resmgrGame.Purge();
			g_resmgrSamples.Purge();
			g_resmgrRes.Purge();
			g_resmgrShell.Purge();

			// Clear the flag.
			pinfo->ClearPurgeSaks();
		}

		// Reset time here so that objects can use it when they are loaded
		prealm->m_time.Reset();

		// If there's already a realm error, then don't bother with this
		if (!pinfo->m_bBadRealmMP)
		{

			// Check if specified file exists
			if (prealm->DoesFileExist((char *)pinfo->RealmName()))
			{
				// Load realm (false indicates NOT edit mode)
				if (prealm->Load((char *)pinfo->RealmName(), false) == 0)
				{					
					// Startup the realm
					if (prealm->Startup() == 0)
					{						
						//==============================================================================
						// Set up dudes.  This can take a while (dude's need lots of resources) so it
						// should be done before the cutscene stuff.
						//==============================================================================
						// If this game was loaded from a saved game file, then copy the
						// global stockpile into the level persistent data array.											
						for (int8_t cPlayerId = 0; cPlayerId < MAX_DUDES; cPlayerId++)
						{
							if (g_sActivePlayers & g_sPlayerFlags[cPlayerId])
							{
								if (g_bTransferStockpile)
								m_alevelpersist[cPlayerId].stockpile.Copy(&g_stockpile[cPlayerId]);
								// Otherwise, keep the global savable stockpile up to date.
								else						
								g_stockpile[cPlayerId].Copy(&m_alevelpersist[cPlayerId].stockpile);
							}
						}
												
						g_bTransferStockpile = false;						

						// Set up as many dudes as needed and get pointer to local dude
						sResult = SetupDudes(pinfo, m_alevelpersist);
					}
					else
					{
						sResult = -1;
						TRACE("CPlayRealm::PrepareRealm(): Error starting-up realm!\n");
					}
				}
				else
				{
					sResult = -1;
					TRACE("CPlayRealm::PrepareRealm(): Error loading realm!\n");
				}
			}
			else
			{
				sResult = -1;
				TRACE("CPlayRealm::PrepareRealm(): File does not exist: %s\n", (char *)pinfo->RealmName());
			}

			// If there was an error, and this is an MP game, then we ignore the error for now,
			// and instead we set a flag saying the realm is bad.  This is done so we can handle
			// the error as part of the core loop, which is where similar errors are already handled.
			if ((sResult != 0) && pinfo->IsMP())
			{
				sResult = 0;
				pinfo->m_bBadRealmMP = true;
			}
		}

		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	int16_t StartRealm(	  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		if (!pinfo->m_bBadRealmMP)
		{
			// Get realm
			CRealm *prealm = pinfo->Realm();
					
			// Setup camera
			pinfo->Camera()->SetScene(&(prealm->m_scene));
			pinfo->Camera()->SetHood((CHood *)(prealm->m_aclassHeads[CThing::CHoodID].GetNext()));
			pinfo->Camera()->SetView(g_GameSettings.m_sViewX, g_GameSettings.m_sViewY, g_GameSettings.m_sViewW, g_GameSettings.m_sViewH);

			// Set grip to control camera.
			pinfo->Grip()->SetCamera(pinfo->Camera());

			// Set hood's palette.
			prealm->m_phood->SetPalette();

			// Setup initial film scaling
			ScaleFilm(pinfo, false);

			// Reset time so that the first time update doesn't show (much) elapsed time.
			prealm->m_time.Reset();

			// Reset
			m_lNumSeqSkippedFrames = 0;
		}

		return 0;
	}

	void ResetCamera(CPlayInfo *pinfo)
	{
		if (!pinfo->m_bBadRealmMP)
		{
			// Get realm
			CRealm *prealm = pinfo->Realm();					
			// Setup camera						
			pinfo->Camera()->SetView(g_GameSettings.m_sViewX, g_GameSettings.m_sViewY, g_GameSettings.m_sViewW, g_GameSettings.m_sViewH);
			// Set grip to control camera.
			pinfo->Grip()->SetCamera(pinfo->Camera());			
			// Setup initial film scaling
			ScaleFilm(pinfo, false);						
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop render -- create and update images to the composite buffer but do
	// NOT update the screen.
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void CoreLoopRender(
		CPlayInfo *pinfo, bool bCanDraw) // I/O: Play info
	{

		if (!pinfo->m_bBadRealmMP)
		{
			// If scale or radius have changed from our current values, then we need
			// to redo everything now.
			if ((m_dCurrentFilmScale != g_GameSettings.m_dGameFilmScale) ||
				(m_sCurrentGripZoneRadius != g_GameSettings.m_sGripZoneRadius))
			{
				ScaleFilm(pinfo);
			}

			// Figure out whether to do a frame or not
			bool bDoFrame = true;
			if (pinfo->IsMP())
			{
				// In MP mode, we simply follow the special MP flag.  We don't care about
				// the menu, because we must let the game continue to run so we stay in
				// sync with everyone else.
				bDoFrame = pinfo->m_bDoRealmFrame;
			}		

			if (bDoFrame)
			{

				// Get realm pointer
				CRealm *prealm = pinfo->Realm();

				// Adjust realm time.  How we do it depends on the mode we're in.
				if (GetInputMode() == INPUT_MODE_LIVE)
				{
					if (pinfo->IsMP() && !pinfo->m_bInMenu)
					{
						// In multiplayer mode, time moves in hardwired increments
						prealm->m_time.Update(pinfo->FrameTime());
					}
					else if (!pinfo->m_bInMenu)
					{
						// In non-network mode, time flows freely
						prealm->m_time.Update();
					}
				}
				else
				{
					// In demo mode, time moves in hardwired increments
					prealm->m_time.Update(DEMO_TIME_PER_FRAME);
				}

				// Update Realm
				prealm->Update();

				// Prepare Realm for rendering (Snap()).							
				prealm->Render();
								
				// In demo mode (record or playback) we don't draw the results of the frame if
				// we're falling behind.  However, we always draw when doing a demo-mode-movie .
				pinfo->m_bDrawFrame = true;
				if ((pinfo->DemoModeDebugMovie() == 0) && (GetInputMode() != INPUT_MODE_LIVE))
				{
					// If we've fallen behind the demo frame rate by our max lag . . .
					if (prealm->m_time.GetRealTime() - prealm->m_time.GetGameTime() > DEMO_MAX_LAG)
					{
						// If we haven't already skipped too many frames . . .
						if (m_lNumSeqSkippedFrames < DEMO_MAX_SEQUENTIAL_SKIPPED_FRAMES)
						{
							m_lNumSeqSkippedFrames++;
							pinfo->m_bDrawFrame = false;
						}
						else
							m_lNumSeqSkippedFrames = 0;
					}
					else
						m_lNumSeqSkippedFrames = 0;
				}

				if (pinfo->m_ucNumLocalDudes > 1)
				{
					// todo: clean up
					int16_t sX, sY;
					int16_t X, Y, gX, gY, gZ;
					X = Y = gX = gY = gZ = 0;
					float fNumAlive = 0.f;
					for (int8_t index = 0; index < MAX_DUDES; index++)
					{						
						CDude *pdude = pinfo->LocalDudePointer(index);						
						if (pdude && !pdude->IsDead())
						{
							prealm->Map3Dto2D(pdude->GetX(), pdude->GetY(), pdude->GetZ(), &sX, &sY);
							X += sX;
							Y += sY;	
							gX += pdude->GetX();
							gY += pdude->GetY();
							gZ += pdude->GetZ();					
							fNumAlive+=1.f;
						}						
					}			

					if (fNumAlive > 0.f)
					{
						gX = int16_t(rspFastDiv(float(gX), fNumAlive));
						gY = int16_t(rspFastDiv(float(gY), fNumAlive));
						gZ = int16_t(rspFastDiv(float(gZ), fNumAlive)); 
						X = int16_t(rspFastDiv(float(X), fNumAlive));
						Y = int16_t(rspFastDiv(float(Y), fNumAlive));						
						pinfo->Grip()->TrackTarget(X, Y, 30);						
						SetSoundLocation(gX, gY, gZ);
					}
					else
					{
						CDude *pdudeLocal = pinfo->LocalDudePointer(g_cLastPlayerToDie);
						if (pdudeLocal != NULL)
						{													
							prealm->Map3Dto2D(pdudeLocal->GetX(), pdudeLocal->GetY(), pdudeLocal->GetZ(), &sX, &sY);						
							pinfo->Grip()->TrackTarget(sX, sY, 30);						
							SetSoundLocation(pdudeLocal->GetX(), pdudeLocal->GetY(), pdudeLocal->GetZ());
						}
					}
				}
				else
				{
					// Track the local dude with the grip/camera and adjust the sound, too				
					CDude *pdudeLocal = pinfo->LocalDudePointer();
					if (pdudeLocal != NULL)
					{
						// Update grip/camera
						int16_t sX, sY;
						prealm->Map3Dto2D(pdudeLocal->GetX(), pdudeLocal->GetY(), pdudeLocal->GetZ(), &sX, &sY);
						// todo: local coop
						pinfo->Grip()->TrackTarget(sX, sY, 30);

						// Set coordinates for the "ear"
						SetSoundLocation(pdudeLocal->GetX(), pdudeLocal->GetY(), pdudeLocal->GetZ());
					}
				}

				// Snap picture of scene.  Even if we DON'T want to draw this frame, we still
				// have to allow a certain amount of work to get done (we still need things like
				// collision areas to be updated via the 3D scene rendered).  The scene flag tells
				// the scene whether or not to do BLiT's (and anything else that's purely cosmetic.)
				g_bSceneDontBlit = !pinfo->m_bDrawFrame;

				// todo: ???						
				g_bSceneDontBlit = !bCanDraw;
				pinfo->Camera()->Snap();
				g_bSceneDontBlit = false;

				// If in MP mode, clear the flag
				if (pinfo->IsMP())
					pinfo->m_bDoRealmFrame = false;
			}
			else
			{
				// 11/18/97	JMI	This didn't seem to get cleared in the case bDoFrame
				//						is false but I didn't see why we'd need to update the
				//						screen in this case (perhaps this is part of our net
				//						slow down?).
				pinfo->m_bDrawFrame = false;
			}

			// If we should draw the frame . . .
			if (pinfo->m_bDrawFrame)
			{
				CCamera *pcamera = pinfo->Camera();			
			}			
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// Core loop draw -- Draw CoreLoopRender() results to the screen.
	////////////////////////////////////////////////////////////////////////////////
	virtual void CoreLoopDraw(
		CPlayInfo *pinfo) // I/O: Play info
	{
		// Only if we're not on the menu and not a bad realm
		//	if (!pinfo->m_bInMenu && !pinfo->m_bBadRealmMP)
		{
			// If we did draw the frame, we need to copy the results to the screen
			if (pinfo->m_bDrawFrame)
			{
				// Special demo-mode debug stuff
				//if (pinfo->DemoModeDebugMovie() && (GetInputMode() != INPUT_MODE_LIVE))
				//MakeDemoMovie(pinfo->DemoModeDebugMovie());
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// End realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void EndRealm(
		CPlayInfo *pinfo) // I/O: Play info
	{
		if (!pinfo->m_bBadRealmMP)
		{
			CRealm *prealm = pinfo->Realm();

			// If we're not simply restarting the level . . .
			if (pinfo->IsRestartingRealm() == false)
			{
				// Update players' stockpiles.
				CListNode<CThing> *plnDude = prealm->m_aclassHeads[CThing::CDudeID].m_pnNext;
				CListNode<CThing> *plnDudeTail = &(prealm->m_aclassTails[CThing::CDudeID]);
				while (plnDude != plnDudeTail)
				{
					CDude *pdude = (CDude *)plnDude->m_powner;

					if (pinfo->Gauntlet())
					{
						pdude->m_stockpile.Zero();
					}

					m_alevelpersist[pdude->m_sDudeNum].stockpile.Copy(&(pdude->m_stockpile));
					m_alevelpersist[pdude->m_sDudeNum].weapon = pdude->GetCurrentWeapon();
					plnDude = plnDude->m_pnNext;
				}
			}

			// Shutdown realm
			prealm->Shutdown();
		}
	}

private:
	////////////////////////////////////////////////////////////////////////////////
	// Setup local dude
	////////////////////////////////////////////////////////////////////////////////
	void SetupLocalDude(
		CPlayInfo *pinfo, // I/O: Play info
		CDude *pdude,
		int8_t cDudeNum)	  // In:  Dude to setup
	{
		// Get local dude's ID
		pinfo->m_idLocalDudes[cDudeNum] = pdude->GetInstanceID();

		// Turn on local dude's xray effect
		pdude->m_sprite.m_sInFlags |= CSprite::InXrayee;

		// Set local dude's targeting status
		pdude->m_bTargetingHelpEnabled = (g_GameSettings.m_sCrossHair != FALSE) ? true : false;	

		pdude->SetCamera(pinfo->Camera());
	}

	////////////////////////////////////////////////////////////////////////////////
	// Setup general dude
	////////////////////////////////////////////////////////////////////////////////
	void SetupGeneralDude(
		CDude *pdude,				  // In:  Dude to setup
		int16_t sColor,				  // In:  Player's color
		LevelPersist *palevelpersist) // In:  Players' level persistent data.
	{
		// Union player's pre-existing stockpile with warped-in dude and give him his prior weapon
		ASSERT(pdude != NULL);
		pdude->m_stockpile.Union(&(palevelpersist[pdude->m_sDudeNum].stockpile));
		pdude->SetWeapon(palevelpersist[pdude->m_sDudeNum].weapon, true, true);

		// Set dude's color
		pdude->m_sTextureIndex = sColor;
		if (pdude->m_sTextureIndex < 0)
			pdude->m_sTextureIndex = 0;
		if (pdude->m_sTextureIndex >= CDude::MaxTextures)
			pdude->m_sTextureIndex = CDude::MaxTextures - 1;
	}

	////////////////////////////////////////////////////////////////////////////////
	//
	// Setup dudes in this realm based on the specified parameters.
	//
	// This function is designed to work properly with (1) levels that have CDude's
	// but no CWarps, (2) levels that have CWarp's but no CDude's, and (3) levels
	// that have a combination of CWarp's and CDude's.
	//
	// If this function completes successfully, the specified number of CDude's
	// will exist (no more, no less).
	//
	////////////////////////////////////////////////////////////////////////////////
	int16_t SetupDudes(
		CPlayInfo *pinfo,			  // I/O: Play info
		LevelPersist *palevelpersist) // In:  Players' level persistent data.
	{
		int16_t sResult = 0;

		CRealm *prealm = pinfo->Realm();

		// Always default to nil for safety!
			
		pinfo->m_idLocalDudes[0] = CIdBank::IdNil;
		pinfo->m_idLocalDudes[1] = CIdBank::IdNil;
		pinfo->m_idLocalDudes[2] = CIdBank::IdNil;
		pinfo->m_idLocalDudes[3] = CIdBank::IdNil;

		//------------------------------------------------------------------------------
		// This is for backwards compatibility with VERY OLD realms that used CDude's
		// to determine where dude's started out and what they got.  This is completely
		// obsolete, since we now use CWarp's instead.
		//
		// If there are no CDude's, then we don't do anything.  If there are, we
		// convert them into CWarp's.  The CWarp's get their settings from the first
		// CDude we find, and all subsequent CWarp's use those same settings.
		//
		// After this point, there will be NO DUDE'S, either because there weren't any
		// to start with or because we converted them into warps.
		//------------------------------------------------------------------------------

		CListNode<CThing> *pln = prealm->m_aclassHeads[CThing::CDudeID].m_pnNext;
		CListNode<CThing> *plnTail = &(prealm->m_aclassTails[CThing::CDudeID]);
		CDude *pdude = NULL;
		CDude *pdude2 = NULL;
		CDude *pdude3 = NULL;
		CDude *pdude4 = NULL;		
		CWarp *pwarp;
		bool bFirst = true;

		g_bAllCoopPlayersDead = true;
		g_cLastPlayerToDie = 0;

		//------------------------------------------------------------------------------
		// Here, we warp-in as many dude's as we need.  If there are no warps, it
		// probably means the realm wasn't designed correctecly, and we bail out.
		//------------------------------------------------------------------------------
		if (prealm->m_asClassNumThings[CThing::CWarpID] > 0)
		{
			// Setup warp pointers
			CListNode<CThing> *plnWarpHead = &(prealm->m_aclassHeads[CThing::CWarpID]);
			CListNode<CThing> *plnWarp = plnWarpHead->m_pnNext;
			CListNode<CThing> *plnWarpTail = &(prealm->m_aclassTails[CThing::CWarpID]);
					
			// Use the first warp
			pwarp = (CWarp *)plnWarp->m_powner;
			ASSERT(pwarp != NULL);
			
			// Warp in dude (creates a new dude since the pointer starts out NULL)
			pdude = NULL;
			if (pwarp->WarpIn(&pdude, CWarp::CopyStockPile) == 0)
			{
				// Set general dude stuff
				SetupGeneralDude(pdude, (g_sActivePlayers > PLAYER_1) ? g_cCoopPlayerColours[0] : g_GameSettings.m_sPlayerColorIndex, palevelpersist);
				// Special stuff just for local dudes
				SetupLocalDude(pinfo, pdude, 0);
				pinfo->m_ucNumLocalDudes = 1;
			}
			else
			{
				sResult = -1;
				TRACE("SetupDudes(): pwarp->WarpIn() failed.\n");
			}

			// Player 2				
			if (g_sActivePlayers & PLAYER_2)
			{									
				if (pwarp->WarpIn(&pdude2, CWarp::CopyStockPile) == 0)
				{						
					SetupGeneralDude(pdude2, g_cCoopPlayerColours[1], palevelpersist);						
					SetupLocalDude(pinfo, pdude2, 1);
					pdude2->m_pController = rspGetJoy(1);						
					pinfo->m_ucNumLocalDudes++;
				}
				else
				{
					sResult = -1;
					TRACE("SetupDudes(): pwarp->WarpIn() failed.\n");
				}
			}

			// Player 3
			if (g_sActivePlayers & PLAYER_3)
			{									
				if (pwarp->WarpIn(&pdude3, CWarp::CopyStockPile) == 0)
				{						
					SetupGeneralDude(pdude3, g_cCoopPlayerColours[2], palevelpersist);
					SetupLocalDude(pinfo, pdude3, 2);
					pdude3->m_pController = rspGetJoy(2);											
					pinfo->m_ucNumLocalDudes++;
				}
				else
				{
					sResult = -1;
					TRACE("SetupDudes(): pwarp->WarpIn() failed.\n");
				}
			}

			// Player 4
			if (g_sActivePlayers & PLAYER_4)
			{									
				if (pwarp->WarpIn(&pdude4, CWarp::CopyStockPile) == 0)
				{						
					SetupGeneralDude(pdude4, g_cCoopPlayerColours[3], palevelpersist);
					SetupLocalDude(pinfo, pdude4, 3);
					pdude4->m_pController = rspGetJoy(3);
					pinfo->m_ucNumLocalDudes++;
				}
				else
				{
					sResult = -1;
					TRACE("SetupDudes(): pwarp->WarpIn() failed.\n");
				}
			}		

			g_cPlayerCount = pinfo->m_ucNumLocalDudes;

			if (g_cPlayerCount > 1)
			{
				g_bAllCoopPlayersDead = false;
			}
		}
		else
		{
			TRACE("SetupDudes(): No warps!!!\n");
			sResult = -1;
		}

		return sResult;
	}

	////////////////////////////////////////////////////////////////////////////////
	//
	// Blanks the specified area of the display.
	//
	////////////////////////////////////////////////////////////////////////////////
	void BlankDisplay(	  // Returns nothing.
		int16_t sX,		  // In:  X start position.
		int16_t sY,		  // In:  Y start position.
		int16_t sW,		  // In:  Width.
		int16_t sH,		  // In:  Height
		CPlayInfo *pinfo) // Out: Dimensions to update to the display later.
	{
		// todo: remove
	}

	////////////////////////////////////////////////////////////////////////////////
	//
	// Scale the film to g_GameSettings.m_dGameFilmScale.
	//
	////////////////////////////////////////////////////////////////////////////////
	void ScaleFilm(
		CPlayInfo *pinfo,	 // I/O: Play info
		bool bRedraw = true) // In:  true to clear any newly created dirty areas.
	{
		// Get pointers to camera and grip
		CCamera *pcamera = pinfo->Camera();
		CGrip *pgrip = pinfo->Grip();

		// Remember previous values so we know what portion of the screen needs to be cleared
		int16_t sOldFilmX = pcamera->m_sFilmViewX;
		int16_t sOldFilmY = pcamera->m_sFilmViewY;
		int16_t sOldViewW = pcamera->m_sViewW;
		int16_t sOldViewH = pcamera->m_sViewH;

		// Clamp the scale to fit the valid range
		if (g_GameSettings.m_dGameFilmScale > FILM_MAX_SCALE)
			g_GameSettings.m_dGameFilmScale = FILM_MAX_SCALE;
		if (g_GameSettings.m_dGameFilmScale < FILM_MIN_SCALE)
			g_GameSettings.m_dGameFilmScale = FILM_MIN_SCALE;

		// Scale the actual film.
		int16_t sViewW = g_GameSettings.m_sViewW * g_GameSettings.m_dGameFilmScale;
		int16_t sViewH = g_GameSettings.m_sViewH * g_GameSettings.m_dGameFilmScale;
		int16_t sFilmX = g_GameSettings.m_sFilmX + (g_GameSettings.m_sViewW - sViewW) / 2;
		int16_t sFilmY = g_GameSettings.m_sFilmY + (g_GameSettings.m_sViewH - sViewH) / 2;

		// Update the camera to the new film size.
		pcamera->m_sViewW = sViewW;
		pcamera->m_sViewH = sViewH;
		pcamera->SetFilm(sFilmX, sFilmY);

		// Update the grip to the new film scaling.
		pgrip->SetParms(
			MAX(int16_t(g_GameSettings.m_sGripZoneRadius * g_GameSettings.m_dGameFilmScale), int16_t(MIN_GRIP_ZONE_RADIUS)),
			GRIP_MIN_MOVE_X,
			GRIP_MIN_MOVE_Y,
			GRIP_MAX_MOVE_X,
			GRIP_MAX_MOVE_Y,
			GRIP_ALIGN_X,
			GRIP_ALIGN_Y,
			true);


		if (pinfo->m_ucNumLocalDudes > 1)
		{
			double X = 0.f;
			double Y = 0.f;
			// todo: clean up
			
			for (int8_t index = 0; index < MAX_DUDES; index++)
			{
				CDude *pdudeLocal = pinfo->LocalDudePointer(index);
				if (pdudeLocal)
				{
					X += pdudeLocal->GetX();
					Y += pdudeLocal->GetZ();
				}				
			}

			pgrip->ResetTarget(
				int16_t(rspFastDiv(X, pinfo->m_ucNumLocalDudes)),
				int16_t(rspFastDiv(Y, pinfo->m_ucNumLocalDudes)), 
				30);
		}
		else
		{
			// If local dude exists, reset the grip's targetting
			CDude *pdudeLocal = pinfo->LocalDudePointer();		
			if (pdudeLocal)
				pgrip->ResetTarget(pdudeLocal->GetX(), pdudeLocal->GetZ(), 30);
		}
		
		// Clear any portion of the screen that was revealed by the change in scale
		if (bRedraw == true)
		{
			// Update revealed zones.
			//  ________
			//	|xxxxxxxx|
			//	|xxxxxxxx|
			//	|**|	|++|
			//	|**|__|++|
			//	|--------|
			//	|--------|

			// Top strip.
			//  ________
			//	|xxxxxxxx|
			//	|xxxxxxxx|
			//	|	|	|	|
			//	|	|__|	|
			//	|			|
			//	|________|
			BlankDisplay(sOldFilmX, sOldFilmY, sOldViewW, sFilmY - sOldFilmY, pinfo);

			// Bottom strip.
			//  ________
			//	|			|
			//	|	 __	|
			//	|	|	|	|
			//	|	|__|	|
			//	|xxxxxxxx|
			//	|xxxxxxxx|
			BlankDisplay(sOldFilmX, sFilmY + sViewH, sOldViewW, (sOldFilmY + sOldViewH) - (sFilmY + sViewH), pinfo);

			// Left strip.
			//  ________
			//	|			|
			//	|	 __	|
			//	|xx|	|	|
			//	|xx|__|	|
			//	|			|
			//	|________|
			BlankDisplay(sOldFilmX, sFilmY, sFilmX - sOldFilmX, sViewH, pinfo);

			// Right strip.
			//  ________
			//	|			|
			//	|	 __	|
			//	|	|	|xx|
			//	|	|__|xx|
			//	|			|
			//	|________|
			BlankDisplay(sFilmX + sViewW, sFilmY, (sOldFilmX + sOldViewW) - (sFilmX + sViewW), sViewH, pinfo);
		}

		// Save new settings so we'll know when they change
		m_dCurrentFilmScale = g_GameSettings.m_dGameFilmScale;
		m_sCurrentGripZoneRadius = g_GameSettings.m_sGripZoneRadius;
	}
};

////////////////////////////////////////////////////////////////////////////////
//
// Score play module
//
////////////////////////////////////////////////////////////////////////////////
class CPlayScore : public CPlay
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayScore(void)
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	virtual ~CPlayScore()
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Prepare game
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	int16_t PrepareGame(  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		// Init and reset score module
		ScoreInit();
		ScoreReset();
		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	int16_t StartRealm(	  // Returns 0 if successfull, non-zero otherwise
		CPlayInfo *pinfo) // I/O: Play info
	{
		if (!pinfo->m_bBadRealmMP)
		{
			// Reset the display timer
			ScoreResetDisplay();

			// Set the scoring type
			if (pinfo->IsMP())
			{
				ScoreSetMode(CScoreboard::MultiPlayer);
				if (pinfo->Realm()->m_ScoringMode == 0)
					pinfo->Realm()->m_ScoringMode = CRealm::MPFrag;
			}
			else if (g_sActivePlayers > PLAYER_1)
			{
				pinfo->Realm()->m_ScoringMode = CRealm::MPCampaignCoop;
				ScoreReset();
				ScoreSetMode(CScoreboard::Coop);
			}
			else
			{
				ScoreSetMode(CScoreboard::SinglePlayer);
			}
		}

		return 0;
	}

	////////////////////////////////////////////////////////////////////////////////
	// End realm
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void EndRealm(
		CPlayInfo *pinfo) // I/O: Play info
	{
		if (!pinfo->m_bBadRealmMP)
		{
			// todo???
		}
	}
};

////////////////////////////////////////////////////////////////////////////////
//
// Base class for all "Play Modules"
//
////////////////////////////////////////////////////////////////////////////////
class CPlayCutscene : public CPlay
{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
private:
	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
private:
	bool m_bSimple;

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
public:
	////////////////////////////////////////////////////////////////////////////////
	// Constructor
	////////////////////////////////////////////////////////////////////////////////
	CPlayCutscene(void)
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Destructor
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	~CPlayCutscene()
	{
	}

	////////////////////////////////////////////////////////////////////////////////
	// Start cutscene
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void StartCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
		// Clear input events (don't let user press anything before the cutscene appears)
		//	rspClearAllInputEvents();

		// For demo and specific file modes, use simple cutscenes.  Otherwise, use real cutscenes.
		m_bSimple = ((GetInputMode() != INPUT_MODE_LIVE) || (pinfo->RealmNum() < 0)) ? true : false;

		// If this is the spawn version, it only has 1 cutscene image, so it should use
		// simple mode to display that one.
#ifdef SPAWN
		m_bSimple = true;
#endif

		// Special case for the last level demo
		if (g_bLastLevelDemo)
			m_bSimple = false;

		// Start cutscene
		RString strSection;
		RString strEntry;

		Play_GetRealmSectionAndEntry(pinfo->IsMP(), pinfo->CoopLevels(), pinfo->Gauntlet(), pinfo->AddOn(), pinfo->RealmNum(), pinfo->Realm()->m_flags.sDifficulty, &strSection, &strEntry);

		CutSceneStart(m_bSimple, &strSection, &strEntry, 24, 24);
	}

	////////////////////////////////////////////////////////////////////////////////
	// Do cutscene
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void DoCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
		// If this is NOT simple and NOT multiplayer, do the effect while waiting for user input
		if (!m_bSimple && !pinfo->IsMP())
		{
			// Configure cutscene effect
			CutSceneConfig(
				3600,
				-24, 24, 10000L,
				-24, 24, 10000L,
				0.6, 0.6, 4000L,
				0, 0, 640, 480);

			// Insert effects into this loop!
			RInputEvent ie;
			ie.type = RInputEvent::None;

			//todo?
			//	rspClearAllInputEvents();

			while (rspGetQuitStatus() == 0)
			{
				CutSceneUpdate();
				UpdateSystem();
				
				if (IsMapleInputButtonPressed() 
					|| IsMapleInputButtonPressed(1)
					|| IsMapleInputButtonPressed(2)
					|| IsMapleInputButtonPressed(3))
					break;
			}
		}
	}

	////////////////////////////////////////////////////////////////////////////////
	// End cutscene
	////////////////////////////////////////////////////////////////////////////////
	/* virtual */
	void EndCutscene(
		CPlayInfo *pinfo) // I/O: Play info
	{
		// End cutscene
		CutSceneEnd();

		// A quick delay while on the black screen looks better than no delay
		int32_t lBlackTime = rspGetMilliseconds();
		while (rspGetMilliseconds() - lBlackTime < BLACK_HOLD_TIME);			
	}
};
 
////////////////////////////////////////////////////////////////////////////////
//
// Abort all currently playing sounds and do not return until they are gone
// unless timed out for safety.
//
////////////////////////////////////////////////////////////////////////////////
inline void SynchronousSampleAbortion(void)
{
	// Stop all currently playing samples abruptly.
	AbortAllSamples();

	// We should never need a timeout but I don't want to risk a Muppets
	// scenario where a shitty sound driver causes us to think a sound is always
	// playing.
	// Wait for all samples to finish.
	int32_t lTimeOutTime = rspGetMilliseconds() + TIME_OUT_FOR_ABORT_SOUNDS;
	// Wait for them to stop.
	while (IsSamplePlaying() == true && rspGetMilliseconds() < lTimeOutTime)
	{
		// Always do periodic updates.
		// Crucial to sound completing.
		UpdateSystem();
	}
}

////////////////////////////////////////////////////////////////////////////////
//
// Play game using specified settings.
//
////////////////////////////////////////////////////////////////////////////////
extern int16_t Play(				// Returns 0 if successfull, non-zero otherwise	
	INPUT_MODE inputMode,			// In:  Input mode
	const int16_t sRealmNum,		// In:  Realm number to start on or -1 to use specified realm file
	const char *pszRealmFile,		// In:  Realm file to play (ignored if sRealmNum >= 0)
	const bool bJustOneRealm,		// In:  Play just this one realm (ignored if sRealmNum < 0)
	const bool bGauntlet,			// In:  Play challenge levels gauntlet - as selected on menu
	const int16_t bAddOn,			// In:  Play add on levels
	const int8_t sDifficulty,		// In:  Difficulty level
	const bool bRejuvenate,			// In:  Whether to allow players to rejuvenate (MP only)
	const int16_t sTimeLimit,		// In:  Time limit for MP games (0 or negative if none)
	const int16_t sKillLimit,		// In:  Kill limit for MP games (0 or negative if none)
	const int16_t sCoopLevels,		// In:  Zero for deathmatch levels, non-zero for cooperative levels.
	const int16_t sCoopMode,		// In:  Zero for deathmatch mode, non-zero for cooperative mode.
	const int16_t sFrameTime,		// In:  Milliseconds per frame (MP only)
	RFile *pfileDemoModeDebugMovie) // In:  File for loading/saving demo mode debug movie
{
	int16_t sResult = 0;

	//#ifdef MOBILE
	if (inputMode == INPUT_MODE_PLAYBACK)
		demoCompat = true; //DEMO playback
	else
		demoCompat = false;
	//#endif

	// todo input?
	// Clear any events that might be in the queue	
	// rspClearAllInputEvents();

	// Set input mode
	SetInputMode(inputMode);

	// Reset AI logging feature to avoid potential multiplayer/demo sync problems
	CPerson::ResetLogAI();

	// Reseed random number generator to keep multiplayer/demo games sync'ed.
	SeedRand(1);

	// Create all the play modules	
	CPlayStatus playStatus;
	CPlayRealm playRealm;
	CPlayInput playInput;
	CPlayCutscene playCutscene;
	CPlayScore playScore;

	// Create play group and add all the modules to it
	CPlayGroup playgroup;	
	playgroup.AddModule(&playStatus);
	playgroup.AddModule(&playRealm);
	playgroup.AddModule(&playInput);
	playgroup.AddModule(&playScore);
	playgroup.AddModule(&playCutscene);

	// Create and fill in play info
	CPlayInfo *info = new CPlayInfo;	
	info->m_sRealmNum = sRealmNum;
	info->m_bGauntlet = bGauntlet;
	info->m_bAddOn = bAddOn;
	info->m_sFrameTime = sFrameTime;
	info->m_sCoopLevels = sCoopLevels;

	info->Realm()->m_flags.bCoopMode = sCoopMode ? true : false;
	info->Realm()->m_flags.sDifficulty = sDifficulty; // MUST be set before Play_GetRealmInfo() calls.

	if (info->m_sRealmNum < 0)
	{
		strncpy(info->m_szRealm, pszRealmFile, sizeof(info->m_szRealm));
		info->m_szRealm[sizeof(info->m_szRealm) - 1] = 0;
		info->m_bJustOneRealm = true;
	}
	else
	{
		if (Play_GetRealmInfo(info->IsMP(), info->CoopLevels(), info->Gauntlet(), info->AddOn(), info->m_sRealmNum, info->Realm()->m_flags.sDifficulty, info->m_szRealm, sizeof(info->m_szRealm)) == 0)
		{
			info->m_bJustOneRealm = bJustOneRealm;
		}
		else
		{
			// 09/12/97 MJR - Clear the string.  The CPlayInfo constructor actually does this, but this
			// makes it more obvious.
			info->m_szRealm[0] = 0;
			sResult = -1;
			TRACE("Play(): Couldn't get info for realm #%hd!\n", (int16_t)sRealmNum);
		}
	}
	info->m_pfileDemoModeDebugMovie = pfileDemoModeDebugMovie;

	// 09/12/97 MJR - Here exists yet another error in the release version, but thankfully,
	// it works out okay.  Note how completely ignore the value in sResult and simply
	// overwrite it with the return value from PrepareGame().  This should, in general,
	// fail further along the way when we try to load this realm.

	// Open the realm prefs file
	RPrefs prefsRealm;	
	sResult = prefsRealm.Open(FullPathCD(g_GameSettings.m_pszRealmPrefsFile), "rt");
	g_GameSettings.m_bLimitedSampleTesters = true;

	if (sResult == 0)
	{

		int16_t sNumLevels;
#if TARGET == POSTAL_2015
		if (bAddOn == 3)
		{
			prefsRealm.GetVal("Info", "NumAllSinglePlayerLevels", 22, &sNumLevels);
		}
		else
		{
#endif
			prefsRealm.GetVal("Info", "NumSinglePlayerLevels", 16, &sNumLevels);
#if TARGET == POSTAL_2015
		}
#endif
		
		prefsRealm.Close();

		g_pvrRenderer->FlushBatchedDrawQueues();

		// Prepare game
		sResult = playgroup.PrepareGame(info);

		if (!sResult)
		{

			// Wait until game is ready
			bool bGameReady = false;
			do
			{
				sResult = playgroup.IsGameReady(info, &bGameReady);
			} while (!sResult && !bGameReady);

			if (!sResult && bGameReady)
			{
				// Start game
				sResult = playgroup.StartGame(info);

				if (sResult == 0)
				{
					/*** 12/5/97 AJC ***/
					// Outer loop keeps playing one realm after another
					do
					{
						int32_t startRealmMS = -1;

						// Clear game status
						info->SetGameState_Ok();
					
						// Sounds playing during the load suck.
						SynchronousSampleAbortion();
						
						// enable this level in the level select
						if (info->m_sRealmNum >= 0 && info->m_bGauntlet == false && g_sActivePlayers == PLAYER_1)
						{
							int16_t unlockLevel = info->m_sRealmNum;
							//Unlock right addon levels
							if (bAddOn == 1)
								unlockLevel += 16;
							if (bAddOn == 2)
								unlockLevel += 20;

							g_GameSettings.m_ulUnlockedLevels |= 1 << unlockLevel;
						}
					
						// Get realm exit X/Y.
						// todo: Move this into startcutscene?
						prefsRealm.Open(FullPathCD(g_GameSettings.m_pszRealmPrefsFile), "rt");
						RString strSection;
						RString strEntry;
						int16_t sExitX;
						int16_t sExitY;
						int16_t sPatchX[3];
						int16_t sPatchY[3];

						CHood *pHood = info->Realm()->m_phood;
						if (pHood && pHood->m_pTextureAmbient)
						{						
							delete pHood->m_pTextureAmbient;
							pHood->m_pTextureAmbient = nullptr;
						}	
						
						Play_GetRealmSectionAndEntry(info->IsMP(), info->CoopLevels(), info->Gauntlet(), info->AddOn(), info->m_sRealmNum, info->Realm()->m_flags.sDifficulty, &strSection, &strEntry);
						prefsRealm.GetVal(strSection, "ExitX", 0, &sExitX);
						prefsRealm.GetVal(strSection, "ExitY", 0, &sExitY);								

						prefsRealm.GetVal(strSection, "Patch1X", 0, &sPatchX[0]);
						prefsRealm.GetVal(strSection, "Patch1Y", 0, &sPatchY[0]);
						prefsRealm.GetVal(strSection, "Patch2X", 0, &sPatchX[1]);
						prefsRealm.GetVal(strSection, "Patch2Y", 0, &sPatchY[1]);
						prefsRealm.GetVal(strSection, "Patch3X", 0, &sPatchX[2]);
						prefsRealm.GetVal(strSection, "Patch3Y", 0, &sPatchY[2]);
						
						prefsRealm.Close();
							
						// Start the cutscene
						playgroup.StartCutscene(info);

						// Prepare realm												
						sResult = playgroup.PrepareRealm(info);					
						
						info->Realm()->m_sExitX = sExitX;
						info->Realm()->m_sExitY = sExitY;
											
						if (!sResult)
						{
							// Wait until realm is ready
							bool bRealmReady = false;
							do
							{
								sResult = playgroup.IsRealmReady(info, &bRealmReady);
							} while (!sResult && !bRealmReady);
					
							if (!sResult && bRealmReady)
							{							
								g_bHighScoreUpdated = false;
								g_bSaveHighScore = false;	
								g_sPlayerScorePos = -1;
								// do the cutscene
								g_pvrRenderer->GetQueue(PVR::cRenderMgr::LayerBg)->Unlock();
								g_pvrRenderer->ClearScreen(true);
								playgroup.DoCutscene(info);																
															
								// End the cutscene
								playgroup.EndCutscene(info);
								pHood = info->Realm()->m_phood;
								pHood->SetupAmbientTexture();
							
								CSprite2 *patchSprites[3];
								patchSprites[0] = pHood->backgroundPatchSprite1;
								patchSprites[1] = pHood->backgroundPatchSprite2;
								patchSprites[2] = pHood->backgroundPatchSprite3;

								for (int8_t index = 0; index < 3; index++)
								{									
									CSprite2 *patchSprite = patchSprites[index];																												 
									if (patchSprite)
									{												
										patchSprite->m_sX2 = sPatchX[index];
										patchSprite->m_sY2 = sPatchY[index];											 
									}
								} 

								// Start realm
								sResult = playgroup.StartRealm(info);
								pvr_set_pal_entry(RSP_WHITE_INDEX, PACK_ARGB8888(0, 0, 0, 0));

								if (sResult == 0)
								{

									// Init local input
									ClearLocalInput();

									// Set the resource managers to trace uncached loads
									g_resmgrGame.TraceUncachedLoads(true);
									g_resmgrSamples.TraceUncachedLoads(true);
									g_resmgrRes.TraceUncachedLoads(true);

									// Start the music:
									if (g_bLastLevelDemo)
									{										
										// Begin Final Scene Music:										
										PlaySample(
											g_smidFinalScene,
											SampleMaster::Unspecified,
											255,
											&g_siFinalScene,
											NULL,
											0,
											0,
											true);
									}
								
									g_pvrRenderer->FlushBatchedDrawQueues();

									startRealmMS = rspGetMilliseconds();
									// Inner loop plays current realm until it's done
									RInputEvent ie;																							
									uint16_t frameTime = 1;

									#ifdef MSTATS
									RPrint print;
									// Create text layer
									print.SetFont(28, &g_fontPostal);
									print.SetColorRGB(192, 0, 0);									
									print.SetEffectAbs(RPrint::SHADOW_X, 1);
									print.SetEffectAbs(RPrint::SHADOW_Y, 1);
									print.SetWordWrap(TRUE);
									print.SetColumn(10,180, 300, 200);		
									print.offsetX = 0;	
									print.offsetY = 0;											
									#endif

									#ifdef MSTATS
									int skipped = 0;
									#endif
						
									info->m_bInMenu = false;
									info->m_bInHighScoreTable = false;
									info->m_iMenuInTransition = 0;
									g_bKillLocalDudes = false;
									CRealm *pRealm = info->Realm();

									do
									{
										int32_t lTime = rspGetMilliseconds();																	
								
										if (g_bResetCamera)
										{
											playRealm.ResetCamera(info);
											playStatus.ResetCamera();
											//info->Realm()->ResetCamera();
											g_bResetCamera = false;
										}

										// User input
										ie.type = RInputEvent::None;
										rspGetNextInputEvent(&ie);

										// Update
										//We get local input here
										playgroup.CoreLoopUpdate(info);

										// Update chained AICA sound buffers.
										g_AicaSounds->PollActiveSounds();

										// Begin sending vertex data to PVR.										
										bool canDraw = g_pvrRenderer->BeginScene(info->m_bDFrawFrameToTexture);																				

										if (canDraw)
										{																						
											g_pvrRenderer->BeginDirectDrawMode(PVR::cRenderMgr::DirectOpaque);																					
										}
										#ifdef MSTATS
										else
										{
											skipped++;
										}
										#endif
										 
										//This is where we assign local input										
										playgroup.CoreLoopUserInput(info, &ie);																				
										
										// The PVR transparent list is opened in here.
										playgroup.CoreLoopRender(info, canDraw);
										
										if (canDraw)
										{
											playgroup.CoreLoopRenderOnTop(info);																			
										}
							
										#ifdef MSTATS																													
										if (canDraw)
										{
											char gg[100];																					
											//sprintf(gg, "Frame Time: %d\nMesh Tris: %d\nSkipped frames: %d", frameTime, g_pvrRenderer->numtris, skipped);
											//sprintf(gg, "Frame Time: %d\nMesh Tris: %d\nRam: %ld\n%d", frameTime, g_pvrRenderer->numtris, mstats(false), g_pvrRenderer->m_cCurrentFrameTokenLevel);											
											sprintf(gg, "Frame Time: %d\nRam: %ld\nVerts: %ld\n%d", frameTime, mstats(false), g_pvrRenderer->m_lNumVerts, g_pvrRenderer->m_cCurrentFrameTokenLevel);											
											print.printPVR(0, 0, gg);
											skipped = 0;
										}
										#endif

										// If we're suspended then set global colour to red.
										if (pRealm->IsSuspended())
										{											
											if (!info->m_bInMenu && !info->m_bInHighScoreTable && !info->m_iMenuInTransition)
											{
												#ifdef GPROF
												profiler_stop();
												profiler_clean_up();
												#endif
												StartMenuTrans(750);
												info->m_iMenuInTransition = 1;
											}

											if (DoPreMenuTrans() && info->m_iMenuInTransition == 1 && !info->m_bInMenu && !info->m_bInHighScoreTable)
											{											
												if (pRealm->IsEndOfLevelGoalMet(g_bEndLevelKey))
												{
													info->m_bInHighScoreTable = true;
													AbortAllSamples();
												}												
												else
												{
													Menu *pMenu = nullptr;
													

													if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Disconnected)
													pMenu = &menuMapleDisconnect;

													for (int8_t index = 0; index < MAX_DUDES; index++)
													{													
														cMapleController *pController ;	
														int8_t cStatus = rspGetJoy(index)->Status();
														if (g_sActivePlayers & g_sPlayerFlags[index] && cStatus == MAPLE::cMapleController::ControllerDisconnected)
														{
															pMenu = &menuMapleDisconnect;
														}
													}

													if (!pMenu)
													{
														pMenu = &menuClientGame;
													}

													playInput.StartMenu(info, pMenu);													
												}
												
												info->m_iMenuInTransition = 2;
											}										

											if (info->m_bInMenu || info->m_bInHighScoreTable)
											{
												float r = g_pvrRenderer->m_fGlobalRScale;
												float g = g_pvrRenderer->m_fGlobalGScale;
												float b = g_pvrRenderer->m_fGlobalBScale;
												g_pvrRenderer->m_fGlobalRScale = 1.0f;
												g_pvrRenderer->m_fGlobalGScale = 1.0f;
												g_pvrRenderer->m_fGlobalBScale = 1.0f;
												//g_pvrRenderer->EndImmediate();

												//g_pvrRenderer->BeginImmediate(PVR_LIST_PT_POLY);																								
												if (canDraw && info->m_bInHighScoreTable)
												{
													ScoreDisplayHighScores(pRealm, info->m_sRealmNum);
												}
												else if (canDraw)
												{
													DoMenuOutput();
												}

												g_pvrRenderer->m_fGlobalRScale = r;
												g_pvrRenderer->m_fGlobalGScale = g;
												g_pvrRenderer->m_fGlobalBScale = b;
											}

											// todo: magin numbers
											if (!info->m_bInMenu && !info->m_bInHighScoreTable && info->m_iMenuInTransition == 2)
											{
												if (DoPostMenuTrans() == true)
												{
													EndMenuTrans(true);
													if (!info->IsMP())
														pRealm->Resume();
													info->m_iMenuInTransition = 0;													

													if (g_bKillLocalDudes)
													{
														info->KillLocalDudes();	
														g_bKillLocalDudes = false;
													}

													if (pRealm->IsEndOfLevelGoalMet(g_bEndLevelKey))
													{									
														info->SetGameState_NextRealm();
													}													
												}
											}
										}
									
										// Finish sending data to the PVR and draw to the screen.
										if (canDraw)
										{												
											g_pvrRenderer->DrawGuiText();											
											g_pvrRenderer->EndScene();
										}
											
										// We only draw the very last frame of the ending demo sequence to a texture.										
										if(info->m_bDFrawFrameToTexture && g_pvrRenderer->renderTarget != nullptr)
										{
											// Game be done now.											
											info->SetGameState_GameOver();
										}
										
										frameTime = rspGetMilliseconds() - g_pvrRenderer->m_lLastFrameStart;
																				
										if (canDraw)
										{
											g_pvrRenderer->m_lNumPrevVerts = g_pvrRenderer->m_lNumVerts;
										}		
																													
										// todo: should this be here?										
										g_pvrRenderer->m_lNumVerts = 0;
																																									
										if (!g_bLastLevelDemo)
										{				
											// Keep the game pace.

											// Enable frameskip
											if (frameTime > (rspIs50Hz() ? 20 : 17) && !info->m_bInMenu && !info->m_bInHighScoreTable)
											{													
												g_pvrRenderer->m_bFrameSkip = true;
											}

											if (g_pvrRenderer->m_bFrameSkip)
											{					
												int8_t cSkipVal = (rspIs50Hz() ? 19 : 16);
												const Uint32 now = rspGetMilliseconds();
												const Uint32 elapsed = (now - g_pvrRenderer->m_lLastFrameTicks);
												// todo: 50hz stuff?
												if (now - lTime < cSkipVal || info->m_bInMenu || info->m_bInHighScoreTable)
												{																																						
													if (g_pvrRenderer->m_lLastFrameTicks && (g_pvrRenderer->m_lLastFrameTicks <= now))
													{																																								
														if (elapsed < cSkipVal)
														{																	
															timer_spin_sleep(cSkipVal - elapsed); // try to get closer to 60fps.			
														}
													}													
												
												}
												
												g_pvrRenderer->m_lLastFrameTicks = rspGetMilliseconds();																				
											}																																																		
										}			

										g_pvrRenderer->m_pGuiTextString->Flush();																
										// Check if core loop is done
									} while (!playgroup.IsCoreLoopDone(info));
							
									// Set the resource managers to trace uncached loads
									g_resmgrGame.TraceUncachedLoads(false);
									g_resmgrSamples.TraceUncachedLoads(false);
									g_resmgrRes.TraceUncachedLoads(false);

									// If this was the last demo level, then do the martini effect
									if (g_bLastLevelDemo)
									{																																									
										MartiniDo(
											0,
											0,												  
											15000,
											24,
											5000,
											9000,												  
											5000,
											g_siFinalScene // to dim out...
										);

										// End the sound:
										if (g_siFinalScene)
										{
											// Cut it off.
											AbortSample(g_siFinalScene);
											// Clear.
											g_siFinalScene = 0;
											// Play final sample that completes the cut off sound. ***

											if (g_pvrRenderer->renderTarget != nullptr)
											{												
												delete g_pvrRenderer->renderTarget;
												g_pvrRenderer->renderTarget = nullptr;												
											}											
										}
										
										TRACE("Stop here before clearing screen");
									}

									// *** MP Score display ///////////////
									if (info->IsMP() && !rspGetQuitStatus() && !info->IsGameAborted())
									{
										// Display the high scores.  Currently, this is MODAL (but has a timeout)!
									//todo ?									
										//ScoreDisplayHighScores(info->Realm(), info->Client(), MP_HIGH_SCORES_MAX_TIME);
									}

									// End realm																		
									playgroup.EndRealm(info);									
								}
								else
									playgroup.StartRealmErr(info);
							}
							else
							{								
								playgroup.IsRealmReadyErr(info);
							}
						}
						else
						{							
							playgroup.PrepareRealmErr(info);
						}
									
						const int32_t endRealmMS = rspGetMilliseconds();
						const int32_t timePlayedMS = ((startRealmMS > 0) && (endRealmMS > 0) && (endRealmMS > startRealmMS)) ? endRealmMS - startRealmMS : -1;
						const int32_t newPlaythroughMS = playthroughMS + timePlayedMS;

						if (!g_bLastLevelDemo) // don't charge the last level demo to playthroughMS.
							playthroughMS = ((playthroughMS < 0) || (timePlayedMS < 0) || (newPlaythroughMS < 0)) ? -1 : newPlaythroughMS;

						// End the cutscene.  It normally gets called above, but if an error
						// occurs it doesn't, so this is the backup.  Multiple calls are safe.

						playgroup.EndCutscene(info);
	
						// Figure out what to do next (same realm, next realm, game over, etc.)
						if (!sResult)
						{ 
							// Save challenge high scores
							if (g_bSaveHighScore)
							{
								if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Connected)
								{
									RPrint print;								
									print.SetFont(30, &g_fontPostal);
									print.SetColorRGB(192, 0, 0);
									print.SetEffectAbs(RPrint::SHADOW_X, 1);
									print.SetEffectAbs(RPrint::SHADOW_Y, 1);
									print.SetWordWrap(TRUE);
									g_pvrRenderer->FlushBatchedDrawQueues();									
									int16_t sOffsetX = 0;
				switch (g_GameSettings.m_cActiveLanguage)
				{
					case 0:
						sOffsetX = 260;
					break;
					case 1:
						sOffsetX = 235;
					break;
					case 2:
						sOffsetX = 155;
					break;
					case 3:
						sOffsetX = 175;
					break;
					case 4:
						sOffsetX = 175;
					break;
					case 5:
						sOffsetX = 215;
					break;
				}
				print.SetColumn(sOffsetX, (g_GameSettings.m_cCutSceneUnderscan ? 390 : 420), 410, 50);
									print.printPVR(0, 0, g_pszSaving[g_GameSettings.m_cActiveLanguage]);																																								
									g_pvrRenderer->DrawGuiText();
									g_pvrRenderer->RenderBatchedFrame();
									if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Connected)
									{
										Game_SavePlayerHighScores(MAPLE::cMapleVmu::m_pActiveVmu, g_sSaveGameSlot, MAPLE::cMapleVmu::m_pszSaveAppName);
									}

									g_pvrRenderer->FlushBatchedDrawQueues();
									g_pvrRenderer->m_pGuiTextString->Flush();
								}
							}

							if (!info->IsRestartingRealm())
							{
								info->m_prealm->m_flags.bPurgeResOnShutdown = true;
								AbortAllSamples();

								g_resmgrGame.FreeAllResources();
								g_resmgrGame.CloseSak();
								g_resmgrGame.m_map.erase(g_resmgrGame.m_map.begin(), g_resmgrGame.m_map.end());
								g_resmgrGame.m_ptrMap.erase(g_resmgrGame.m_ptrMap.begin(), g_resmgrGame.m_ptrMap.end());
								g_resmgrGame.m_duplicateSet.erase(g_resmgrGame.m_duplicateSet.begin(), g_resmgrGame.m_duplicateSet.end());
								g_resmgrGame.m_accessList.erase(g_resmgrGame.m_accessList.begin(), g_resmgrGame.m_accessList.end());
								g_resmgrGame.m_LoadList.erase(g_resmgrGame.m_LoadList.begin(), g_resmgrGame.m_LoadList.end());
								g_resmgrGame.m_DirectoryMap.erase(g_resmgrGame.m_DirectoryMap.begin(), g_resmgrGame.m_DirectoryMap.end());
								g_resmgrGame.m_SakDirectory.erase(g_resmgrGame.m_SakDirectory.begin(), g_resmgrGame.m_SakDirectory.end());
								g_resmgrGame.m_SakDirOffset.erase(g_resmgrGame.m_SakDirOffset.begin(), g_resmgrGame.m_SakDirOffset.end());
								g_resmgrGame.OpenSak(FullPath(GAME_PATH_GAME, "res/game/game.sak"));

								// Keep audio sak open for end credits.
								if (!g_bLastLevelDemo)
								{
									g_resmgrSamples.FreeAllResources();
									g_resmgrSamples.CloseSak();
									g_resmgrSamples.m_map.erase(g_resmgrSamples.m_map.begin(), g_resmgrSamples.m_map.end());
									g_resmgrSamples.m_ptrMap.erase(g_resmgrSamples.m_ptrMap.begin(), g_resmgrSamples.m_ptrMap.end());
									g_resmgrSamples.m_duplicateSet.erase(g_resmgrSamples.m_duplicateSet.begin(), g_resmgrSamples.m_duplicateSet.end());
									g_resmgrSamples.m_accessList.erase(g_resmgrSamples.m_accessList.begin(), g_resmgrSamples.m_accessList.end());
									g_resmgrSamples.m_LoadList.erase(g_resmgrSamples.m_LoadList.begin(), g_resmgrSamples.m_LoadList.end());
									g_resmgrSamples.m_DirectoryMap.erase(g_resmgrSamples.m_DirectoryMap.begin(), g_resmgrSamples.m_DirectoryMap.end());
									g_resmgrSamples.m_SakDirectory.erase(g_resmgrSamples.m_SakDirectory.begin(), g_resmgrSamples.m_SakDirectory.end());
									g_resmgrSamples.m_SakDirOffset.erase(g_resmgrSamples.m_SakDirOffset.begin(), g_resmgrSamples.m_SakDirOffset.end());
									g_resmgrSamples.SetBasePath("/");
								}
							}

							if (info->JustOneRealm() == true && info->IsRestartingRealm() == false)
							{
								info->SetGameState_GameOver();
							}
							else if (info->IsNextRealm())
							{
								info->m_sRealmNum++;

								// Update global realm number so the "save game" mechanism knows what realm we're on
								g_sRealmNumToSave = info->m_sRealmNum;

								RPrint print;								
								print.SetFont(30, &g_fontPostal);
								print.SetColorRGB(192, 0, 0);
								print.SetEffectAbs(RPrint::SHADOW_X, 1);
								print.SetEffectAbs(RPrint::SHADOW_Y, 1);
								print.SetWordWrap(TRUE);
								
								if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Connected)
								{	
									int16_t sOffsetX = 0;
									switch (g_GameSettings.m_cActiveLanguage)
									{
										case 0:
											sOffsetX = 260;
										break;
										case 1:
											sOffsetX = 235;
										break;
										case 2:
											sOffsetX = 155;
										break;
										case 3:
											sOffsetX = 175;
										break;
										case 4:
											sOffsetX = 175;
										break;
										case 5:
											sOffsetX = 215;
										break;
									}
									print.SetColumn(sOffsetX, (g_GameSettings.m_cCutSceneUnderscan ? 390 : 420), 410, 50);
									print.printPVR(0, 0, g_pszSaving[g_GameSettings.m_cActiveLanguage]);
								}
								
								switch (Play_GetRealmInfo(info->IsMP(), info->CoopLevels(), info->Gauntlet(), info->AddOn(), info->m_sRealmNum, info->Realm()->m_flags.sDifficulty, info->m_szRealm, sizeof(info->m_szRealm)))
								{
								case 0: // Got info

									if (!bGauntlet) //Dont autosave if playing a challenge
									{										
										for (int8_t cPlayerId = 0; cPlayerId < MAX_DUDES; cPlayerId++)
										{
											if (g_sActivePlayers & g_sPlayerFlags[cPlayerId])
											{																
												g_stockpile[cPlayerId].Copy(&(playRealm.m_alevelpersist[cPlayerId].stockpile));
											}
										}
																														
										g_pvrRenderer->FlushBatchedDrawQueues();																														
										g_pvrRenderer->DrawGuiText();
										g_pvrRenderer->RenderBatchedFrame();

										if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Connected)
										{
											Game_SavePlayersGame(MAPLE::cMapleVmu::m_pActiveVmu, g_sSaveGameSlot, MAPLE::cMapleVmu::m_pszSaveAppName, info->Realm()->m_flags.sDifficulty);
										}

										g_pvrRenderer->FlushBatchedDrawQueues();
										g_pvrRenderer->m_pGuiTextString->Flush();
									}									

									break;

								case 1: // No such realm number
										// This is a bit weird, but it works!  If the player has reached the
										// last level, either the game is over or the player has won the game,
										// depending on what mode we're in.  If the game is over, we just set
										// the appropriate game state.  If the player won the game, we set a
										// special flag and allow the loop we're in to continue, even though
										// there is no such realm (that's how we get to this point).  Other
										// special-case code handles everything that happens after that to do
										// the actual ending scene for the game.
										if (!info->Gauntlet() && !info->JustOneRealm() && info->RealmNum() == sNumLevels)
										{
											g_bLastLevelDemo = true;	
											g_pvrRenderer->FlushBatchedDrawQueues();																														
											g_pvrRenderer->DrawGuiText();
											g_pvrRenderer->RenderBatchedFrame();
											if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Connected && !sCoopMode)
											{
												Game_SavePlayersGame(MAPLE::cMapleVmu::m_pActiveVmu, g_sSaveGameSlot, MAPLE::cMapleVmu::m_pszSaveAppName, info->Realm()->m_flags.sDifficulty, FLAG_SAVE_UNLOCKED_LEVELS);
											}
											g_pvrRenderer->FlushBatchedDrawQueues();
											g_pvrRenderer->m_pGuiTextString->Flush();
										}
										else
										{										
											g_pvrRenderer->FlushBatchedDrawQueues();																														
											g_pvrRenderer->DrawGuiText();
											g_pvrRenderer->RenderBatchedFrame();	
											info->SetGameState_GameOver();
											if (MAPLE::cMapleVmu::m_pActiveVmu != nullptr && MAPLE::cMapleVmu::m_pActiveVmu->Status() == MAPLE::cMapleModule::Connected && !sCoopMode)
											{											
												Game_SavePlayersGame(MAPLE::cMapleVmu::m_pActiveVmu, g_sSaveGameSlot, MAPLE::cMapleVmu::m_pszSaveAppName, info->Realm()->m_flags.sDifficulty, FLAG_SAVE_UNLOCKED_LEVELS);
											}
											g_pvrRenderer->FlushBatchedDrawQueues();
											g_pvrRenderer->m_pGuiTextString->Flush();
										}
																				
									break;

								default: // Error
									// 09/12/97 MJR - In MP, we don't want to exit the loop if this happens.  Instead,
									// we set the bad realm flag and let the core loop handle the abort process.
									if (info->IsMP())
										info->m_bBadRealmMP = true;
									else
										sResult = -1;
									TRACE("Play(): Couldn't get info for realm #%hd!\n", (int16_t)info->m_sRealmNum);
									break;
								}
							}
						}
					} while (!sResult && !info->IsGameOver() && !g_bLastLevelDemo);

					/*** 12/5/97 AJC ***/
#ifdef WIN32
					if (info->IsMP())
						CloseLogFile();
#endif
					/*** 12/5/97 AJC ***/
				}
				else
					playgroup.StartGameErr(info);
			}
			else
				playgroup.IsGameReadyErr(info);
		}
		else
			playgroup.PrepareGameErr(info);

		// Unprepare game
		playgroup.UnprepareGame(info);
	}

	rspUpdateDisplay();

	// Abort all playing sounds.
	SynchronousSampleAbortion();

	// Clear any events that might be in the queue
	// todo?
	//	rspClearAllInputEvents();
	delete info;
	return sResult;
}

////////////////////////////////////////////////////////////////////////////////
//
// Snap picture to disk.
//
////////////////////////////////////////////////////////////////////////////////
extern void Play_SnapPicture(void)
{
}

////////////////////////////////////////////////////////////////////////////////
//
// Callback from g_menuVerifyQuitGame with choice.
//
////////////////////////////////////////////////////////////////////////////////
extern bool Play_VerifyQuitMenuChoice( // Returns true to accept, false to deny choice.
	Menu *pmenuCurrent,				   // In:  Current menu.
	int16_t sMenuItem,				   // In:  Item chosen or -1 for change of focus.
	int16_t sMenuCurr)
{
	bool bAcceptChoice = true; // Assume accepting choice.

	switch (sMenuItem)
	{
	case 0: // Continue.
		ms_menuaction = MenuActionEndMenu;
		break;	
	case 1: // Options.
		break;
	case 2: // Restart.
		ms_menuaction = MenuActionRestart;
		break;
	case 3: // Quit.
		ms_menuaction = MenuActionQuit;
		break;
#ifdef MOBILE
	case 10: // Menu cancelled, set in menus_android.cpp
		ms_menuaction = MenuActionEndMenu;
		break;
#endif
	}

	// Audible Feedback.
	if (sMenuItem == -1)
		PlaySample(g_smidMenuItemChange, SampleMaster::UserFeedBack);
	else
		PlaySample(g_smidMenuItemSelect, SampleMaster::UserFeedBack);

	return bAcceptChoice;
}

////////////////////////////////////////////////////////////////////////////////
//
// Get info about specified realm
//
////////////////////////////////////////////////////////////////////////////////
extern int16_t Play_GetRealmInfo(	 // Returns 0 if successfull, 1 if no such realm, negative on error
	bool bNetwork,					 // In:  true if network game, false otherwise
	bool bCoop,						 // In:  true if coop net game, false otherwise -- no effect if bNetwork is false.
	bool bGauntlet,					 // In:  true if playing challenge mode
	int16_t bAddOn,					 // In:  true if playing the new add on levels
	int16_t sRealmNum,				 // In:  Realm number
	int8_t sDifficulty,			 // In:  Realm difficulty.
	char *pszFile,					 // Out: Realm's file name
	int16_t sMaxFileLen,			 // In:  Max length of returned file name, including terminating null
	char *pszTitle /*= 0*/,			 // Out: Realm's title
	int16_t sMaxTitleLen /*= NULL*/) // In:  Max length of returned title, including terminating null
{
	ASSERT(sRealmNum >= 0);
	ASSERT(pszFile != NULL);
	ASSERT(sMaxFileLen > 0);

	int16_t sResult = 0;

	// Open the realm prefs file
	RPrefs prefsRealm;

	// Try opening the realms.ini file on the HD path first, if that fails go to the CD
	//todo?
	sResult = prefsRealm.Open(FullPathCD(g_GameSettings.m_pszRealmPrefsFile), "rt");
	//sResult = prefsRealm.Open(FullPathHD(g_GameSettings.m_pszRealmPrefsFile), "rt");
	//if (sResult != 0)
	//sResult = prefsRealm.Open(FullPathCD(g_GameSettings.m_pszRealmPrefsFile), "rt");
	if (sResult == 0)
	{

		// Get realm's section and entry strings
		RString strSection;
		RString strEntry;
		Play_GetRealmSectionAndEntry(bNetwork, bCoop, bGauntlet, bAddOn, sRealmNum, sDifficulty, &strSection, &strEntry);

		// Get realm file name from prefs file
		char szText[RSP_MAX_PATH * 2];
		prefsRealm.GetVal((char *)strSection, (char *)strEntry, "", szText);
		if (strlen(szText) == 0)
		{
			// Realm not found
			sResult = 1;
		}
		else if ((strlen(szText) + 1) <= sMaxFileLen)
		{
			// Return the file name
			strcpy(pszFile, szText);

			// Check if caller wants the title, too
			if ((sMaxTitleLen > 0) && (pszTitle != NULL))
			{
				// Get title from prefs file
				prefsRealm.GetVal((char *)strSection, "Title", "Untitled", szText);

				// Copy amount that will fit
				strncpy(pszTitle, szText, sMaxTitleLen - 2);
				pszTitle[sMaxTitleLen - 1] = '\0';
			}
		}
		else
		{
			// File name too long (and can't be truncated)
			sResult = -1;
			TRACE("Play_GetRealmInfo(): Realm file name/path too long!\n");			
		}

		prefsRealm.Close();
	}
	else
	{
		sResult = -1;
		TRACE("Play_GetRealmInfo(): Error opening realm prefs file: '%s'!\n", FullPathCD(g_GameSettings.m_pszRealmPrefsFile));
		//rspMsgBox(RSP_MB_ICN_STOP | RSP_MB_BUT_OK, "", "Can't open realm prefs file '%s'.\n", FullPathCD(g_GameSettings.m_pszRealmPrefsFile));
	}

	return sResult;
}

////////////////////////////////////////////////////////////////////////////////
//
// Get the section and entry that should be used when querying the realms prefs
// file for the described realm.
//
////////////////////////////////////////////////////////////////////////////////
extern void Play_GetRealmSectionAndEntry(
	bool bNetwork,		  // In:  true if network game, false otherwise
	bool bCoop,			  // In:  true if coop net game, false otherwise -- no effect if bNetwork is false.
	bool bGauntlet,		  // In:  true if playing challenge mode
	int16_t bAddOnLevels, // In:  true if playing new add on levels
	int16_t sRealmNum,	  // In:  Realm number
	int8_t sDifficulty,  // In:  Realm difficulty.
	RString *pstrSection, // Out: Section is returned here
	RString *pstrEntry)	  // Out: Entry is returned here
{
	if (bNetwork)
	{
		if (bCoop == false)
		{
			// Deathmatch multiplayer sections are named "RealmNet1, "RealmNet2", etc.
			*pstrSection = "RealmNet";
		}
		else
		{
			// Cooperative multiplayer sections are named "RealmCoopNet1, "RealmCoopNet2", etc.
			*pstrSection = "RealmCoopNet";
		}

		*pstrSection += (int16_t)(sRealmNum + 1);
		// Multiplayer realm entry is always "Realm"
		*pstrEntry = "Realm";
	}
	else if (bGauntlet)
	{
		// Challenge sections are named "Challenge1", "Challenge2", etc.
		*pstrSection = "Gauntlet";
		*pstrSection += (int16_t)(sRealmNum + 1);
		// Challen realm entry is always "Realm"
		*pstrEntry = "Realm";
	}
	else
	{
		if (g_bLastLevelDemo)
		{
			*pstrSection = "RealmEnd";
			*pstrEntry = "RealmHard";
		}
		else
		{
			// Single player sections are named "Realm1", "Realm2", etc.
			// AddOn single player sections are named "AddOn1", "AddOn2", etc.
			// JAddOn... You get the picture.
			// Selecting "ALL LEVELS" will play the levels in sequence
			// Realm, AddOn, JAddOn (Carnival?)
			switch (bAddOnLevels)
			{
			case 3:
				if (sRealmNum < REALM_NUM)
				{
					*pstrSection = "Realm";
					*pstrSection += (int16_t)(sRealmNum + 1);
				}
				else if (sRealmNum < ADDON_NUM)
				{
					*pstrSection = "AddOn";
					*pstrSection += (int16_t)(sRealmNum + 1 - REALM_NUM);
				}
				else
				{
					*pstrSection = "JAddOn";
					*pstrSection += (int16_t)(sRealmNum + 1 - ADDON_NUM);
				}
				break;
			case 2:
				*pstrSection = "JAddOn";
				*pstrSection += (int16_t)(sRealmNum + 1);
				break;
			case 1:
				*pstrSection = "AddOn";
				*pstrSection += (int16_t)(sRealmNum + 1);
				break;
			default:
				*pstrSection = "Realm";
				*pstrSection += (int16_t)(sRealmNum + 1);
			}
			// Single player entry depends on difficulty level
			switch (sDifficulty)
			{
			case 0:
			case 1:
			case 2:
			case 3:
				*pstrEntry = "RealmEasy";
				break;

			case 4:
			case 5:
			case 6:
				*pstrEntry = "RealmMedium";
				break;

			case 7:
			case 8:
			case 9:
			case 10:
			case 11:
			default:
				*pstrEntry = "RealmHard";
				break;
			}
		}
	}
}

////////////////////////////////////////////////////////////////////////////////
//
// Creates descriptor including app's time stamp, debug status (debug or release)
// and, if defined, TRACENASSERT flag.
//
////////////////////////////////////////////////////////////////////////////////
extern void Play_GetApplicationDescriptor( // Returns nothing.
	char *pszText,						   // Out: Text descriptor.
	int16_t sMaxBytes)					   // In:  Amount of writable
										   // memory pointed to by pszText.
{
	// Set default in case there's an error
	ASSERT(strlen(DEFAULT_APP_TIMESTAMP) < sMaxBytes);
	strcpy(pszText, DEFAULT_APP_TIMESTAMP);

#if defined(WIN32)
	char szModuleFileName[RSP_MAX_PATH];
	if (GetModuleFileName(NULL, szModuleFileName, sizeof(szModuleFileName)) > 0)
	{
		struct _stat statExe;
		if (_stat(szModuleFileName, &statExe) == 0)
		{
			char *pszTime = ctime(&statExe.st_mtime);
			if (pszTime)
			{
				if (strlen(pszText) + strlen(pszTime) < sMaxBytes)
				{
					// ctime() returns a string of exactly 26 characters, including /n and null.
					strcpy(pszText, pszTime);
				}

				// Get rid of trailing '\n'.
				pszText[strlen(pszText) - 1] = '\0';
			}
		}
	}
#endif

#ifdef _DEBUG
	if (strlen(pszText) + strlen(DEBUG_STR) < sMaxBytes)
	{
		strcat(pszText, DEBUG_STR);
	}
#else
	if (strlen(pszText) + strlen(RELEASE_STR) < sMaxBytes)
	{
		strcat(pszText, RELEASE_STR);
	}
#endif

#ifdef TRACENASSERT
	if (strlen(pszText) + strlen(TRACENASSERT_STR) < sMaxBytes)
	{
		strcat(pszText, TRACENASSERT_STR);
	}
#endif
}

//////////////////////////////////////////////////////////////////////////////
// Called to setup a level select
//////////////////////////////////////////////////////////////////////////////
extern int16_t Play_InitLevelSelectMenu( // Returns 0 on success.
	Menu *pmenu)						 // In:  Menu to setup.
{
	int16_t sRes = 0;		 // Assume success.
	int16_t sInputIndex = 0; // Safety.
	char tempFile[256];
	char tempText[256];

	// Assert we have enough room for the levels
	ASSERT(JADDON_NUM < (NUM_ELEMENTS(pmenu->ami)));

	for (sInputIndex = 0; sInputIndex < JADDON_NUM && sRes == 0; sInputIndex++)
	{
		// Set text describing level of this menu item.
		Play_GetRealmInfo(false, false, false, 3, sInputIndex, 1, tempFile, 256, tempText, 256);
		memset(levelNames[sInputIndex], '\0', sizeof(levelNames[sInputIndex]));
		strcpy(levelNames[sInputIndex], tempText);
		// Enable item if the level is unlocked.
		if (sInputIndex < JADDON_NUM)
		pmenu->ami[sInputIndex].sEnabled = (g_GameSettings.m_ulUnlockedLevels & (1 << sInputIndex)) ? TRUE : FALSE;
		else
		pmenu->ami[sInputIndex].sEnabled = false;		
	}

	return sRes;
}

////////////////////////////////////////////////////////////////////////////////
// EOF
////////////////////////////////////////////////////////////////////////////////
