////////////////////////////////////////////////////////////////////////////////
//
// 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 "NetDlg.h"
#include "cutscene.h"
#include "play.h"
#include "warp.h"
#include "scene.h"
#include "score.h"
#include "person.h"
#include "InputSettingsDlg.h"
#include "toolbar.h"
#include "title.h"
#include "credits.h"
#ifdef WIN32
	#include "log.h"
#endif

#if defined(WIN32)
	// For file timestamp.
	#include <windows.h>
	#include <time.h>
	#include <sys/types.h>
	#include <sys/stat.h>
#endif

#if WITH_STEAMWORKS
#include "steam/steam_api.h"
#endif

//#define RSP_PROFILE_ON
//#include "ORANGE/Debug/profile.h"


////////////////////////////////////////////////////////////////////////////////
// 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

#define VIEW_X									0
#define VIEW_Y									0 
#define VIEW_W									wideScreenWidth
#define VIEW_H									400

#define FILM_X									0
#define FILM_Y									40

// 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					((VIEW_W - 640)/2)
#define INFO_STATUS_RECT_Y					(FILM_Y - (INFO_STATUS_RECT_H + 3) )
#define INFO_STATUS_RECT_W					(g_pimScreenBuf->m_sWidth - INFO_STATUS_RECT_X)
#define INFO_STATUS_RECT_H					DISP_INFO_FONT_HEIGHT

#define DUDE_STATUS_RECT_X					0
#define DUDE_STATUS_RECT_Y					(FILM_Y + VIEW_H)
#define DUDE_STATUS_RECT_W					(g_pimScreenBuf->m_sWidth - DUDE_STATUS_RECT_X)
#define DUDE_STATUS_RECT_H					(g_pimScreenBuf->m_sHeight - DUDE_STATUS_RECT_Y)

#define REALM_STATUS_RECT_X				0
#define REALM_STATUS_RECT_Y				0
#define REALM_STATUS_RECT_W				(FILM_X + VIEW_W - 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 KEY_MENU								27

#define KEY_PAUSE							RSP_GK_PAUSE
#define KEY_NEXT_LEVEL						RSP_GK_F1
#define KEY_TOGGLE_TARGETING				RSP_GK_F2
// NOTE THAT F3 IS IN USE: DONT USE RSP_GK_F3.
#define KEY_TOGGLE_DISP_INFO				RSP_GK_F4
#define KEY_SHOW_MISSION					RSP_GK_F5
#define KEY_ENLARGE_FILM1					RSP_GK_NUMPAD_PLUS
#define KEY_ENLARGE_FILM2					'+'
#define KEY_ENLARGE_FILM3					'='
#define KEY_REDUCE_FILM1					RSP_GK_NUMPAD_MINUS
#define KEY_REDUCE_FILM2					'-'
#define KEY_TALK1								'T'
#define KEY_TALK2								't'
#define KEY_ACCEPT_CHAT						'\r'
#define KEY_ABORT_CHAT						27

// Note that this uses RSP_SK_* macros for use the rspGetKeyStatusArray() key interface.
#define KEY_XRAY_ALL							RSP_SK_F3
#define KEY_SNAP_PICTURE					RSP_SK_ENTER

#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.
	MenuActionSaveGame,	// Save user's game.
	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

//#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
		CNetClient*	pclient,									// In:  Client object or NULL if not network game
		CNetServer*	pserver,									// In:  Server object or NULL if not server or not network game
		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 int16_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:
		CNetClient*		m_pclient;						// Client object or NULL if not network game
		CNetServer*		m_pserver;						// Server object or NULL if not server or not network game

		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:
		U16				m_idLocalDude;					// Local dude's ID
		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_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.
		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_pclient = 0;
			m_pserver = 0;
			
			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_idLocalDude = 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_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
		////////////////////////////////////////////////////////////////////////////////
		CNetClient*	Client(void)					{ return m_pclient; }
		CNetServer*	Server(void)					{ return m_pserver; }
		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(void)
			{
			CDude* pdudeLocal;
			if (m_prealm->m_idbank.GetThingByID((CThing**)&pdudeLocal, m_idLocalDude) != 0)
				m_idLocalDude = CIdBank::IdNil;
			return pdudeLocal;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Query the game mode
		////////////////////////////////////////////////////////////////////////////////
		bool IsMP(void)
			{ return (m_pclient) ? true : false; }

		bool IsServer(void)
			{ return (m_pserver) ? true : 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)										// 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)										// I/O: Play info
			{
			for (Plays::Pointer p = m_Plays.GetHead(); p != 0; p = m_Plays.GetNext(p))
				m_Plays.GetData(p)->CoreLoopRender(pinfo);
			}


		////////////////////////////////////////////////////////////////////////////////
		// 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);

			// Update the display in the dirtied areas defined by m_drl.
			RDRect*	pdr	= pinfo->m_drl.GetHead();
			while (pdr)
				{
				int32_t lTime = rspGetMilliseconds();

				// Update the portion of the display.
				rspCacheDirtyRect(pdr->sX, pdr->sY, pdr->sW, pdr->sH);

				pinfo->m_lSumUpdateDisplayTimes += (rspGetMilliseconds() - lTime);

				// Remove the current rectangle from the list and delete it.
				pinfo->m_drl.Remove();

				delete pdr;

				// Get the next one.
				pdr	= pinfo->m_drl.GetNext();
				}
			rspUpdateDisplayRects();
			}


		////////////////////////////////////////////////////////////////////////////////
		// 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);
			}
	};


////////////////////////////////////////////////////////////////////////////////
//
// Client Play Module
//
////////////////////////////////////////////////////////////////////////////////
class CPlayNet : public CPlay
	{
	//------------------------------------------------------------------------------
	// Types, enums, etc.
	//------------------------------------------------------------------------------
	private:

	//------------------------------------------------------------------------------
	// Variables
	//------------------------------------------------------------------------------
	private:
		bool				m_bEndServerCleanly;
		bool				m_bServerDone;
		bool				m_bEndClientCleanly;
		bool				m_bClientDone;

		bool				m_bHandledUserQuit;
		bool				m_bHandledNextRealm;

		bool				m_bAbortNow;						// Ultimate abort flag

		bool				m_bCheckForAbortKey;				// Whether to check for user abort
		bool				m_bTimeBombActive;				// Whether time bomb is active
		int32_t				m_lTimeBomb;						// Time when bomb explodes

		bool				m_bShowNetFeedback;				// Whether to show net feedback thingy

		bool				m_bFirstCoreLoopUserInput;
		REdit*			m_apeditChats[NUM_CHATS];		// Received chat edit fields.
		int32_t				m_lLastChatMoveTime;				// Last time chats were adjusted.

	//------------------------------------------------------------------------------
	// Functions
	//------------------------------------------------------------------------------
	public:
		////////////////////////////////////////////////////////////////////////////////
		// Constructor
		////////////////////////////////////////////////////////////////////////////////
		CPlayNet(void)
			{
			// Note that if any of this fails, we don't care (we just won't use them).
			int16_t	sIndex;
			for (sIndex = 0; sIndex < NUM_CHATS; sIndex++)
				{
				m_apeditChats[sIndex]	= (REdit*)RGuiItem::LoadInstantiate(FullPathHD(CHAT_GUI) );
				if (m_apeditChats[sIndex])
					{
					// Recreate in the correct spot and dimensions . . .
					if (m_apeditChats[sIndex]->Create(
						REALM_STATUS_RECT_X,
						REALM_STATUS_RECT_Y + REALM_STATUS_RECT_H + (m_apeditChats[sIndex]->m_im.m_sHeight * sIndex),
						REALM_STATUS_RECT_W,
						m_apeditChats[sIndex]->m_im.m_sHeight,
						g_pimScreenBuf->m_sDepth) == 0)
						{
						m_apeditChats[sIndex]->SetText("");
						m_apeditChats[sIndex]->SetVisible(FALSE);
						}
					else
						{
						delete m_apeditChats[sIndex];
						m_apeditChats[sIndex]	= NULL;
						}
					}
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// Destructor
		////////////////////////////////////////////////////////////////////////////////
		/* virtual */
		~CPlayNet()
			{
			int16_t	sIndex;
			for (sIndex = 0; sIndex < NUM_CHATS; sIndex++)
				{
				delete m_apeditChats[sIndex];
				m_apeditChats[sIndex]	= NULL;
				}
			}

		
		////////////////////////////////////////////////////////////////////////////////
		// Move all the chat texts up one field.  Hides emptied fields.
		////////////////////////////////////////////////////////////////////////////////
		void MoveChatsUp(
			CPlayInfo*	pinfo)			// In:  Info object.
			{
			int16_t	sIndex	= 0;
			// Goto last chat that is filled moving them up as we go.
			while (sIndex < NUM_CHATS)
				{
				if (m_apeditChats[sIndex]->m_szText[0])
					{
					m_apeditChats[sIndex]->SetText("%s", (sIndex >= NUM_CHATS - 1) ? "" : m_apeditChats[sIndex + 1]->m_szText);
					m_apeditChats[sIndex]->Compose();

					// If this emptied the field . . .
					if (m_apeditChats[sIndex]->m_szText[0] == '\0')
						{
						m_apeditChats[sIndex]->SetVisible(FALSE);	

						// If we're at all off of the view edge . . .
						if (pinfo->Camera()->m_sFilmViewX > 0)
							{
							// Erase now.
							rspGeneralLock(g_pimScreenBuf);

							rspRect(
								RSP_BLACK_INDEX,
								g_pimScreenBuf,
								m_apeditChats[sIndex]->m_sX,
								m_apeditChats[sIndex]->m_sY,
								m_apeditChats[sIndex]->m_im.m_sWidth,
								m_apeditChats[sIndex]->m_im.m_sHeight);

							rspGeneralUnlock(g_pimScreenBuf);

							// Add dirty rectangle to update erased area.
							pinfo->m_drl.Add(
								m_apeditChats[sIndex]->m_sX,
								m_apeditChats[sIndex]->m_sY,
								m_apeditChats[sIndex]->m_im.m_sWidth,
								m_apeditChats[sIndex]->m_im.m_sHeight);
							}
						}
					}
				else
					{
					break;
					}

				sIndex++;
				}

			m_lLastChatMoveTime	= rspGetMilliseconds();
			}

		////////////////////////////////////////////////////////////////////////////////
		// Draw visible chats.
		////////////////////////////////////////////////////////////////////////////////
		void DrawChats(
			CPlayInfo*	pinfo)			// In:  Info object.
			{
			int16_t	sIndex;
			for (sIndex = 0; sIndex < NUM_CHATS; sIndex++)
				{
				if (m_apeditChats[sIndex]->m_sVisible != FALSE)
					{
					m_apeditChats[sIndex]->Draw(g_pimScreenBuf);

					// Make sure this gets to the display.
					pinfo->m_drl.Add(
						m_apeditChats[sIndex]->m_sX,
						m_apeditChats[sIndex]->m_sY,
						m_apeditChats[sIndex]->m_im.m_sWidth,
						m_apeditChats[sIndex]->m_im.m_sHeight);
					}
				}
			}

		////////////////////////////////////////////////////////////////////////////////
		// Prepare realm
		////////////////////////////////////////////////////////////////////////////////
		/* virtual */
		int16_t PrepareRealm(										// Returns 0 if successfull, non-zero otherwise
			CPlayInfo* pinfo)										// I/O: Play info
			{
			CNetClient* pclient = pinfo->Client();
			if (pclient)
				{
				// Clear server flags even if there's no server so we can rely on these flags
				m_bEndServerCleanly = false;
				m_bServerDone = false;
				m_bEndClientCleanly = false;
				m_bClientDone = false;

				m_bHandledUserQuit = false;
				m_bHandledNextRealm = false;

				m_bAbortNow = false;
				m_bCheckForAbortKey = false;
				m_bTimeBombActive = false;
				m_lTimeBomb = 0;

				m_bShowNetFeedback = false;

				m_bFirstCoreLoopUserInput = true;

				m_lLastChatMoveTime = 0;

				pinfo->m_bUserQuitMP = false;
				pinfo->m_bNextRealmMP = false;

				pinfo->m_bDoRealmFrame = false;

				// Call this periodically to let it know we're not locked up
				NetBlockingWatchdog();

				// 09/12/97 MJR - Having this here is a MAJOR MISTAKE, but it happened
				// to work out okay.  The order in which the CPlay-derived objects
				// were added to the CPlayGroup HAPPENS TO WORK OUT so that this
				// function is called AFTER the same function in the CPlayRealm object,
				// which is what actually loads the realm.  Therefore, this message is
				// only sent AFTER the realm was loaded, which is good.  However, the
				// release version also had this hardwired to "true", which means that
				// regardless of whether the realm file loaded or not, it always told
				// the server that it was ready.  That's bad.
				//
				// But it gets worse.  The original intention of this, assuming it had
				// been done correctly, was for the client to either say "yes, this worked,
				// let's play", or "I had a problem, abort the game".  That sounded good
				// at the time, but unfortunately we didn't realize until too late that
				// in the case where some users have added new levels or some users had
				// a limited version of the game, not all the players would have all the
				// same levels available.  The preferred approach, in retrospect, would
				// have been for the server to make sure all the clients had all the
				// necessary levels ahead of time.  Instead, we're now stuck trying to
				// patch it after-the-fact.
				//
				// The new idea is that if we try to load a realm and we fail, then
				// it will most likely be due to the fact that we don't have that
				// realm, as opposed to the very rare file reading error.  And instead
				// of telling the host of the problem, which would cause the host to
				// end the game, we instead lie to the host and say everything is fine,
				// but then follow that up immediately with a request to drop from the
				// game.  This way, everyone else can go on playing.
				//
				// So, after all that, we leave the original bad line of code as is!

				// Tell the server we've got the realm ready to go
				pclient->SendRealmStatus(true);
				}
			return 0;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Start realm
		////////////////////////////////////////////////////////////////////////////////
		virtual
		int16_t StartRealm(											// Returns 0 if successfull, non-zero otherwise
			CPlayInfo* pinfo)										// I/O: Play info
			{
			if (pinfo->IsMP())
				{
				// Most of the players will likely end up waiting for at least one other
				// player to start, which means they are staring at a blank screen.  To
				// alleviate their fears of a lockup, we show this message.
				RTxt* ptxt = GetNetProbGUI();
				if (ptxt)
					{
					ptxt->SetText(
						"Waiting for other\n"
						"players to get ready...");
					ptxt->Compose();
					ptxt->Move(
						(g_pimScreenBuf->m_sWidth / 2) - (ptxt->m_im.m_sWidth / 2),
						(g_pimScreenBuf->m_sHeight / 2) - (ptxt->m_im.m_sHeight / 2));
					ptxt->SetVisible(TRUE);
					ptxt->Draw(g_pimScreenBuf);
					rspUpdateDisplay(ptxt->m_sX, ptxt->m_sY, ptxt->m_im.m_sWidth, ptxt->m_im.m_sHeight);
					m_bShowNetFeedback = true;
					}
				}
			return 0;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Core loop user input
		////////////////////////////////////////////////////////////////////////////////
		void CoreLoopUserInput(
			CPlayInfo* pinfo,										// I/O: Play info
			RInputEvent* pie)										// I/O: Input event
			{
			if (pinfo->IsMP())
				{
				// Get pointers to make this more readable
				CNetServer* pserver = pinfo->Server();
				CNetClient* pclient = pinfo->Client();

				// If this is the first time this function is being called, then turn off
				// the feedback we displayed in StartRealm().
				if (m_bFirstCoreLoopUserInput)
					{
					m_bFirstCoreLoopUserInput = false;
					m_bShowNetFeedback = false;
					}

				// Check if user aborted the net blocking (meaning we were stuck and the
				// user decided he had waited long enough).
				if (NetBlockingWasAborted())
					pinfo->m_bUserQuitMP = true;

				// Check if the realm is bad (meaning it couldn't be loaded) and we've
				// started playing, and if so, abort the game.  If we're the server, this
				// will end the entire game, and if we're a client we'll just drop out
				// and let everyone else continue.  Note that we need to wait until we're
				// playing because we don't currently support dropping players during
				// while waiting for the realm to start.
				if (pinfo->m_bBadRealmMP && pclient->IsPlaying())
					{
					// Quit game
					pinfo->m_bUserQuitMP = true;

					// Tell user what's going on
					RTxt* ptxt = GetNetProbGUI();
					if (ptxt)
						{
						ptxt->SetText(
							"You don't have\n"
							"the selected hood.\n"
							"Dropping out...");
						ptxt->Compose();
						ptxt->Move(
							(g_pimScreenBuf->m_sWidth / 2) - (ptxt->m_im.m_sWidth / 2),
							(g_pimScreenBuf->m_sHeight / 2) - (ptxt->m_im.m_sHeight / 2));
						ptxt->SetVisible(TRUE);
						ptxt->Draw(g_pimScreenBuf);
						m_bShowNetFeedback = true;
						}
					}

				// If user wants to quit the MP game, try to do it cleanly.  It is
				// possible that the network links are down, so we also set a timer
				// that quits the game if it takes too long to do it the "right" way.
				// Note that a second flag keeps us from doing this more than once.
				if (pinfo->m_bUserQuitMP && !m_bHandledUserQuit)
					{
					m_bHandledUserQuit = true;

					if (pserver)
						{
						// Abort game and end server (local client will end when it gets the message)
						pserver->AbortGame(NetMsg::UserAbortedGame);
						m_bEndServerCleanly = true;
						}
					else
						{
						// Tell server we're dropping and then disconnect from it (cleanly, which means
						// it waits until all messages have been sent).  We then set the flag so we
						// do essentially the same thing at this level -- wait for all messages to be sent.
						pclient->Drop();
						pinfo->SetGameState_GameAborted();
						m_bEndClientCleanly = true;
						}

					// Set time bomb in case the "nice" way doesn't work
					m_lTimeBomb = rspGetMilliseconds() + g_GameSettings.m_lNetForceAbortTime;
					m_bTimeBombActive = true;
					}

				// Once the time bomb goes off, we display a message telling the user that something
				// has gone very wrong, and they can either wait a little longer or hit a key to abort.
				// Once that message is displayed, we check if the user has hit the abort key, and if
				// so, we set the ultimate abort flag.
				if (m_bCheckForAbortKey)
					{
					if ((pie->type == RInputEvent::Key) && (pie->lKey == NET_PROB_GUI_ABORT_GK_KEY))
						{
						pinfo->SetGameState_GameAborted();
						m_bAbortNow = true;
						}
					}
				else
					{
					if (m_bTimeBombActive)
						{
						if (rspGetMilliseconds() > m_lTimeBomb)
							{
							RTxt* ptxt = GetNetProbGUI();
							if (ptxt)
								{
								ptxt->SetText("%s", g_pszNetProb_General);
								ptxt->Compose();
								ptxt->SetVisible(TRUE);
								ptxt->Move(
									(g_pimScreenBuf->m_sWidth / 2) - (ptxt->m_im.m_sWidth / 2),
									(g_pimScreenBuf->m_sHeight / 2) - (ptxt->m_im.m_sHeight / 2));

								// Set flag to indicate that we're waiting for user to abort
								m_bCheckForAbortKey = true;
								}
							else
								{
								// If we can't display the message for the user, we don't have much
								// choice but to abort
								TRACE("CPlayNet::CoreLoopUpdate(): Unable to show abort GUI.  Aborting.\n");
								pinfo->SetGameState_GameAborted();
								m_bAbortNow = true;
								}
							}
						}
					}

				// Try doing the next frame, which involves setting all the players' inputs.
				// We must NEVER try to do another frame if we haven't done the previous one,
				// because skipping a frame would screw up network sync!
				if (!pinfo->m_bDoRealmFrame)
					{
					// If we can do a frame, all player's inputs will be returned
					UINPUT aInputs[Net::MaxNumIDs];

					/** SPA **/
					int16_t sFrameTime = 0;
					if (pclient->CanDoFrame(aInputs, &sFrameTime)) // Get frame time as well *SPA
						{
						pinfo->SetFrameTime(sFrameTime);
						/** SPA **/

						// Copy inputs into the input module
						for (Net::ID id = 0; id < Net::MaxNumIDs; id++)
							SetInput(id, aInputs[id]);

						// Set flag so we'll do the frame
						pinfo->m_bDoRealmFrame = true;
						}
					else
						{
						//------------------------------------------------------------------------------
						// Server section *SPA 1/13/98
						//------------------------------------------------------------------------------
						if (pserver && !m_bServerDone)
							{
							// Check to see if we have lost a peer through a timeout
							Net::ID peerID = pclient->CheckForLostPeer();
							if (peerID < Net::MaxNumIDs)
								// Lost one, so drop him so the rest of us can go on
								pserver->DropClient(peerID);
							}
						}


					}
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// Core loop update
		////////////////////////////////////////////////////////////////////////////////
		/* virtual */
		void CoreLoopUpdate(
			CPlayInfo* pinfo)										// I/O: Play info
			{
			// If we're in MP mode, then  there's always a client and there may be a server
			if (pinfo->IsMP())
				{

				// Get pointers to make this more readable
				CNetServer* pserver = pinfo->Server();
				CNetClient* pclient = pinfo->Client();

				// Call this periodically to let it know we're not locked up
				NetBlockingWatchdog();

				//------------------------------------------------------------------------------
				// Server section
				//------------------------------------------------------------------------------
				if (pserver && !m_bServerDone)
					{

					// Update server (send and receive messages)
					pserver->Update();

					// Check if we're trying to end
					if (m_bEndServerCleanly)
						{
						// If there isn't anything more to send, then we're done.  It's safe to
						// ignore incoming messages because they will get bufferred and we'll
						// handle them in the next realm if there is one, and if the game is
						// over, they don't matter anyway!!!
						if (!pserver->IsMoreToSend())
							m_bServerDone = true;
						}
					else
						{
						// If the realm is done, tell the clients about it.  Only the server ever
						// checks for the end of the realm -- the clients rely on the server to
						// tell them when to end.  In case this flag somehow gets set multiple
						// times, we only want to handle it once per realm, hence the purpose
						// of the second flag.
						if (pinfo->m_bNextRealmMP && !m_bHandledNextRealm)
							{

							//------------------------------------------------------------------------------
							// DIRECT BREACH OF CLIENT/SERVER SEPARATION!
							//
							// In order to get all the clients to stop on the same frame, the server
							// needs to choose a frame that no client has yet reached.  Unfortunately,
							// it has no way of knowing this (having the clients constantly report
							// their frame seq's would not help because the information would always
							// be out of date due to latencies.  Instead, we violate the client/server
							// separation and ask the local client for the most recent input seq that
							// it sent to any of the other clients.  Since no client can possibly be
							// ahead of that (it can't do a frame unless it has all the inputs), we
							// can safely use that as the frame to stop on.
							//
							// But that's not all!!!  If we were to send the "stop on frame" message
							// to all the clients, including the local one, there is still the
							// latency to deal with, and even message to local clients have some
							// latnecy.  During that time, if the local client sends out input beyond
							// the stop frame, all is lost.  So, we again violate the client/server
							// separation and DIRECTLY tell the local client what frame to stop on.
							// This ensures that it will not send out any excess inputs.
							//------------------------------------------------------------------------------
							// Note that we add one to the client's sequence as an extra safey measure.
							Net::SEQ seqHalt = (Net::SEQ)(pclient->GetInputSeqNotYetSent() + (Net::SEQ)1);
							if (pserver->NextRealm(seqHalt))
								{
								// Tell local client (see above explanation)
								pclient->SetHaltFrame(seqHalt);

								// Note that we handled this 
								m_bHandledNextRealm = true;

								// End the server cleanly
								m_bEndServerCleanly = true;
								}
							}
						else
							{
							// Process messages
							NetMsg msg;
							pserver->GetMsg(&msg);
							switch (msg.msg.nothing.ucType)
								{
								case NetMsg::ERR:
									// We'll learn about any really bad errors via the client!
									break;

								case NetMsg::NOTHING:
									break;

								case NetMsg::CHAT_REQ:
									// Handled at lower level.
									break;

								default:
									TRACE("CPlayNet::CoreLoopUpdate(): Unhandled message: %hd\n", (int16_t)msg.msg.nothing.ucType);
									break;
								}
							}
						}
					}

				//------------------------------------------------------------------------------
				// Client section
				//------------------------------------------------------------------------------
				//Mouse work-around code
				bool bRealMP = true;
				if (pclient->IsAlone()) {
					bRealMP = false;
				}

				// Check if local input is needed, and if so, hand it over
				if (!pinfo->m_bBadRealmMP)
					{
					if (pclient->IsLocalInputNeeded())
						{
						if (!pinfo->m_bInMenu && !pinfo->m_bChatting)
							pclient->SetLocalInput(GetLocalInput(pinfo->Realm(), pinfo->Camera(), pinfo->m_idLocalDude, NULL, bRealMP) );
						else
							pclient->SetLocalInput(INPUT_IDLE);
						}
					}

				// Handle network communiciation stuff
				if (!m_bClientDone)
					{
					pclient->Update();

					// Check if we're trying to end
					if (m_bEndClientCleanly)
						{
						// If there isn't anything more to send, then we're done
						if (!pclient->IsMoreToSend())
							m_bClientDone = true;
						}
					else
						{
						// Process messages from server
						NetMsg msg;
						pclient->GetMsg(&msg);
						switch (msg.msg.nothing.ucType)
							{
							case NetMsg::JOINED:
								break;

							case NetMsg::DROPPED:
								// If I am dropped, abort the game and end immediately
								if (msg.msg.dropped.id == pclient->GetID())
									{
									pinfo->SetGameState_GameAborted();
									m_bClientDone = true;
									}
								break;

								case NetMsg::CHAT:
									{
									int16_t	sIndex	= 0;

									while (m_apeditChats[sIndex]->m_szText[0])
										{
										if (++sIndex >= NUM_CHATS)	// Should never be greater but...
											{
											MoveChatsUp(pinfo);
											sIndex	= NUM_CHATS - 1;
											break;
											}
										}

									ASSERT(sIndex < NUM_CHATS);

									m_apeditChats[sIndex]->SetText("%s", msg.msg.chat.acText);
									m_apeditChats[sIndex]->Compose();
									m_apeditChats[sIndex]->SetVisible(TRUE);

									m_lLastChatMoveTime	= rspGetMilliseconds();

									break;
									}

							case NetMsg::ERR:
								break;

							case NetMsg::NOTHING:
								break;

							case NetMsg::ABORT_GAME:
								// Abort game and end client immediately
								pinfo->SetGameState_GameAborted();
								m_bClientDone = true;
								break;

							case NetMsg::NEXT_REALM:
								// Go on to the next realm (in case there's a server we end it cleanly,
								// too, because it was waiting for it's local client to get this message)
								pinfo->SetGameState_NextRealm();
								m_bEndServerCleanly = true;
								m_bEndClientCleanly = true;
								break;

							case NetMsg::PROGRESS_REALM:
// This was disabled for reasons explained at length in NetServer.cpp -- suffice it to say it's
// far from an easy fix.
#if 0
								m_bShowNetFeedback = false;
								if (msg.msg.progressRealm.sNumNotReady > 0)
									{
									// Show the player how many other players we're waiting for
									RTxt* ptxt = GetNetProbGUI();
									if (ptxt)
										{
										ptxt->SetText("Waiting for %hd\nplayer(s) to get ready...", msg.msg.progressRealm.sNumNotReady);
										ptxt->Compose();
										ptxt->Move(
											(g_pimScreenBuf->m_sWidth / 2) - (ptxt->m_im.m_sWidth / 2),
											(g_pimScreenBuf->m_sHeight / 2) - (ptxt->m_im.m_sHeight / 2));
										ptxt->SetVisible(TRUE);
										m_bShowNetFeedback = true;
										}
									}
#endif
								break;

							case NetMsg::PROCEED:
								break;

							default:
								TRACE("CPlayNet::CoreLoopUpdate(): Unhandled message: %hd\n", (int16_t)msg.msg.nothing.ucType);
								break;
							}
						}
					}

				// Govern loop to a maximum of frames-per-second rate
				//
				// This is definitely not done.  It's a waste to sit here in a dead loop, when we could be checking
				// for more network input/output/whatever.  Perhaps this whole function should be inside a do/while
				// loop that loops as long until we've slowed to the proper frame rate.
				//
				// Actually, I think the real answer might be to put timers on everything in Play.  There's no
				// point in checking for keyboard input 100 times per second.  There's no point in doing many of
				// the things we do at that speed, but we DO want to loop through everything as quickly as possible
				// to give an opportunity to everyone to do whatever DOES need to be done.
				//
				// The frames-per-second governor would be done at the point were we actually render the frame
				// and increment the frame seq.  That portion would say "If we're ready to render the frame AND
				// it's been 1/50th of a second since we rendered the last frame, then do it."
//				while (rspGetMilliseconds() < m_lGovernTime)
//					;
//				m_lGovernTime = rspGetMilliseconds() + Net::MinFrameTime;

				if (m_lLastChatMoveTime	+ CHAT_DELAY < rspGetMilliseconds() )
					{
					// Move the chats up -- this updates m_lLastChatMoveTime.
					MoveChatsUp(pinfo);
					}
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// Core loop render -- create and update images to the composite buffer but do
		// NOT update the screen.
		////////////////////////////////////////////////////////////////////////////////
		void CoreLoopRender(
			CPlayInfo* pinfo)										// I/O: Play info
			{
			if (pinfo->IsMP())
				{
				// Draw chats, if any.
				DrawChats(pinfo);

				// If we're waiting for user to abort or there was a net problem
				if (m_bCheckForAbortKey || IsNetProb() || m_bShowNetFeedback)
					{
					// What we want to do is put it in the composite buffer.  It will be
					// copied to the screen in CoreLoopDraw().
					RTxt* ptxt = GetNetProbGUI();
					if (ptxt)
						{
						// Draw locks the screen as is necessary for the current buffer type.
						// Even though it's already locked, this should be fine.
						ptxt->Draw(g_pimScreenBuf);
						// Add a rectangle representing this GUI so it gets updated to the screen.
						pinfo->m_drl.Add(ptxt->m_sX, ptxt->m_sY, ptxt->m_im.m_sWidth, ptxt->m_im.m_sHeight);
						}
					}
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// Determine if core loop is done
		////////////////////////////////////////////////////////////////////////////////
		/* virtual */
		bool IsCoreLoopDone(										// Returns true if done, false otherwise
			CPlayInfo* pinfo)										// I/O: Play info
			{
			bool bDone;
			if (pinfo->IsMP())
				{
				// This is the ultimate abort flag
				if (m_bAbortNow)
					bDone = true;
				else
					{
					// Check server (if there is one)
					bDone = pinfo->Server() ? m_bServerDone : true;

					// There's always a client in MP mode
					bDone &= m_bClientDone;
					}
				}
			else
				{
				// If client doesn't exist, use base-class implimentation
				bDone = CPlay::IsCoreLoopDone(pinfo);
				}
			return bDone;
			}
	};


////////////////////////////////////////////////////////////////////////////////
//
// 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 */
		Net::SEQ			m_seqPrevFrameSeq;
		Net::SEQ			m_seqCurrFrameSeq;
		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;
		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_seqCurrFrameSeq = 0;
				m_lFramePerSecond = 0;
				if (pinfo->IsMP())
					m_seqPrevFrameSeq = pinfo->Client()->GetInputSeqNotYetSent();
				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.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);
				m_print.SetDestination(g_pimScreenBuf);

				// 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)
					{
					ToolBarRender(
						pinfo->Realm()->m_phood, 
						g_pimScreenBuf, 
						m_rectDude.sX, 
						m_rectDude.sY,
						pinfo->LocalDudePointer(),TRUE);

					// Draw the top bar just once.  I'm assuming all the rest of the updates are
					// partial and, therefore, this only needs to be done once.
					rspBlit(
						pinfo->Realm()->m_phood->m_pimTopBar,
						g_pimScreenBuf,
						0,
						0,
						REALM_STATUS_RECT_X,
						REALM_STATUS_RECT_Y,
						pinfo->Realm()->m_phood->m_pimTopBar->m_sWidth,
						pinfo->Realm()->m_phood->m_pimTopBar->m_sHeight);

					}

				// Unlock once done.
				rspUnlockBuffer();

				// Make sure this these get reflected on the screen intially.
				rspUpdateDisplay(m_rectDude.sX, m_rectDude.sY, m_rectDude.sW, m_rectDude.sH);

				rspUpdateDisplay(
					REALM_STATUS_RECT_X,
					REALM_STATUS_RECT_Y,
					pinfo->Realm()->m_phood->m_pimTopBar->m_sWidth,
					pinfo->Realm()->m_phood->m_pimTopBar->m_sHeight);
				}
			return 0;
			}


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

				/**** 12/3/97  AJC ****/
				if (pinfo->IsMP())
					{

					// Get current frame sequence number
						m_seqCurrFrameSeq = pinfo->Client()->GetInputSeqNotYetSent();

					// Check if it's the next frame, if it is, calculate frame per sec
					if (m_seqCurrFrameSeq != m_seqPrevFrameSeq)
						{
						int32_t lCurrSeqTime = rspGetMilliseconds();
						m_lFramePerSecond = 1000.0 * (m_seqCurrFrameSeq - m_seqPrevFrameSeq) / (lCurrSeqTime - m_lPrevSeqTime);

						m_seqPrevFrameSeq = m_seqCurrFrameSeq;
						m_lPrevSeqTime = lCurrSeqTime;
						}
					}
				/**** 12/3/97  AJC ****/

				//==============================================================================
				// Check for death stuff
				//==============================================================================
				CDude* pdudeLocal = pinfo->LocalDudePointer();
				// If the local dude is dead . . .
				if (pdudeLocal != NULL)
					{
					if (pdudeLocal->IsDead() )
						{
						// Make sure X Ray is on so we can see him.
						pinfo->Realm()->m_scene.SetXRayAll(TRUE);
						}
					else
						{
						// If alive, use user setting.
						pinfo->Realm()->m_scene.SetXRayAll( (pinfo->m_bXRayAll == true) ? TRUE : FALSE);
						}
					}
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// 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)
				{
				// Only if we're not on the menu
				if (!pinfo->m_bInMenu)
					{
					// 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)
							{
							// Update the realm (or Score) status
							bUpdateRealm	= ScoreUpdateDisplay(
								g_pimScreenBuf, 
								&m_rectRealm, 
								pinfo->Realm(), 
								pinfo->Client(),
								REALM_STATUS_RECT_X,
								REALM_STATUS_RECT_Y,
								pinfo->Realm()->m_phood);
							if (bUpdateRealm == true)
								{
								pinfo->m_drl.Add(m_rectRealm.sX, m_rectRealm.sY, m_rectRealm.sW, m_rectRealm.sH);
								}

							// Generate TooL Bar
							if (ToolBarRender(
								pinfo->Realm()->m_phood, 
								g_pimScreenBuf, 
								m_rectDude.sX, 
								m_rectDude.sY,
								pinfo->LocalDudePointer()) == true)
								{
								pinfo->m_drl.Add(m_rectDude.sX, m_rectDude.sY, m_rectDude.sW, m_rectDude.sH);
								}
							}

						// 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;
								}
							}

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


#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)
			{
			// We don't care if this fails.
			m_peditChatIn	= (REdit*)RGuiItem::LoadInstantiate(FullPathHD(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,
					g_pimScreenBuf->m_sDepth) == 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		= 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 usefull pointers
				CDude* pdudeLocal = pinfo->LocalDudePointer();
				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.
					//==============================================================================

					// Always default to "not pressed" for the "end-the-level-now" key
					bool 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::None))
						if ((GetInputMode() == INPUT_MODE_PLAYBACK) && (pie->type == RInputEvent::Key))
							{
							pinfo->SetGameState_GameAborted();
							}
						else
							{
							// MUST NOT DO THIS IN SALES MODE!  Normally, this cheat just takes
							// you to the next level.  In sales mode, though, this cheat gives
							// you lots o stuff as well.  Going to the next level in Sales Mode
							// is handled separately.
							#if !defined(SALES_DEMO)
								// Cheats are disabled in MP mode by other code, but it still seems better to
								// clarify that this is NOT allowed in MP mode.
								if (!pinfo->IsMP())
									{
									if ((GetInput(0) & INPUT_WEAPONS_MASK) == INPUT_CHEAT_29)
										{
										// End the realm without checking whether goal has been met
										pinfo->SetGameState_NextRealm();
										}
									}
							#endif // !SALES_DEMO

							// Handle pause button on joystick
							if (g_InputSettings.m_sUseJoy)
							{
								XInputState xis;
								GetXInputStateNoUpdate(&xis);
								if (xis.ButtonState[g_InputSettings.m_sJoyStartButton] == XInputState::Press)
								{
									// Start the menu.  I have no idea why the check to make sure the game
									// is not over was necessary, but it doesn't seem like it could hurt,
									// so it was safer to leave it in.
									if (!pinfo->IsGameOver())
										StartMenu(pinfo);
								}
							}

							// Process local key events
							if (pie->type == RInputEvent::Key && pie->sUsed == FALSE)
								{
								// This is the switch for things that don't want specific RSP_GKF_* modifier keys
								switch (pie->lKey & 0x0000FFFF)
									{
									case KEY_NEXT_LEVEL:
										if (pinfo->IsMP())
											{
											// Only the server's local user can advance to the next level, but even
											// then only when the game is playing.  The actual handling of this
											// is done elsewhere -- we just set the flag here.
											if (pinfo->IsServer() && pinfo->Client()->IsPlaying())
												pinfo->m_bNextRealmMP = true;
											}
										else
											{
											// If sales demo cheat is enabled, we can go to the next level
											#if defined(SALES_DEMO)
												if (g_bEnableLevelAdvanceWithoutGoal)
													pinfo->SetGameState_NextRealm();
											#endif // SALES_DEMO

											// Normally, all that happens when you press this key is that we
											// set a flag that gets passed to the function that determines whether
											// the end-of-level-goal has been reached, which may or may not depend
											// on the player actually pressing the key.
											bEndLevelKey = true;

											pie->sUsed	= TRUE;
											}
										break;

									case KEY_PAUSE:
										// Only allow pause if we're NOT in MP mode and we're in live mode
										if (!pinfo->IsMP() && (GetInputMode() == INPUT_MODE_LIVE))
											{
											PauseGame(pinfo->Realm(), "Press <Pause> key to resume", KEY_PAUSE);

											pie->sUsed	= TRUE;
											}
										break;

									case KEY_MENU:
										// If not chatting . . .
										if (pinfo->m_bChatting == false)
											{
											// Start the menu.  I have no idea why the check to make sure the game
											// is not over was necessary, but it doesn't seem like it could hurt,
											// so it was safer to leave it in.
											if (!pinfo->IsGameOver())
												StartMenu(pinfo);

											pie->sUsed	= TRUE;
											}
										break;

									case KEY_TOGGLE_TARGETING:
										// Toggle game settings flag
										g_GameSettings.m_sCrossHair = !g_GameSettings.m_sCrossHair;

										// Toggle local dude's flag (if he exists)
										if (pdudeLocal != NULL)
											pdudeLocal->m_bTargetingHelpEnabled = (g_GameSettings.m_sCrossHair != FALSE) ? true : false;

										pie->sUsed	= TRUE;
										break;

									case KEY_SHOW_MISSION:
										// Show the mission goal line again for about 5 seconds.
										ScoreDisplayStatus(prealm);

										pie->sUsed	= TRUE;
										break;

									case KEY_ENLARGE_FILM1:
									case KEY_ENLARGE_FILM2:
									case KEY_ENLARGE_FILM3:
										// Increase film scale
										g_GameSettings.m_dGameFilmScale += FILM_INCDEC_SCALE;

										pie->sUsed	= TRUE;
										break;
									
									case KEY_REDUCE_FILM1:
									case KEY_REDUCE_FILM2:
										// Decrease film scale
										g_GameSettings.m_dGameFilmScale -= FILM_INCDEC_SCALE;

										pie->sUsed	= TRUE;
										break;

									case KEY_TOGGLE_DISP_INFO:
										// Toggle display info flag.
										if (g_GameSettings.m_sDisplayInfo == FALSE)
											g_GameSettings.m_sDisplayInfo	= TRUE;
										else
											g_GameSettings.m_sDisplayInfo	= FALSE;

										pie->sUsed	= TRUE;
										break;

									case KEY_TALK1:
									case KEY_TALK2:
										if (m_peditChatIn && pinfo->IsMP() && pinfo->m_bChatting == false)
											{
											// Activate talk mode.
											pinfo->m_bChatting	= true;

											m_peditChatIn->SetVisible(TRUE);

											pie->sUsed	= TRUE;
											}
										break;
									}

								// If in talk mode . . .
								if (pinfo->m_bChatting == true && m_peditChatIn && pinfo->IsMP() )
									{
									switch (pie->lKey)
										{
										case KEY_ACCEPT_CHAT:
											// If anything was typed . . .
											if (m_peditChatIn->m_szText[0])
												{
												// Send the chat text.
												pinfo->Client()->SendChat(m_peditChatIn->m_szText);
												}

											// Intentional fall through.

										case  KEY_ABORT_CHAT:	// Cancel jumps in here to clean up but not send anything.

											// If we're at all off of the view edge . . .
											if (pinfo->Camera()->m_sFilmViewX > 0)
												{
												// Erase now.
												rspGeneralLock(g_pimScreenBuf);

												rspRect(
													RSP_BLACK_INDEX,
													g_pimScreenBuf,
													m_peditChatIn->m_sX,
													m_peditChatIn->m_sY,
													m_peditChatIn->m_im.m_sWidth,
													m_peditChatIn->m_im.m_sHeight);

												rspGeneralUnlock(g_pimScreenBuf);

												// Add dirty rectangle to update erased area.
												pinfo->m_drl.Add(
													m_peditChatIn->m_sX,
													m_peditChatIn->m_sY,
													m_peditChatIn->m_im.m_sWidth,
													m_peditChatIn->m_im.m_sHeight);
												}

											// Clear the chat text.
											m_peditChatIn->SetText("");
											m_peditChatIn->Compose();
											m_peditChatIn->SetVisible(FALSE);

											// Reset the input.
											ClearLocalInput();

											pinfo->m_bChatting	= false;
											break;

										default:
											m_peditChatIn->Do(pie);
											break;
										}
									}
								}
							else
								{
								// If we're in chat mode, even if there's no input for the chat box,
								// we need to call the Edit's Do() so it can flash the caret and stuff.
								if (pinfo->m_bChatting == true && m_peditChatIn && pinfo->IsMP() )
									{
									m_peditChatIn->Do(pie);
									}
								}

							// Note that this key's status element in the key status array could be determined once and
							// stored statically, but, if we do this, the key cannot be changed during gameplay.  That
							// is, if the user changed the key to say Caps Lock, it would not work until the next
							// run of the game.

							// If xray all pressed . . .
							if (m_pau8KeyStatus[KEY_XRAY_ALL])
								{
								// Toggle user choice for XRay all.
								pinfo->m_bXRayAll	= !pinfo->m_bXRayAll;
								// Set new value to the scene.
								prealm->m_scene.SetXRayAll( (pinfo->m_bXRayAll == true) ? TRUE : FALSE);
								// Clear key's status.
								m_pau8KeyStatus[KEY_XRAY_ALL]	= 0;
								}

							// If snap picture pressed . . .
							if (m_pau8KeyStatus[KEY_SNAP_PICTURE])
								{
								// If not chatting . . .
								if (pinfo->m_bChatting == false)
									{
									// If snap shots are enabled, take one
									if (g_GameSettings.m_sCanTakeSnapShots != 0)
										Play_SnapPicture();

									// Clear the key.
									m_pau8KeyStatus[KEY_SNAP_PICTURE]	= 0;
									}
								}

							// If app goes to background, we put up a "pause game" message and wait for it to return
							// to the foreground.  In MP mode, we can't do this or all the other players will freeze!
							if (rspIsBackground() && !pinfo->IsMP())
								PauseGame(prealm, "Make Postal the foreground app to resume", 0);

							// If "live" and not MP . . .
							if ((GetInputMode() == INPUT_MODE_LIVE) && !pinfo->IsMP())
								{
								// If revive key pressed . . .
#ifdef MOBILE
								if (continueIsRestart)
#else
								if (GetInput(0) & INPUT_REVIVE)
#endif
									{
#ifdef MOBILE
									continueIsRestart = false; //Reset this flag for next time
#endif

									bool	bRestart	= false;

									// If there's a local dude . . .
									if (pdudeLocal)
										{
										// If he's dead . . .
										if (pdudeLocal->IsDead() == true)
											{
											// Restart the realm
											bRestart	= true;
											}
										}
									else
										{
										// Restart the realm
										bRestart	= true;
										}

									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.
					//==============================================================================

					if (pinfo->IsMP())
						{
						if (pinfo->IsServer() && pinfo->Client()->IsPlaying())
							{
							if (prealm->IsEndOfLevelGoalMet(bEndLevelKey))
								pinfo->m_bNextRealmMP = true;
							}
						}
					else
						{
						if (prealm->IsEndOfLevelGoalMet(bEndLevelKey))
							{
							// Set so we'll go to the next realm
							pinfo->SetGameState_NextRealm();

#ifndef DISABLE_SP_CHALLENGE_SCORES
							// Display high scores
							ScoreDisplayHighScores(prealm);
#endif
							}
						}

					//==============================================================================
					// If NOT multiplayer mode, get player's input here.  Otherwise, player input
					// is handled by the network stuff.
					//==============================================================================

					if (!pinfo->IsMP())
						{
						// Set controls for the one-and-only dude now (allow cheats).
						SetInput(0, GetLocalInput(prealm, pinfo->Camera(), pinfo->m_idLocalDude, pie));
						}

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

					if (GetInputMode() != INPUT_MODE_LIVE)
						{
						// 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)
								{
								// Game be done now.
								pinfo->SetGameState_GameOver();
								}
							}

						// 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
			{
			if (!pinfo->m_bBadRealmMP)
				{
				// Only if we're not on the menu
				if (!pinfo->m_bInMenu)
					{
					// If in chat input mode . . .
					if (pinfo->m_bChatting && m_peditChatIn)
						{
						m_peditChatIn->Draw(g_pimScreenBuf);

						// Add dirty rectangle.
						pinfo->m_drl.Add(
							m_peditChatIn->m_sX,
							m_peditChatIn->m_sY,
							m_peditChatIn->m_im.m_sWidth,
							m_peditChatIn->m_im.m_sHeight);
						}
					}
				}
			}

		
		////////////////////////////////////////////////////////////////////////////////
		//
		// Pause the specified realm, fade colors to red, display specified message,
		// and wait for specified key or focus.
		// When the key is hit, or foreground status is regained, the color are faded
		// back from red, the realm is resumed, and the function returns.
		//
		////////////////////////////////////////////////////////////////////////////////
		void PauseGame(
			CRealm*	prealm,			// In:  Realm to pause or NULL.
			char*		pszMsg,			// In:  Message to be displayed.
			int32_t		lKey)				// In:  Key to continue or 0 to wait for foreground status
			{
			// Suspend realm.
			if (prealm)
				{
				prealm->Suspend();
				}

			// Fade colors to red.
			PalTranOn();

			// Create colors.

			// 'PAUSED' fore color.
			rspSetPaletteEntry(
				PAUSED_BASE_PAL_INDEX + 0,			// Palette entry (0 to 255)
				PAUSED_FONT_COLOR_R,					// Red component (0 to 255)
				PAUSED_FONT_COLOR_G,					// Green component (0 to 255)
				PAUSED_FONT_COLOR_B);				// Blue component (0 to 255)
															
			// 'PAUSED' shadow color.				
			rspSetPaletteEntry(						
				PAUSED_BASE_PAL_INDEX + 1,			// Palette entry (0 to 255)
				PAUSED_FONT_SHADOW_COLOR_R,		// Red component (0 to 255)
				PAUSED_FONT_SHADOW_COLOR_G,		// Green component (0 to 255)
				PAUSED_FONT_SHADOW_COLOR_B);		// Blue component (0 to 255)
															
			// Message fore color.					
			rspSetPaletteEntry(						
				PAUSED_BASE_PAL_INDEX + 2,			// Palette entry (0 to 255)
				PAUSED_MSG_FONT_COLOR_R,			// Red component (0 to 255)
				PAUSED_MSG_FONT_COLOR_G,			// Green component (0 to 255)
				PAUSED_MSG_FONT_COLOR_B);			// Blue component (0 to 255)

			// Message shadow color.
			rspSetPaletteEntry(
				PAUSED_BASE_PAL_INDEX + 3,			// Palette entry (0 to 255)
				PAUSED_MSG_FONT_SHADOW_COLOR_R,	// Red component (0 to 255)
				PAUSED_MSG_FONT_SHADOW_COLOR_G,	// Green component (0 to 255)
				PAUSED_MSG_FONT_SHADOW_COLOR_B);	// Blue component (0 to 255)

			// Update hardware palette.
			rspUpdatePalette();

			// Lock the buffer before drawing to it.
			rspLockBuffer();

			// Draw text.
			RPrint	print;
			print.SetFont(PAUSED_FONT_HEIGHT, &g_fontPostal);
			print.SetEffectAbs(RPrint::SHADOW_X, PAUSED_FONT_SHADOW_X);
			print.SetEffectAbs(RPrint::SHADOW_Y, PAUSED_FONT_SHADOW_Y);

			print.SetColor(
				PAUSED_BASE_PAL_INDEX + 0, 
				0, 
				PAUSED_BASE_PAL_INDEX + 1);

			print.SetDestination(g_pimScreenBuf);
			print.SetJustifyCenter();

			int16_t	sTotalH	= PAUSED_FONT_HEIGHT + PAUSED_FONT_SHADOW_Y;
			if (pszMsg)
				{
				// Include message height as well.
				sTotalH	+= PAUSED_MSG_FONT_HEIGHT + PAUSED_MSG_FONT_SHADOW_Y;
				}

			int16_t	sPosY		= g_pimScreenBuf->m_sHeight / 2 - sTotalH; // / 2;

			print.print(
				0,
				sPosY,
				"PAUSED");

			if (pszMsg)
				{
				sPosY	+= PAUSED_FONT_HEIGHT + PAUSED_FONT_SHADOW_Y;

				print.SetFont(PAUSED_MSG_FONT_HEIGHT);
				print.SetEffectAbs(RPrint::SHADOW_X, PAUSED_MSG_FONT_SHADOW_X);
				print.SetEffectAbs(RPrint::SHADOW_Y, PAUSED_MSG_FONT_SHADOW_Y);
				
				print.SetColor(
					PAUSED_BASE_PAL_INDEX + 2, 
					0, 
					PAUSED_BASE_PAL_INDEX + 3);

				print.print(
					0,
					sPosY,
					"%s",
					pszMsg);
				}

			// Unlock the buffer now that we're done drawing to it.
			rspUnlockBuffer();

			// Update the screen with the text.
			rspUpdateDisplay();

			// Loop until signaled to continue.
			bool	bResume	= false;
			RInputEvent		ie;
			while (bResume == false)
				{
				UpdateSystem();

				if (lKey)
					{
					ie.type	= RInputEvent::None;
					rspGetNextInputEvent(&ie);
					if (ie.type == RInputEvent::Key)
						{
						if ((ie.lKey & 0x0000FFFF) == lKey)
							{
							bResume	= true;
							}
						}
					}
				else
					{
					if (rspIsBackground() == FALSE)
						{
						bResume	= true;
						}
					}

				if (rspGetQuitStatus() )
					{
					bResume	= true;
					}
				}

			// Fade colors back from red.
			PalTranOff();

			// Resum realm.
			if (prealm)
				{
				prealm->Resume();
				}

			// Clear queues.
			rspClearAllInputEvents();

			// Re-init input.
			ClearLocalInput();
			}


		////////////////////////////////////////////////////////////////////////////////
		//
		// Start the menu
		//
		////////////////////////////////////////////////////////////////////////////////
		void StartMenu(
			CPlayInfo* pinfo)										// I/O: Play info
			{
			// If not in multiplayer mode, suspend the realm while on the menu
			if (!pinfo->IsMP())
				pinfo->Realm()->Suspend();

			// Fade out colors -- for MP do it fast to avoid hanging the game up
			if (pinfo->IsMP())
				PalTranOn(0);
			else
				PalTranOn();

			// Clear input events.
			rspClearAllInputEvents();

#ifdef MOBILE
			if (pinfo->LocalDudePointer()->IsDead()) //Only enable RETRY if player is dead
				menuClientGame.ami[0].sEnabled = TRUE;
			else
				menuClientGame.ami[0].sEnabled = FALSE;
#endif
			// 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(&menuClientGame, &g_resmgrShell, g_pimScreenBuf) == 0)
				{
				// Disable autopump.
				RMix::SetAutoPump(FALSE);

				// Disable Camera's screen access by making the view really friggin small.
				pinfo->Camera()->SetViewSize(0, 0);

				// Set flag to indicate we're in the menu
				pinfo->m_bInMenu = true;
#ifdef MOBILE
	AndroidSetScreenMode(TOUCH_SCREEN_MENU);
#endif
				}
			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, g_InputSettings.m_sUseJoy);

			switch (ms_menuaction)
				{
				// Nothing in particular.
				case MenuActionNone:
					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;
				// User save choice.
				case MenuActionSaveGame:
					{
					int16_t sResult;
					// Static so dialog will "remember" the previously-used name
					static char	szFile[RSP_MAX_PATH]	= "";

					// If not yet used, start out in appropriate directory
					if (szFile[0] == '\0')
						strcpy(szFile, FullPathHD(SAVEGAME_DIR));

					// Display open dialog to let user choose a file
					#if 1 //PLATFORM_UNIX
					if (PickFile("Choose Game Slot", EnumSaveGamesSlots, szFile, sizeof(szFile)) == 0)
                    {
#ifdef MOBILE
						//Android we have the format "1 - date"
						//Need to create the filename
						char number = szFile[0];
						snprintf(szFile, sizeof (szFile), "%s/%c.gme", SAVEGAME_DIR,number);
#else
						char *ptr = strrchr(szFile, '[');
						if (ptr) *(ptr-1) = '\0';
#endif

						// This function will open the saved game file and set the correct game mode
						// and settings.  Note that this modifies the m_action (that's how we get
						// out this state...this confused me for a while but it seems like a good
						// way to choose the appropriate original action).
						if (Game_SavePlayersGame(szFile, pinfo->Realm()->m_flags.sDifficulty) == SUCCESS)
						{
							#if WITH_STEAMWORKS
							if ((EnableSteamCloud) && (strncmp(szFile, "steamcloud/", 11) == 0))
							{
								char fname[64];
								snprintf(fname, sizeof (fname), "savegame_%s", szFile + 11);
								ISteamRemoteStorage *cloud = SteamRemoteStorage();
								if (cloud)
								{
									FILE *io = fopen(FindCorrectFile(szFile, "rb"), "rb");
									if (io)
									{
										char buf[1024];
										const size_t br = fread(buf, 1, sizeof (buf), io);
										fclose(io);
										if (br > 0)
											cloud->FileWrite(fname, buf, (int32) br);
									}
								}
							}
							#endif
						}
					}
					#else
						#if WITH_STEAMWORKS
						#error You need to switch over from this code to the in-game file UI first.
						#endif
					sResult = rspSaveBox(g_pszSaveGameTitle, szFile, szFile, sizeof(szFile), SAVEGAME_EXT);
					if (sResult == 0)
						{
						if (Game_SavePlayersGame(szFile, pinfo->Realm()->m_flags.sDifficulty) != SUCCESS)
							{
							rspMsgBox(RSP_MB_ICN_EXCLAIM | RSP_MB_BUT_OK, g_pszSaveGameErrorTitle,
							 g_pszSaveGameErrorText);
							}
						}
                    #endif

					break;
					}
				case MenuActionEndMenu:
					StopMenu(pinfo);
					break;
				default:
					TRACE("RespondToMenuRequest(): Unhandled action %d.\n", ms_menuaction);
					break;
				}
	
			DoMenuOutput(pinfo->Camera()->m_pimFilm);

			ms_menuaction	= MenuActionNone;

			// This is CHEEZY AS HELL but the normal menu callback calls
			// game.cpp which sets its action flag telling it to call this
			// function.  Not sure how to do it here.  Will we need to call
			// game.cpp, play.cpp, and gameedit.cpp whenever this menu is
			// activated?
			Menu*	pmenu	= GetCurrentMenu();
			if (pmenu == &menuJoystick || pmenu == &menuMouse || pmenu == &menuKeyboard)
				{
				// Do the input settings.
				EditInputSettings();
				}

			rspUpdateDisplay();
			}


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

			// Update the display reflecting the erasure of the menu.
			rspUpdateDisplay();

			// Re-enable autopump.
			RMix::SetAutoPump(TRUE);

			// 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 (!pinfo->IsMP())
				{
				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
			PalTranOff();

			// Clear queues.
			rspClearAllInputEvents();

			// Clear the local input.
			ClearLocalInput();

			// If not in multiplayer mode, resume the realm
			if (!pinfo->IsMP())
				pinfo->Realm()->Resume();

			// Restore camera's screen access.
			pinfo->Camera()->SetViewSize(
				VIEW_W * g_GameSettings.m_dGameFilmScale,
				VIEW_H * g_GameSettings.m_dGameFilmScale);
			
			// If the user toggled the crosshair via the options menu,
			// they'll want their changes to take effect immediately,
			// so we update the Dude's value to the global value here.
			if (pdudeLocal != NULL)
				pdudeLocal->m_bTargetingHelpEnabled = (g_GameSettings.m_sCrossHair != FALSE) ? true : false;

			// 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:
		LevelPersist	m_alevelpersist[Net::MaxNumIDs];	// Index by CDude::m_sDudeNum.
		bool				m_bMakeDemoMovie_WaitForClick;	// Flag used when making demo movies
		double			m_dCurrentFilmScale;
		int16_t				m_sCurrentGripZoneRadius;
		int32_t				m_lNumSeqSkippedFrames;


	//------------------------------------------------------------------------------
	// 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;
			for (sDudeIndex = 0; sDudeIndex < Net::MaxNumIDs; 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.
							if (g_bTransferStockpile)
								m_alevelpersist[0].stockpile.Copy(&g_stockpile);
							// Otherwise, keep the global savable stockpile up to date.
							else
								g_stockpile.Copy(&m_alevelpersist[0].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 we're in the specific realm mode, then display a message telling the user that
					// this version only handles one specific realm.  Otherwise, this shouldn't happen
					// to a user of a normal version, so we don't say anything.
					#if defined(ENABLE_PLAY_SPECIFIC_REALMS_ONLY)
						// MP is a special case that is handled below
						if (!pinfo->IsMP())
							{
							rspMsgBox(
								RSP_MB_ICN_INFO | RSP_MB_BUT_OK,
								g_pszAppName,
								g_pszPlayOneRealmOnlyMessage);
							}
					#endif	// ENABLE_PLAY_SPECIFIC_REALMS_ONLY
					}

				// 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(VIEW_X, VIEW_Y, VIEW_W, VIEW_H);

				// 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;
			}


		////////////////////////////////////////////////////////////////////////////////
		// Core loop render -- create and update images to the composite buffer but do
		// NOT update the screen.
		////////////////////////////////////////////////////////////////////////////////
		/* virtual */
		void CoreLoopRender(
			CPlayInfo* pinfo)										// 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;
					}
				else
					{
					// If we're in the menu, then DON'T do the frame
					if (pinfo->m_bInMenu)
						bDoFrame = false;
					}

				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())
							{
							// In multiplayer mode, time moves in hardwired increments
							prealm->m_time.Update(pinfo->FrameTime());
							}
						else
							{
							// 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;
						}

					// 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);
						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;
					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 not in menu . . .
				if (pinfo->m_bInMenu == false)
					{
					// If we should draw the frame . . .
					if (pinfo->m_bDrawFrame)
						{
						CCamera* pcamera = pinfo->Camera();

						pinfo->m_drl.Add(
							pcamera->m_sFilmViewX, 
							pcamera->m_sFilmViewY, 
							pcamera->m_sViewW, 
							pcamera->m_sViewH);
						}
					}
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		// 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;
						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)											// In:  Dude to setup
			{
			// Get local dude's ID
			pinfo->m_idLocalDude = 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;
			}


		////////////////////////////////////////////////////////////////////////////////
		// 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);

			// 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_idLocalDude = 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;
			CWarp*	pwarp;
			bool		bFirst = true; 
			while (pln != plnTail)
				{
				CListNode<CThing>*	plnNext	= pln->m_pnNext;

				pdude	= (CDude*)pln->m_powner;
				if (CWarp::CreateWarpFromDude(prealm, pdude, &pwarp, bFirst) == 0)
					bFirst = false;
				else
					TRACE("SetupDudes(): CWarp::CreateWarpFromDude() failed.\n");

				delete pdude;
				pdude = 0;

				pln = plnNext;
				}

			//------------------------------------------------------------------------------
			// 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]);

				// Multiplayer mode is handled separately
				if (pinfo->IsMP())
					{
					// Get convenient pointer
					CNetClient* pclient = pinfo->Client();

					// Find a random starter.  Pick a number from 0 to n - 1 where n is the
					// number of CWarps in the realm.  Next, iterate to that warp so we start
					// creating dudes at a 'random' warp.
					int16_t	sStartWarpNum	= GetRand() % prealm->m_asClassNumThings[CThing::CWarpID];
					int16_t	i;
					for (i = 0; i < sStartWarpNum; i++, plnWarp = plnWarp->m_pnNext)
						;

					// Warp in as many dude's as we need
					for (Net::ID id = 0; (sResult == 0) && (id < Net::MaxNumIDs); id++)
						{
						// If this player needs a dude
						if (pclient->DoesPlayerNeedDude(id))
							{
							// If we've hit the tail of the warps, wrap around
							if (plnWarp == plnWarpTail)
								plnWarp	= plnWarpHead->m_pnNext;

							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)
								{
								// SPECIAL CASE!!!  In multiplayer mode, we overwrite the dude numbers
								// that are assigned by the CDude constructor, instead using the
								// corresponding network ID.  This isn't a great solution, but it
								// was the best we could do given the little time we have left.
								ASSERT(pdude != NULL);
								pdude->m_sDudeNum = (int16_t)id;

								// Set general dude stuff
								SetupGeneralDude(pdude, (int16_t)pclient->GetPlayerColor(id), palevelpersist);

								// Set dude's instance ID (not to be confused with network ID)
								pclient->SetPlayerDudeID(id, pdude->GetInstanceID());

								// Special stuff for local dude
								if (id == pclient->GetID())
									SetupLocalDude(pinfo, pdude);
								}
							else
								{
								sResult = -1;
								TRACE("SetupDudes(): pwarp->WarpIn() failed.\n");
								}
							plnWarp	= plnWarp->m_pnNext;
							}
						}
					}
				else
					{
					// 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_GameSettings.m_sPlayerColorIndex, palevelpersist);

						// Special stuff just for local dude
						SetupLocalDude(pinfo, pdude);
						}
					else
						{
						sResult = -1;
						TRACE("SetupDudes(): pwarp->WarpIn() failed.\n");
						}
					}
				}
			else
				{
				TRACE("SetupDudes(): No warps!!!\n");
				rspMsgBox(RSP_MB_ICN_STOP | RSP_MB_BUT_OK, 
					"Realm Error", 
					"There are no warps in this realm!  There must be at least one warp in a realm!\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.
			{
			if (sW > 0 && sH > 0)
				{
				rspLockBuffer();

				rspRect(RSP_BLACK_INDEX, g_pimScreenBuf, sX, sY, sW, sH);

				rspUnlockBuffer();

				pinfo->m_drl.Add(sX, sY, sW, sH);
				}
			}


		////////////////////////////////////////////////////////////////////////////////
		//
		// 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 = VIEW_W * g_GameSettings.m_dGameFilmScale;
			int16_t sViewH = VIEW_H * g_GameSettings.m_dGameFilmScale;
			int16_t	sFilmX = FILM_X + (VIEW_W - sViewW) / 2;
			int16_t	sFilmY = FILM_Y + (VIEW_H - sViewH) / 2;

			// Update the camera to the new film size.
			pcamera->m_sViewW	= sViewW;
			pcamera->m_sViewH	= sViewH;
			pcamera->SetFilm(g_pimScreenBuf, 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 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;
			}


		////////////////////////////////////////////////////////////////////////////////
		//
		// Make a demo mode movie for debugging purposes.
		//
		////////////////////////////////////////////////////////////////////////////////
		void MakeDemoMovie(
			RFile* pfileDemoModeDebugMovie)					// In:  File for loading/saving demo mode debug movie
			{
			// The basic idea is that in demo record mode, we save every frame of the
			// game being recorded to a file.  Then, in demo playback mode, we compare
			// each frame of the game as it plays back to the recorded frames, and if
			// there's a difference between the frames, we highlight that difference.
			// From that, we hope that the programmer can figure out his stupid mistake
			// that somehow caused such a difference. :)  Naturally, the actual bug may
			// not be directly related to the visual difference, but it should help.
			if (pfileDemoModeDebugMovie && (GetInputMode() != INPUT_MODE_LIVE))
				{
				if (pfileDemoModeDebugMovie->IsOpen())
					{
					bool bDemoErr = false;
					RImage im;

					if (GetInputMode() == INPUT_MODE_RECORD)
						{
						// In record mode, we create an image, copy the screen buffer to it, and save it
						if (im.CreateImage(VIEW_W, VIEW_H, RImage::BMP8) == 0)
							{
							// Must lock the buffer before reading from it.
							rspLockBuffer();

							rspBlit(g_pimScreenBuf, &im, FILM_X, FILM_Y, 0, 0, VIEW_W, VIEW_H);

							// Done with the composite buffer.
							rspUnlockBuffer();

							if (im.Save(pfileDemoModeDebugMovie) != 0)
								{
								TRACE("PlayRealm(): Error writing demo movie!\n");
								rspMsgBox(RSP_MB_ICN_STOP | RSP_MB_BUT_OK, g_pszAppName, "Error writing demo movie!\n");
								bDemoErr = true;
								}
							}
						else
							{
							TRACE("PlayRealm(): Error create demo image!\n");
							rspMsgBox(RSP_MB_ICN_STOP | RSP_MB_BUT_OK, g_pszAppName, "Error creating demo image!\n");
							bDemoErr = true;
							}
						}
					else
						{
						// In playback mode, we load the previously saved image and compare it to the screen bufer
						if (im.Load(pfileDemoModeDebugMovie) == 0)
							{
							// Must lock the buffer before reading from it.
							rspLockBuffer();

							bool bMatch = true;
							int i;
							U8* pSrcLine = im.m_pData;
							U8* pDstLine = g_pimScreenBuf->m_pData + ((int32_t)FILM_Y * g_pimScreenBuf->m_lPitch) + (int32_t)FILM_X;
							int16_t sHeight = im.m_sHeight;
							U8* pSrc;
							U8* pDst;
							while (sHeight--)
								{
								pSrc = pSrcLine;
								pDst = pDstLine;
								i = im.m_sWidth;
								while (i--) 
									{
									if (*pSrc != *pDst) bMatch = false;
									pDst++;pSrc++;
									}
								pSrcLine += im.m_lPitch;
								pDstLine += g_pimScreenBuf->m_lPitch;
								}

							// If there's a mismatch, highlight the differences between the two frames
							if (!bMatch)
								{
								int i;
								U8* pSrcLine = im.m_pData;
								U8* pDstLine = g_pimScreenBuf->m_pData + ((int32_t)FILM_Y * g_pimScreenBuf->m_lPitch) + (int32_t)FILM_X;
								int16_t sHeight = im.m_sHeight;
								U8* pSrc;
								U8* pDst;
								while (sHeight--)
									{
									pSrc = pSrcLine;
									pDst = pDstLine;
									i = im.m_sWidth;
									while (i--) 
										{
										if (*pSrc == *pDst)
											*pSrc = 0;
										pDst++;pSrc++;
										}
									pSrcLine += im.m_lPitch;
									pDstLine += g_pimScreenBuf->m_lPitch;
									}

								// Copy modified image to screen buffer and update the screen
								rspBlit(&im, g_pimScreenBuf, 0, 0, FILM_X, FILM_Y, VIEW_W, VIEW_H);

								// Done with the composite buffer.
								rspUnlockBuffer();

								rspUpdateDisplay();

								// If wait-for-click is enabled, wait for click.  Otherwise, don't.
								// It will always wait for a click on the first different frame, and thereafter
								// the user can disable the waiting by clicking the right mouse button.
								if (m_bMakeDemoMovie_WaitForClick)
									{
									int16_t sButtons;
									do	{
										rspGetMouse(NULL, NULL, &sButtons);
										UpdateSystem();
										} while (sButtons);
									do {
										rspGetMouse(NULL, NULL, &sButtons);
										UpdateSystem();
										} while (!sButtons);
									if (sButtons & 2)
										m_bMakeDemoMovie_WaitForClick = false;
									do	{
										rspGetMouse(NULL, NULL, &sButtons);
										UpdateSystem();
										} while (sButtons);

									rspClearMouseInputEvents();
									}
								}
							else
								{
								// Done with the composite buffer.
								rspUnlockBuffer();
								}
							}
						else
							{
							TRACE("PlayRealm(): Error reading demo movie!\n");
							rspMsgBox(RSP_MB_ICN_STOP | RSP_MB_BUT_OK, g_pszAppName, "Error reading demo movie!\n");
							bDemoErr = true;
							}
						}

					// If there was an error, close file to turn off demo movie mode
					if (bDemoErr)
						pfileDemoModeDebugMovie->Close();
					}
				}
			}
	};


////////////////////////////////////////////////////////////////////////////////
//
// 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
					{
					ScoreSetMode(CScoreboard::SinglePlayer);
					}
				}

			return 0;
			}


		////////////////////////////////////////////////////////////////////////////////
		// End realm
		////////////////////////////////////////////////////////////////////////////////
		/* virtual */
		void EndRealm(
			CPlayInfo* pinfo)										// I/O: Play info
			{
			if (!pinfo->m_bBadRealmMP)
				{
				// If MP mode, check if score should be reset after each level
				if (pinfo->IsMP() && g_GameSettings.m_sHostResetScoresEachLevel)
					ScoreReset();
				}
			}
	};


////////////////////////////////////////////////////////////////////////////////
//
// 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,g_pimScreenBuf->m_sWidth,g_pimScreenBuf->m_sHeight);

				// Insert effects into this loop!
				RInputEvent	ie;
				ie.type	= RInputEvent::None;
				rspClearAllInputEvents();
				while (rspGetQuitStatus() == 0)
					{
					CutSceneUpdate();
					UpdateSystem();
					if (((rspGetNextInputEvent(&ie) == 1) && (ie.type == RInputEvent::Key))
						|| IsXInputButtonPressed())
						break;
					}
				}
			}


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

			// Clear any excess inputs
			rspClearAllInputEvents();

			rspLockBuffer();
			
			// Clear screen (to avoid palette flash when the hood sets its palette)
			rspRect(RSP_BLACK_INDEX, g_pimScreenBuf, 0, 0, g_pimScreenBuf->m_sWidth, g_pimScreenBuf->m_sHeight);

			rspUnlockBuffer();

			rspUpdateDisplay();

			// 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
	CNetClient*	pclient,									// In:  Client object or NULL if not network game
	CNetServer*	pserver,									// In:  Server object or NULL if not server or not network game
	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 int16_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

	// If this is the last demo level, then load the mult alpha needed for the ending
	RMultiAlpha* pDemoMultiAlpha = NULL;

	if (g_bLastLevelDemo)
		{
		sResult = rspGetResource(&g_resmgrGame, DEMO_MULTIALPHA_FILE, &pDemoMultiAlpha, RFile::LittleEndian);
		if (sResult != SUCCESS)
			TRACE("Play - Error loading multialpha mask for the ending demo\n");
		}

	// Enable RMix's autopump.
	RMix::SetAutoPump(TRUE);

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

	// Lock the composite buffer before accessing it.
	rspLockBuffer();
	// Clear screen
	rspRect(RSP_BLACK_INDEX, g_pimScreenBuf, 0, 0, g_pimScreenBuf->m_sWidth, g_pimScreenBuf->m_sHeight);
	// Lock the composite buffer now that we're done.
	rspUnlockBuffer();

	rspUpdateDisplay();

	// 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
	CPlayNet			playNet;
	CPlayStatus		playStatus;
	CPlayRealm		playRealm;
	CPlayInput		playInput;
	CPlayCutscene	playCutscene;
	CPlayScore		playScore;

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

	// Create and fill in play info
	CPlayInfo info;
	info.m_pclient = pclient;
	info.m_pserver = pserver;
	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;

	// Try opening the realms.ini file on the HD path first, if that fails go to the CD
	sResult = prefsRealm.Open(FullPathHD(g_GameSettings.m_pszRealmPrefsFile), "rt");
	if (sResult != 0)
		sResult = prefsRealm.Open(FullPathCD(g_GameSettings.m_pszRealmPrefsFile), "rt");
	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();

		// 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 ***/
	#ifdef WIN32
					if (info.IsMP())
						OpenLogFile();
	#endif

#ifdef MOBILE
					bool doAutoSaveGame = false; //This is set to true when you complete a level, so it's auto saved when the next realm starts
#endif
					/*** 12/5/97 AJC ***/
					// Outer loop keeps playing one realm after another
					do	{
						int32_t startRealmMS = -1;

						// Clear game status
						info.SetGameState_Ok();


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

						// Sounds playing during the load suck.
						SynchronousSampleAbortion();

						// enable this level in the level select
						
						if (info.m_sRealmNum >= 0 && info.m_bGauntlet == false)
						{
							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;
						}
							

						// Start the cutscene
						playgroup.StartCutscene(&info);

						// Prepare realm
						sResult = playgroup.PrepareRealm(&info);
						if (!sResult)
							{

							// Wait until realm is ready
							bool bRealmReady = false;
							do	{
								sResult = playgroup.IsRealmReady(&info, &bRealmReady);
								} while (!sResult && !bRealmReady);
							if (!sResult && bRealmReady)
								{
								if ((!info.IsMP()) && (info.m_sRealmNum == 1))
									UnlockAchievement(ACHIEVEMENT_START_SECOND_LEVEL);
#ifdef MOBILE//Tap screen to get past
								AndroidSetScreenMode(TOUCH_SCREEN_BLANK_TAP);
#endif
								// do the cutscene
								playgroup.DoCutscene(&info);
#ifdef MOBILE
								if (inputMode == INPUT_MODE_PLAYBACK)
									AndroidSetScreenMode(TOUCH_SCREEN_BLANK_TAP); //DEMO playback
								else
									AndroidSetScreenMode(TOUCH_SCREEN_GAME);
#endif
								// End the cutscene
								playgroup.EndCutscene(&info);
								// If multiplayer mode, set up the scoring mode from
								// the flags passed into play.
								if (pclient)
								{
									info.Realm()->m_sKillsGoal = sKillLimit;
									info.Realm()->m_lScoreInitialTime = info.Realm()->m_lScoreTimeDisplay = (int32_t)sTimeLimit * (int32_t)60000;
									
									// If Rejuvenate is allowed, then its not last man standing
									if (bRejuvenate)
									{
										if (sKillLimit > 0 && sTimeLimit > 0)
											info.Realm()->m_ScoringMode = CRealm::MPTimedFrag;

										if (sKillLimit <= 0 && sTimeLimit > 0)
											info.Realm()->m_ScoringMode = CRealm::MPTimed;

										if (sKillLimit > 0 && sTimeLimit <= 0)
											info.Realm()->m_ScoringMode = CRealm::MPFrag;

										if (sKillLimit <= 0 && sTimeLimit <= 0)
										{
											info.Realm()->m_sKillsGoal = KILLS_LIMIT_DEFAULT;
											info.Realm()->m_ScoringMode = CRealm::MPFrag;
										}
									}
									// Last man standing mode
									else
									{
										if (sKillLimit > 0 && sTimeLimit > 0)
											info.Realm()->m_ScoringMode = CRealm::MPLastManTimedFrag;

										if (sKillLimit > 0 && sTimeLimit <= 0)
											info.Realm()->m_ScoringMode = CRealm::MPLastManFrag;

										if (sKillLimit <= 0 && sTimeLimit > 0)
											info.Realm()->m_ScoringMode = CRealm::MPLastManTimed;

										if (sKillLimit <= 0 && sTimeLimit <= 0)
											info.Realm()->m_ScoringMode = CRealm::MPLastMan;
									}
								}
								// Start realm
								sResult = playgroup.StartRealm(&info);
								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);
										}

									StatsAreAllowed = !info.IsMP();  // !!! FIXME: we currently only track for single-player (because we don't check that kills belong to the local player, etc).
									startRealmMS = rspGetMilliseconds();

#ifdef MOBILE
									if (doAutoSaveGame)
									{
										TRACE("Doing autosave");
										char  szFile[256];
										snprintf(szFile, sizeof (szFile), "%s/auto.gme", SAVEGAME_DIR);
										if (Game_SavePlayersGame(szFile, info.Realm()->m_flags.sDifficulty) == SUCCESS)
										{
											TRACE("Auto Save success");
										}
										else
											TRACE("Auto Save FAILED");

										doAutoSaveGame= false; //reset
									}
#endif
									// Inner loop plays current realm until it's done
									RInputEvent	ie;
									do	{

										if ((info.Realm()->m_flags.sDifficulty != 11) && (!g_bLastLevelDemo))
											Flag_Achievements &= ~FLAG_HIGHEST_DIFFICULTY;

										// As always...
										UpdateSystem();

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

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

										//This is where we assign local input
										playgroup.CoreLoopUserInput(&info, &ie);

#ifdef MOBILE //Tap screen to show menu
										if (info.LocalDudePointer()->IsDead())
										{
											if (!info.m_bInMenu)
												AndroidSetScreenMode(TOUCH_SCREEN_BLANK_TAP);
										}
#endif
										


										// Render:

										// This requires access to the composite buffer so lock it down.
										rspLockBuffer();
										//This is where we process 'previous' input
										playgroup.CoreLoopRender(&info);

										playgroup.CoreLoopRenderOnTop(&info);

										// Release the composite buffer now that we're done.
										rspUnlockBuffer();

										// Draw to the screen.
										playgroup.CoreLoopDraw(&info);

										// 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)
										{
										RRect rect(0,40,VIEW_W,VIEW_H);
										MartiniDo(g_pimScreenBuf,
													 0,
													 0,
													 pDemoMultiAlpha,
													 15000,
													 24,
													 5000,
													 9000,
													 &rect,
													 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. ***
											}

										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)!
										ScoreDisplayHighScores(info.Realm(), info.Client(), MP_HIGH_SCORES_MAX_TIME );
										}

									// End realm
									const bool tmpStatsAreAllowed = StatsAreAllowed;
									StatsAreAllowed = false;
									playgroup.EndRealm(&info);
									StatsAreAllowed = tmpStatsAreAllowed;

									}
								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);

						if (StatsAreAllowed)
							{
							Stat_LevelsPlayed++;
							if ((!sResult) && (info.LocalDudePointer()->IsDead()))
								Stat_Deaths++;
							}
						StatsAreAllowed = false;

						#if WITH_STEAMWORKS
						RequestSteamStatsStore();  // this is a good time to push any updated stats from the level.
						#endif

						// Figure out what to do next (same realm, next realm, game over, etc.)
						if (!sResult)
							{
							if (info.JustOneRealm() == true && info.IsRestartingRealm() == false)
								{
								info.SetGameState_GameOver();
								}
							else if (info.IsNextRealm())
								{
								CDude *pDude = info.LocalDudePointer();
								// this is how the toolbar display calculates health.
								const double health = (pDude->GetHealth()*100/pDude->m_sOrigHitPoints);
								if (health < 10)
									UnlockAchievement(ACHIEVEMENT_COMPLETE_LEVEL_ON_LOW_HEALTH);

								if (info.Realm()->m_sPopulation != 0)
									Flag_Achievements &= ~FLAG_KILLED_EVERYTHING;

								if (info.m_sRealmNum == 9)
									UnlockAchievement(ACHIEVEMENT_COMPLETE_LEVEL_10);

								info.m_sRealmNum++;
								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
#ifdef MOBILE
										if (!bGauntlet) //Dont autosave if playing a challenge!
											doAutoSaveGame = true;
#endif
										break;

									case 1:	// No such realm number
										if (info.IsMP())
											{
											// Multiplayer just keeps wrapping around
											info.m_sRealmNum = 0;
											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)
												{
												// 09/12/97 MJR - 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.
												info.m_bBadRealmMP = true;
												TRACE("Play(): Couldn't get info for realm #%hd!\n", (int16_t)info.m_sRealmNum);
												}
											}
										else
											{
											// 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)
											{
#ifdef KID_FRIENDLY_OPTION
												if (bAddOn == 3)
												{
													g_GameSettings.m_sCompletedAllLevelsMode = TRUE;
												}
#endif
												g_bLastLevelDemo = true;
											}
											else
												info.SetGameState_GameOver();
											}
										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);
		}

	rspLockBuffer();

	// Clear screen
	rspRect(RSP_BLACK_INDEX, g_pimScreenBuf, 0, 0, g_pimScreenBuf->m_sWidth, g_pimScreenBuf->m_sHeight);

	rspUnlockBuffer();

	rspUpdateDisplay();

	// Disable autopump.
	RMix::SetAutoPump(FALSE);

	// Abort all playing sounds.
	SynchronousSampleAbortion();

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

	return sResult;
	}


////////////////////////////////////////////////////////////////////////////////
//
// Snap picture to disk.
//
////////////////////////////////////////////////////////////////////////////////
extern void Play_SnapPicture(void)
	{
	// Feedback is nice.
	PlaySample(
		g_smidClick,
		SampleMaster::UserFeedBack);

	// Set up palette for snap shots once.
	RPal	palPicture;
	if (palPicture.CreatePalette(RPal::PDIB) == 0)
		{
		palPicture.m_sStartIndex	= 0;
		palPicture.m_sNumEntries	= 256;
		rspGetPaletteEntries(
			palPicture.m_sStartIndex,		// Palette entry to start copying to (has no effect on source!)
			palPicture.m_sNumEntries,		// Number of palette entries to do
			palPicture.Red(0),				// Pointer to first red component to copy to
			palPicture.Green(0),				// Pointer to first green component to copy to
			palPicture.Blue(0),				// Pointer to first blue component to copy to
			palPicture.m_sPalEntrySize);	// Number of bytes by which to increment pointers after each copy

		// Store screen buffer's actual type and palette
		RImage::Type	typeOrig		= g_pimScreenBuf->m_type;
		RPal*				ppalOrig		= g_pimScreenBuf->m_pPalette;

		// Temporarily change its type and palette
		g_pimScreenBuf->m_type		= RImage::BMP8;
		g_pimScreenBuf->m_pPalette	= &palPicture;

		// Save picture to file
		char szFileName[RSP_MAX_PATH];
		sprintf(szFileName, "PostalShot%03ld.bmp", ms_lCurPicture++);

		// This will require direct access to the composite buffer.
		rspLockBuffer();

		g_pimScreenBuf->SaveDib(szFileName);

		rspUnlockBuffer();

		// Restore original type and palette
		g_pimScreenBuf->m_type		= typeOrig;
		g_pimScreenBuf->m_pPalette	= ppalOrig;
		}
	}


////////////////////////////////////////////////////////////////////////////////
//
// 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.
	{
	bool	bAcceptChoice	= true;	// Assume accepting choice.

	switch (sMenuItem)
		{
		case 0:	// Continue.
#ifdef MOBILE
			continueIsRestart = true; //Now the continue button will restart the realm
#endif
			ms_menuaction	= MenuActionEndMenu;
			break;
		case 1:	// Save game
			ms_menuaction	= MenuActionSaveGame;
			break;
		case 2:	// Options.
			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
	int16_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
	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");
			rspMsgBox(RSP_MB_ICN_STOP | RSP_MB_BUT_OK, g_pszCriticalErrorTitle, g_pszBadPath_s_s, "Realm", (char*)strSection);
			}

		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
	int16_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.
		pmenu->ami[sInputIndex].sEnabled	= (g_GameSettings.m_ulUnlockedLevels & (1 << sInputIndex)) ? TRUE : FALSE;
		}

	return sRes;
	}

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