/*
 ==================
 UpdatePeerTiming
 
 Calculates one way latency based on local and remote times
 ==================
 */
void UpdatePeerTiming( netPeer_t *peer, int remoteMilliseconds ) {
	peer->lastPacketAsyncTic = asyncTicNum;
	peer->lastPacketTime = SysIphoneMilliseconds();
	peer->lastTimeDelta = abs( remoteMilliseconds - peer->lastPacketTime );
	if ( peer->lowestTimeDelta == 0 || peer->lastTimeDelta < peer->lowestTimeDelta ) {
		peer->lowestTimeDelta = peer->lastTimeDelta;
	}
	peer->oneWayLatency = peer->lastTimeDelta - peer->lowestTimeDelta;
	if ( peer->oneWayLatency < 0 ) {
		// this can happen if we context switched at a bad time
		peer->lowestTimeDelta = peer->lastTimeDelta;
		peer->oneWayLatency = 0;
	}
//	printf( "OWL:%i timeDelta:%i  lowest:%i\n", peer->oneWayLatency,
//		   peer->lastTimeDelta, peer->lowestTimeDelta );
}
void iphoneMultiplayerMenu() {
	if ( gameSocket <= 0 ) {
		// no socket, so no multiplayer
		TerminateGameService();		// don't advertise for any more new players
		setupPacket.gameID = 0;		// stop sending packets
		menuState = IPM_MAIN;
		return;
	}

	boolean	server = ( setupPacket.gameID == localGameID );
	
	// different screen when selecting a map to play
	if ( netMenu == NM_MAP_SELECT ) {
		mapStart_t	map;
		if ( !iphoneMapSelectMenu( &map ) ) {
			// haven't selected anything yet
			return;
		}
		netMenu = NM_MAIN;
		if ( map.map != -1 ) {
			// selected something new, didn't hit the back arrow
			setupPacket.map = map;
		}
	}
	
	#ifndef IPAD
	else if ( netMenu == NM_OPTIONS ) {
		
		Cvar_SetValue( fragLimit->name, setupPacket.fraglimit );
		if ( iphoneSlider( 104, 64, 272, 40, "frag limit", fragLimit, 0, 20, SF_INTEGER ) ) {
			if ( server ) {
				setupPacket.fraglimit = fragLimit->value;
			}
		}
		
		Cvar_SetValue( timeLimit->name, setupPacket.timelimit );
		if ( iphoneSlider( 104, 64+56, 272, 40, "time limit", timeLimit, 0, 20, SF_INTEGER ) ) {
			if ( server ) {
				setupPacket.timelimit = timeLimit->value;
			}
		}
		if ( BackButton() ) {
			netMenu = NM_MAIN;
		}
		return;
	}
	#else
	Cvar_SetValue( fragLimit->name, setupPacket.fraglimit );
	if ( iphoneSlider( 314, 410, 400, 40, "frag limit", fragLimit, 0, 20, SF_INTEGER ) ) {
		if ( server ) {
			setupPacket.fraglimit = fragLimit->value;
		}
	}
	
	Cvar_SetValue( timeLimit->name, setupPacket.timelimit );
	if ( iphoneSlider( 314, 410+58, 400, 40, "time limit", timeLimit, 0, 20, SF_INTEGER ) ) {
		if ( server ) {
			setupPacket.timelimit = timeLimit->value;
		}
	}
	
	#endif
	
	if ( !btnDeathmatch.texture ) {
		// initial setup
		#ifdef IPAD
		SetButtonPicsAndSizes( &btnDeathmatch, "iphone/deathmatch.tga", "Deathmatch", 512-128-50, 150, 128, 128 );
		SetButtonPicsAndSizes( &btnCoop, "iphone/co-op.tga", "Cooperative", 512+50, 150, 128, 128 );
		#else
		SetButtonPicsAndSizes( &btnDeathmatch, "iphone/deathmatch.tga", "Deathmatch", 4+48, 64, 96, 96 );
		SetButtonPicsAndSizes( &btnCoop, "iphone/co-op.tga", "Cooperative", 480-148, 64, 96, 96 );		
		#endif
	}

	if ( BackButton() ) {
		if ( server ) {
			TerminateGameService();		// don't advertise for any more new players
			setupPacket.gameID = 0;		// stop sending packets
		}
		menuState = IPM_MAIN;
	}
	
	if ( !server ) {
		// we aren't the server
		// send our join packet every frame
		SendJoinPacket();
		
		if ( setupPacketFrameNum < iphoneFrameNum - 30 ) {
			// haven't received a current server packet
			char	str[1024];
			struct sockaddr_in *sin = (struct sockaddr_in *)&netServer.address;
			byte *ip = (byte *)&sin->sin_addr;
			sprintf( str, "Joining server at %i.%i.%i.%i:%i\n", ip[0], ip[1], ip[2], ip[3], 
					ntohs( sin->sin_port ) );
			#ifdef IPAD
			iphoneCenterText( 512, 384, 1, str );			
			#else	
			iphoneCenterText( 240, 160, 0.75, str );
			#endif
			return;
		}
	} else {
		// cull out any players that haven't given us a packet in a couple seconds
		int	now = SysIphoneMilliseconds();
		for ( int i = 1 ; i < MAXPLAYERS ; i++ ) {
			if ( setupPacket.playerID[i] && now - netPlayers[i].peer.lastPacketTime > 1000 ) {
				printf( "Dropping player %i: last:%i now:%i\n", i, netPlayers[i].peer.lastPacketTime, now );
				setupPacket.playerID[i] = 0;
			}
		}
	}
	
	// draw the level and allow clicking to change
	Cvar_SetValue( mpDataset->name, setupPacket.map.dataset );
	Cvar_SetValue( mpEpisode->name, setupPacket.map.episode );
	Cvar_SetValue( mpMap->name, setupPacket.map.map );
	Cvar_SetValue( mpSkill->name, setupPacket.map.skill );
	
	// map select button / display
	#ifdef IPAD
	if ( NewTextButton( &btnMap,  FindMapName( mpDataset->value, mpEpisode->value, mpMap->value ), 512 - 200, 80, 400, 48 ) ) {
	#else
	if ( NewTextButton( &btnMap,  FindMapName( mpDataset->value, mpEpisode->value, mpMap->value ), 64, 0, 480-128, 48 ) ) {	
	#endif
		
		if ( server ) {
			// clients can't go into this menu
			netMenu = NM_MAP_SELECT;
		}
	}

	
	if ( setupPacket.deathmatch ) {
		btnDeathmatch.buttonFlags = 0;
		btnCoop.buttonFlags = BF_DIMMED;
	} else {
		btnDeathmatch.buttonFlags = BF_DIMMED;
		btnCoop.buttonFlags = 0;
	}
	
	if ( HandleButton( &btnDeathmatch ) ) {
		if ( server ) {
			Cvar_SetValue( mpDeathmatch->name, 3 );	// weapons stay, items respawn rules
			setupPacket.deathmatch = mpDeathmatch->value;
		}
	}
	if ( HandleButton( &btnCoop ) ) {
		if ( server ) {
			Cvar_SetValue( mpDeathmatch->name, 0 );		
			setupPacket.deathmatch = mpDeathmatch->value;
		}
	}	
#ifndef IPAD
	if ( NewTextButton( &btnNetSettings, "Settings",  240-64, 64+24, 128, 48 ) ) {
		netMenu = NM_OPTIONS;
	}
#endif
			
	for ( int i = 0 ; i < 4 ; i ++ ) {
		#ifdef IPAD
		int x = 320 + ( 64+45) * i;
		int y = 64+260;
		#else
		int x = 45 + ( 64+45) * i;
		int y = 64+128;
		#endif
		// FIXME: show proper player colors
		byte	color[4][4] = { { 0, 255, 0, 255 }, { 128, 128, 128, 255 }, { 128,64,0, 255 }, {255,0,0, 255 } };
		glColor4ubv( color[i] );
		PK_DrawTexture( PK_FindTexture( "iphone/multi_backdrop.tga" ), x, y );
		glColor4f( 1, 1, 1, 1 );
		if ( setupPacket.playerID[i] == playerID ) {
			// bigger outline for your player slot
			PK_StretchTexture( PK_FindTexture( "iphone/multi_frame.tga" ), x, y, 64, 64 );
		}
		
		
		// draw doom guy face
		if ( setupPacket.playerID[i] != 0 ) {
			PK_DrawTexture( PK_FindTexture( "iphone/multi_face.tga" ), x, y );
#if 0			
			// temp display IP address
			byte *ip = (byte *)&setupPacket.address[i].sin_addr;
			iphoneDrawText( x-16, (i&1) ? y+16 : y+48, 0.75, va("%i.%i.%i.%i", ip[0], ip[1], ip[2], ip[3] ) );
#endif
		}
		
	}
	
	if ( server ) {
		// flash a tiny pic when transmitting
		if ( iphoneFrameNum & 1 ) {
			glColor4f( 1,1,1,1 );
		} else {
			glColor4f( 0.5,0.5,0.5,1 );
		}
		#ifdef IPAD
		iphoneCenterText( 1024 - 20, 768 - 20, 1, "*" );
		#else
		iphoneCenterText( 470, 310, 0.75, "*" );
		#endif
		glColor4f( 1,1,1,1 );
	}
	if ( setupPacketFrameNum == iphoneFrameNum ) {
	#ifdef IPAD
		iphoneCenterText( 1024 - 40, 768 - 20, 1, "*" );
	#else
		iphoneCenterText( 450, 310, 0.75, "*" );
	#endif
	}
//	iphoneDrawText( 0, 310, 0.75, va("%i:%i", localGameID, setupPacket.gameID ) );

	// only draw the start button if we have at least two players in game
	int	numPlayers = 0;
	for ( int i = 0 ; i < MAXPLAYERS ; i++ ) {
		if ( setupPacket.playerID[i] != 0 ) {
			numPlayers++;
		}
	}
	
	if ( numPlayers > 1 ) {
		if ( server ) {
			static ibutton_t btnStart;
			#ifdef IPAD
			if ( NewTextButton( &btnStart, "Start game", 512-80, 768-100, 160, 48) ) {
			#else
			if ( NewTextButton( &btnStart, "Start game", 240-80, 320-48, 160, 48 ) ) {
			#endif
				setupPacket.startGame = 1;
				StartNetGame();
				TerminateGameService();		// don't advertise for any more new players
				return;
			}
		} else {
		#ifdef IPAD	
			iphoneCenterText( 512, 68-25, 1, "Waiting for server to start the game" );
		#else
			iphoneCenterText( 240, 320-10, 0.60, "Waiting for server to start the game" );
		#endif
		}
	} else {
		byte *ip = (byte *)&gameSocketAddress.sin_addr;
		#ifdef IPAD
		iphoneCenterText( 512, 768-25, 1, va("Waiting for players on %i.%i.%i.%i", 
													ip[0], ip[1], ip[2], ip[3] ) );
		iphoneCenterText( 512, 27, 0.75, "Attention: Multiplayer requires a WiFi connection");
		iphoneCenterText( 512, 50, 0.75, "that doesn't block UDP port 14666");
		#else
		iphoneCenterText( 240, 320-8, 0.60, va("Waiting for players on %i.%i.%i.%i", 
											   ip[0], ip[1], ip[2], ip[3] ) );
		iphoneCenterText( 240, 320-50, 0.60, "Attention: Multiplayer requires a WiFi connection");
		iphoneCenterText( 240, 320-30, 0.60, "that doesn't block UDP port 14666");		
		#endif
	}
	//	static ibutton_t btnStart;
	//	NewTextButton( &btnStart, "Start game", 512-80, 768-100, 160, 48);
}

#ifndef IPAD
static ibutton_t	optionButtons[2][6];
static ibutton_t	defaultsButton;
boolean OptionButton( int col, int row, const char *title ) {
	assert( col >= 0 && col < 2 && row >= 0 && row < 6 );
	return NewTextButton( &optionButtons[col][row], title, 10 + 235 * col, 64 + 50 * row, 225, 48 );
}
#endif

/*
 ==================
 iphoneOptionsMenu
 
 ==================
 */
void iphoneOptionsMenu() {	
	if ( BackButton() ) {
		menuState = IPM_CONTROLS;
	}

	boolean musicState = music->value;
	if ( SysIPhoneOtherAudioIsPlaying() ) {
		// music always off when ipod music is playing
		musicState = false;
	}

	if ( NewTextButton( &defaultsButton, "Defaults", 240-225/2, 2, 225, 48 ) ) {
		// reset all cvars except the reverse-landscape mode value
		float value = revLand->value;
		Cvar_Reset_f();
		Cvar_SetValue( revLand->name, value );
		HudSetForScheme(0);
		iphoneStartMusic();
	}
	
	if ( OptionButton( 0, 0, autoUse->value ? "Auto use: ON" : "Auto use: OFF" ) ) {
		Cvar_SetValue( autoUse->name, !autoUse->value );
	}
	if ( OptionButton( 0, 1, statusBar->value ? "Status bar: ON" : "Status bar: OFF" ) ) {
		Cvar_SetValue( statusBar->name, !statusBar->value );
	}
	if ( OptionButton( 0, 2, touchClick->value ? "Touch click: ON" : "Touch click: OFF" ) ) {
		Cvar_SetValue( touchClick->name, !touchClick->value );
	}
	if ( OptionButton( 0, 3, messages->value ? "Text messages: ON" : "Text messages: OFF" ) ) {
		Cvar_SetValue( messages->name, !messages->value );
	}
	if ( OptionButton( 1, 0, drawControls->value ? "Draw controls: ON" : "Draw controls: OFF" ) ) {
		Cvar_SetValue( drawControls->name, !drawControls->value );
	}
	if ( OptionButton( 1, 1, musicState ? "Music: ON" : "Music: OFF" ) ) {
		if ( !SysIPhoneOtherAudioIsPlaying() ) {
			Cvar_SetValue( music->name, !music->value );
			if ( music->value ) {
				iphoneStartMusic();
			} else {
				iphoneStopMusic();
			}
		}
	}
	if ( OptionButton( 1, 2, centerSticks->value ? "Center sticks: ON" : "Center sticks: OFF" ) ) {
		Cvar_SetValue( centerSticks->name, !centerSticks->value );
	}
	if ( OptionButton( 1, 3, rampTurn->value ? "Ramp turn: ON" : "Ramp turn: OFF" ) ) {
		Cvar_SetValue( rampTurn->name, !rampTurn->value );
	}
}	

/*
 ===================
 iphoneIntermission
 
 The end-of-level switch was just hit, note the state and awards
 for the map select menu
 ===================
 */
void iphoneIntermission( wbstartstruct_t* wb ) {
	if ( deathmatch || netgame ) {
		// no achievements in deathmatch mode
		return;
	}
	
	// find the current episode / map combination 
	
	// if a mapStat_t doesn't exist for this yet, create one
	
	// mark this level / skill combination as tried
	mapStats_t *cms = FindMapStats( playState.map.dataset, playState.map.episode, playState.map.map, true );
	if ( !cms ) {
		return;
	}
	
	int skill = playState.map.skill;
	cms->completionFlags[skill] |= MF_COMPLETED;
	
	// add the awards
	if ( wb->plyr[0].stime < wb->partime ) {
		cms->completionFlags[skill] |= MF_TIME;
	}
	
	int numkills = 0;
	int numsecrets = 0;
	int numitems = 0;
	for ( int i = 0 ; i < MAXPLAYERS ; i++ ) {
		if ( wb->plyr[i].in ) {
			numkills += wb->plyr[i].skills;
			numitems += wb->plyr[i].sitems;
			numsecrets += wb->plyr[i].ssecret;
		}
	}
	if ( numkills >= wb->maxkills ) {
		cms->completionFlags[skill] |= MF_KILLS;
	}
	if ( numitems >= wb->maxitems ) {
		cms->completionFlags[skill] |= MF_TREASURE;
	}
	if ( numsecrets >= wb->maxsecret ) {
		cms->completionFlags[skill] |= MF_SECRETS;
	}	
}

/*
 ===================
 iphoneStartLevel
 
 Do a savegame with the current state
 ===================
 */
void iphoneStartLevel() {
	if ( deathmatch || netgame ) {
		// no achievements in deathmatch mode
		
		// reset the levelTimer
		if ( levelTimer && setupPacket.timelimit > 0 ) {
			// 30 hz, minutes
			levelTimeCount = setupPacket.timelimit * 30 * 60;
		}
		
		return;
	}
	playState.map.map = gamemap;

	// automatic save game
	G_SaveGame( 0, "entersave" );
	G_DoSaveGame(true);

	// mark this level as tried
	mapStats_t *cms = FindMapStats( playState.map.dataset, playState.map.episode, playState.map.map, true );
	if ( cms ) {
		cms->completionFlags[playState.map.skill] |= MF_TRIED;
	}
}


/*
 ===================
 DrawLiveBackground
 
 Draw a randomish moving cloudy background
 ===================
 */
void DrawLiveBackground() {
	static float	bgVectors[2][2] = { { 0.01, 0.015 }, { -0.01, -0.02 } };
	float	fade[2];
	
	// slide and fade a couple textures around
	static float	tc[2][4][2];
	for ( int i = 0 ; i < 2 ; i++ ) {		
		int		ofs = iphoneFrameNum + i * 32;
		float	dist = ( ofs & 63 );
		for ( int j = 0 ; j < 2 ; j ++ ) {
			for ( int k = 0 ; k < 2 ; k++ ) {
				if ( rand()&1 ) {
					if ( bgVectors[j][k] < 0.03 ) {
						bgVectors[j][k] += 0.0001;
					}
				} else {
					if ( bgVectors[j][k] > -0.03 ) {
						bgVectors[j][k] -= 0.0001;
					}
				}
			}
		}
		fade[i] = sin( ( dist - 16 ) / 32.0 * M_PI ) * 0.5 + 0.5;
		fade[i] *= 0.7;
		
		for ( int j = 0 ; j < 2 ; j++ ) {
			tc[i][0][j] += bgVectors[i][j];
			tc[i][0][j] -= floor( tc[i][0][j] );
		}
		tc[i][1][0] = tc[i][0][0]+1;
		tc[i][1][1] = tc[i][0][1]+0;
		
		tc[i][2][0] = tc[i][0][0]+0;
		tc[i][2][1] = tc[i][0][1]+1;
		
		tc[i][3][0] = tc[i][0][0]+1;
		tc[i][3][1] = tc[i][0][1]+1;
	}
	
	
	
	// Fill rate performance is an issue just for two scrolling layers under
	// modest GUI objects.  Using a PVR2 texture and a single multitexture
	// pass helps.  If all the GUI objects were drawn with depth buffering,
	// the surface rejection would help out, but bumping depth after every
	// draw would be a bit of a chore.
	
#if 0	
	glClear( GL_DEPTH_BUFFER_BIT );
	glDepthMask( 1 );	// write the depth buffer
	glEnable( GL_DEPTH_TEST );	// depth test this background
#endif
	
	PK_BindTexture( PK_FindTexture( "iphone/livetile_1.tga" ) );
	
	glDisable( GL_BLEND );
	glDisable( GL_DEPTH_TEST );

	// multitexture setup
	glActiveTexture( GL_TEXTURE1 );
	glClientActiveTexture( GL_TEXTURE1 );
	glEnable( GL_TEXTURE_2D );
	PK_BindTexture( PK_FindTexture( "iphone/livetile_1.tga" ) );	
	glTexCoordPointer( 2, GL_FLOAT, 8, tc[1][0] );
	glEnableClientState( GL_TEXTURE_COORD_ARRAY );
	
	glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_ADD );
//	glColor4f( fade[0], fade[0], fade[0], fade[1] );
	glBegin( GL_TRIANGLE_STRIP );
	
#ifdef IPAD
	glTexCoord2f( tc[0][0][0], tc[0][0][1] );	glVertex3f( 0, 0, 0.5 );
	glTexCoord2f( tc[0][1][0], tc[0][1][1] );	glVertex3f( 1024, 0, 0.5 );
	glTexCoord2f( tc[0][2][0], tc[0][2][1]+1 );	glVertex3f( 0, 768, 0.5 );
	glTexCoord2f( tc[0][3][0], tc[0][3][1]+1 );	glVertex3f( 1024, 768, 0.5 );
#else
	glTexCoord2f( tc[0][0][0], tc[0][0][1] );	glVertex3f( 0, 0, 0.5 );
	glTexCoord2f( tc[0][1][0], tc[0][1][1] );	glVertex3f( 480, 0, 0.5 );
	glTexCoord2f( tc[0][2][0], tc[0][2][1]+1 );	glVertex3f( 0, 320, 0.5 );
	glTexCoord2f( tc[0][3][0], tc[0][3][1]+1 );	glVertex3f( 480, 320, 0.5 );
#endif
	glEnd();
	
	// unbind the second texture
	glBindTexture( GL_TEXTURE_2D, 0 );
	glDisable( GL_TEXTURE_2D );
	glDisableClientState( GL_TEXTURE_COORD_ARRAY );
	glActiveTexture( GL_TEXTURE0 );
	glClientActiveTexture( GL_TEXTURE0 );

	glColor4f( 1, 1, 1, 1 );
	glEnable( GL_BLEND );	
#if 0	
	// Enable depth test, but not depth writes, so the tile dorting
	// minimizes the amount of time drawing the background when it
	// is mostly covered.
	glEnable( GL_DEPTH_TEST );
	glDepthMask( 0 );
#endif	
}

#define MAX_PACKET_LOG	64
int	currentPacketLog;
int	packetLogMsec[MAX_PACKET_LOG];

void iphonePacketTester() {
	glClear( GL_COLOR_BUFFER_BIT );
	
	if ( BackButton() ) {
		menuState = IPM_MAIN;
		return;
	}
	
	struct sockaddr_in sender;
	unsigned senderLen = sizeof( sender );
	byte	buffer[1024];
	while( 1 ) {
		int r = recvfrom( gameSocket, buffer, sizeof( buffer ), 0, (struct sockaddr *)&sender, &senderLen );
		if ( r == -1 ) {
			break;
		}
		packetSetup_t *sp = (packetSetup_t *)buffer;
		if ( sp->sendCount == setupPacket.sendCount ) {
			Com_Printf( "Duplicated receive: %i\n", sp->sendCount );
		} else if ( sp->sendCount < setupPacket.sendCount ) {
			Com_Printf( "Out of order receive: %i < %i\n", sp->sendCount, setupPacket.sendCount );
		} else if ( sp->sendCount > setupPacket.sendCount + 1 ) {
			Com_Printf( "Dropped %i packets before %i\n", sp->sendCount - 1 - setupPacket.sendCount, sp->sendCount );
		}
		setupPacket = *sp;
		packetLogMsec[currentPacketLog&(MAX_PACKET_LOG-1)] = SysIphoneMilliseconds();
		currentPacketLog++;
	}
	
	color4_t activeColor = { 0, 255, 0, 255 };
	for ( int i = 1 ; i < MAX_PACKET_LOG ; i++ ) {
		int	t1 = packetLogMsec[(currentPacketLog - i)&(MAX_PACKET_LOG-1)];
		int	t2 = packetLogMsec[(currentPacketLog - i - 1)&(MAX_PACKET_LOG-1)];
		int	msec = t1 - t2;
		R_Draw_Fill( 0, i * 4, msec, 2, activeColor );
	}
}
/*
 ==================
 iphoneAsyncTic
 
 This is called by a 30hz scheduled timeer in the main application thread.
 Commands are generated and sent to the packet server
 on this regular basis, regardless of the frame rate held by iphoneFrame().
 This thread should be higher priority, so it always interrupts the game
 thread.
 
 It might be nice to run the game tics here, but the rendering code is not
 thread safe with the game, so it isn't an option.
 ==================
 */
void iphoneAsyncTic() {
	// log our timing accuracy (seems to be within a few msec -- not bad)
	static int prev;
	int	now = SysIphoneMilliseconds();
	asyncStats_t	*stats = &asyncStats[asyncTicNum&(MAX_ASYNC_LOGS-1)];
	asyncTicNum++;

	memset( stats, 0, sizeof( *stats ) );
	stats->msecFromLast = now - prev;
	stats->msecToExecute = 0;

	prev = now;
	
	// don't generate any commands while loading levels
	if ( iphoneFrameNum == levelLoadFrameNum  ) {
		return;
	}

	int	afterLock = SysIphoneMilliseconds();
	
	// latch the current touches for processing
	for ( int i = 0 ; i < MAX_TOUCHES ; i++ ) {
		touch_t *t = &sysTouches[i];
		
		gameTouches[i] = *t;
		
		// handle the special case of a touch that went down and up
		// inside a single frame
		if ( t->stateCount == -1 ) {
			gameTouches[i].stateCount = 1;
			t->stateCount = 0;
			t->down = false;
		} else {
			t->stateCount++;
		}
	}	
	
	//---------------------------------
	// Create local user command
	// 
	// We always create one, but it might not wind up being used for a game
	// tic if it doesn't make it to the server at the right time.
	//---------------------------------
	ticcmd_t cmd;
	iphoneBuildTiccmd( &cmd );
	
	//---------------------------------
	// If we are a client, send our command to the server
	//---------------------------------
	if ( consoleplayer != 0 ) {
		if ( gameID != 0 && netgame && !netGameFailure ) {
			stats->latency = packetSequence - lastServerPacket.packetAcknowledge;
			if ( ShouldSendPacket( &netServer, packetSequence - lastServerPacket.packetAcknowledge ) ) {
				packetClient_t	cp;
				memset( &cp, 0, sizeof( cp ) );
				cp.packetType = PACKET_VERSION_CLIENT;
				cp.gameID = gameID;
				cp.packetAcknowledge = lastServerPacket.packetSequence;
				cp.milliseconds = SysIphoneMilliseconds();
				cp.packetSequence = packetSequence++;
				cp.consoleplayer = consoleplayer;
				cp.gametic = gametic;
				cp.cmd = cmd;
				
				idGameCenter::SendPacketToPlayerUnreliable( serverGameCenterID, &cp, sizeof ( cp ) );
			}
		}
	} else {
		
		// take our command directly
		netPlayers[0].pc.cmd = cmd;
		netPlayers[0].pc.gametic = gametic;
		netPlayers[0].peer.lastPacketTime = now;
		
		
		//---------------------------------
		// Decide if we want to latch the current commands for execution by the game
		//
		//---------------------------------
		int	ticIndex = maketic & BACKUPTICMASK;
		
		int	worstTic = gametic;
		for ( int i = 0 ; i < MAXPLAYERS ; i++ ) {
			if ( playeringame[i] ) {
				netcmds[i][ticIndex] = netPlayers[i].pc.cmd;
				if ( netPlayers[i].pc.gametic < worstTic ) {
					worstTic = netPlayers[i].pc.gametic;
				}
			}
		}
		
		// only let the server get a few tics ahead of any client, so if
		// anyone is having significant net delivery problems, everyone will
		// stall instead of losing the player.  If this is too small, then
		// every little hitch that any player gets will cause everyone to hitch.
		if ( maketic - worstTic < netBuffer->value ) {
			maketic++;
		}
		
		
		//---------------------------------
		// Build server packets to send to clients
		//
		// Always send out the current command set over the network
		// even if we didn't create a new command, in case we are just
		// recovering from a lot of dropped packets.
		//---------------------------------
		if ( netgame && !netGameFailure ) {
			// since we are sampling a shared wireless network, any of the player's
			// latencies should be a good enough metric
			stats->latency = packetSequence - netPlayers[1].pc.packetAcknowledge;

			if ( ShouldSendPacket( &netPlayers[1].peer, stats->latency ) ) {
				packetServer_t	gp;
				memset( &gp, 0, sizeof( gp ) );
				gp.packetType = PACKET_VERSION_SERVER;
				gp.gameID = gameID;
				gp.packetSequence = packetSequence++;
				gp.maketic = maketic;
				for ( int i = 0; i < MAXPLAYERS; ++i ) {
					gp.playersInGame[i] = playeringame[i];
				}
				memcpy( gp.netcmds, netcmds, sizeof( gp.netcmds ) );
				
				//---------------------------------
				// Send network packets to the clients
				//---------------------------------
				for ( int i = 1 ; i < MAXPLAYERS ; i++ ) {
					if ( !playeringame[i] ) {
						continue;
					}
					
					netPlayer_t *np = &netPlayers[i];
					
					// only send over the ticcmd that this client needs
					gp.starttic = np->pc.gametic;				
					ticcmd_t *cmd_p = gp.netcmds;
					for ( int j = gp.starttic ; j < gp.maketic ; j++ ) {
						for ( int k = 0 ; k < MAXPLAYERS ; k++ ) {
							if ( playeringame[k] ) {
								*cmd_p++ = netcmds[k][j&BACKUPTICMASK];
							}
						}
					}
					int	packetSize = (byte *)cmd_p - (byte *)&gp;
					(void)packetSize;
					
					// use the most recent tic that both the client and
					// server have run 
					gp.consistancyTic = np->pc.gametic < gametic ? np->pc.gametic : gametic;
					gp.consistancyTic--;
					
					for ( int j = 0 ; j < MAXPLAYERS ; j++ ) {
						gp.consistancy[j] = consistancy[j][gp.consistancyTic&BACKUPTICMASK];
					}
					
					gp.packetAcknowledge = np->pc.packetSequence;
					gp.milliseconds = SysIphoneMilliseconds();
					
					// transmit the packet				
					const std::string theClient = playerIndexToIDMap[i];
					idGameCenter::SendPacketToPlayerUnreliable( theClient, &gp, packetSize );
				}
			}
		}
	}
	
	stats->msecToExecute = SysIphoneMilliseconds() - now;
	if ( stats->msecToExecute > 6 ) {
		printf( "long asyncTic %i: %i msec (%i in lock), %i packets\n", asyncTicNum - 1, stats->msecToExecute, 
			   afterLock - now, stats->received );
	}
}
/*
 ==================
 iphoneProcessPacket
 
 A packet has been received over WiFi or bluetooth
 ==================
 */
void iphoneProcessPacket( const struct sockaddr *from, const void *data, int len ) {
	if ( len < 4 ) {
		printf( "discarding packet because len = %i.\n", len );
		return;
	}
	int	packetID = *(int *)data;
	
	if ( !netgame ) {
		// setup and join are only processed while in the menu system
		
		if ( packetID == PACKET_VERSION_SETUP ) {
			if ( localGameID == setupPacket.gameID ) {
				// if we are sending packets, always ignore other setup packets
				printf( "discarding setup packet because we are the server\n" );
				return;
			}
			setupPacketFrameNum = iphoneFrameNum;

			// save this packet
//			printf( "valid setup packet\n" );
			setupPacket = *(packetSetup_t *)data;
			return;
		}
		
		if ( packetID == PACKET_VERSION_JOIN ) {
			// we should only process join packets if we are in the multiplayer
			// menu and running the current game
			if ( menuState != IPM_MULTIPLAYER ) {
				printf( "discarding join packet because not in IPM_MULTIPLAYER\n" );
				return;
			}
			if ( setupPacket.gameID != localGameID ) {
				printf( "discarding join packet because we aren't the server\n" );
				return;
			}
			
			packetJoin_t *pj = (packetJoin_t *)data;
			if ( pj->playerID == 0 ) {
				// should never happen
				printf( "discarding join packet because playerID is 0\n" );
				return;
			}
			// add this player
			int	i;
			for ( i = 0 ; i < MAXPLAYERS ; i++ ) {
				if ( setupPacket.playerID[i] == pj->playerID ) {
					netPlayers[i].peer.lastPacketTime = SysIphoneMilliseconds();
					break;
				}
			}
			if ( i == MAXPLAYERS ) {
				// not in yet, add if possible
				for ( i = 0 ; i < MAXPLAYERS ; i++ ) {
					if ( setupPacket.playerID[i] == 0 ) {
						setupPacket.playerID[i] = pj->playerID;
						netPlayers[i].peer.address = *from;
						netPlayers[i].peer.lastPacketTime = SysIphoneMilliseconds();
						break;
					}
				}
				// if all players are active, the new join gets ignored
			}
//			printf( "valid join packet\n" );
			return;
		}
		
		// no other packets are processed unless we are in the game
		printf( "discarding packet with id 0x%x when not in netgame\n", packetID );
		return;
	}
	
	// the following are only for running games
	
	if ( consoleplayer == 0 ) {
		// we are the server, and should only receive packetClient_t
		if ( packetID != PACKET_VERSION_CLIENT ) {
			static boolean typeErrorPrinted;
			if ( !typeErrorPrinted ) {
				typeErrorPrinted = true;
				printf( "Packet received with type 0x%x instead of 0x%x\n", packetID, PACKET_VERSION_CLIENT );
			}
			return;
		}
		packetClient_t		*pc = (packetClient_t *)data;
		if ( len != sizeof( *pc ) ) {
			// this should always be an exact length match
			return;
		}
		
		if ( pc->gameID != gameID ) {
			static boolean gameErrorPrinted;
			if ( !gameErrorPrinted ) {
				gameErrorPrinted = true;
				printf( "Packet received with gameID 0x%x instead of 0x%x\n", pc->gameID, gameID );
			}
			return;
		}
		assert( pc->consoleplayer > 0 && pc->consoleplayer < MAXPLAYERS );
		
		netPlayer_t *np = &netPlayers[pc->consoleplayer];
		if ( np->pc.packetSequence >= pc->packetSequence ) {
			printf( "Out of order or duplicated packet from player %i\n", pc->consoleplayer );
			return;
		}
		np->peer.currentPingTics = packetSequence - pc->packetAcknowledge;
		if ( np->pc.packetSequence != pc->packetSequence - 1 ) {
			printf( "Dropped %i packets from player %i\n", pc->packetSequence - 1 - np->pc.packetSequence,
				   pc->consoleplayer );
		}
		
		// good packet from client
		np->pc = *pc;
		UpdatePeerTiming( &np->peer, np->pc.milliseconds );
	} else {
		// we are a client, and should only receive server packets
		if ( packetID != PACKET_VERSION_SERVER ) {
			static boolean typeErrorPrinted;
			if ( !typeErrorPrinted ) {
				typeErrorPrinted = true;
				printf( "Packet received with type 0x%x instead of 0x%x\n", packetID, PACKET_VERSION_CLIENT );
			}
			return;
		}
		packetServer_t		*ps = (packetServer_t *)data;
		if ( len > sizeof( *ps ) ) {
			// packets will usually have less ticcmd_t, but never more
			return;
		}
		
		if ( ps->gameID != gameID ) {
			static boolean gameErrorPrinted;
			if ( !gameErrorPrinted ) {
				gameErrorPrinted = true;
				printf( "Packet received with gameID 0x%x instead of 0x%x\n", ps->gameID, gameID );
			}
			return;
		}
		
		if ( ps->packetSequence <= lastServerPacket.packetSequence ) {
			printf( "Out of order or duplicated packet from server: %i <= %i\n", 
				   ps->packetSequence , lastServerPacket.packetSequence );
			return;
		}
		int drop = ps->packetSequence - (lastServerPacket.packetSequence + 1);
		if ( drop > 0 ) {
			printf( "Dropped %i packets from server\n", drop );
		}
		
		// good packet from server
		memcpy( &lastServerPacket, ps, len );
		UpdatePeerTiming( &netServer, ps->milliseconds );
		netServer.currentPingTics = packetSequence - ps->packetAcknowledge;
		
		// It is possible to have a client run a tic that hasn't been run yet on the game
		// server, since the server can be generating cmds and sending packets while
		// its game frame is hitched for an image load, so this is not an error condition.
		// assert( ps->gametic >= gametic );
		
		// this should never happen
		assert( ps->maketic >= maketic );
		
		// if a ticcmd_t that we need has permanently rolled off the end, we are hosed.
		// This shouldn't happen, since we don't create commands if all the clients
		// haven't processed most of the ones already sent.
		if ( ps->maketic - gametic >= BACKUPTICS ) {
			printf( "BACKUPTICS exceeded: ps->maketic %i, gametic %i\n",
				   ps->maketic, gametic );
			netGameFailure = NF_INTERRUPTED;
		}
		
		// move over the new commands
		// it is possible that some early frames of these are redundant, due
		// to packets crossing in flight.
		ticcmd_t *cmd_p = ps->netcmds;
		for ( int i = ps->starttic ; i < ps->maketic ; i++ ) {
			for ( int j = 0 ; j < MAXPLAYERS ; j++ ) {
				if ( playeringame[j] ) {
					netcmds[j][i&BACKUPTICMASK] = *cmd_p++;
				}
			}
		}
		
		// copy this after the cmds have been updated
		maketic = ps->maketic;
		
		// See if anyone has disconnected.
		for ( int i = 0; i < MAXPLAYERS; ++i ) {
			playeringame[i] = ps->playersInGame[i];
		}
		
		// check consistancy for all in-game players on the most
		// recent gametic that the server knew this client had run
		int	checkTic = ps->consistancyTic;
		assert( checkTic > gametic - BACKUPTICS );	// if older than this, we have lost the data
		checkTic &= BACKUPTICMASK;
		for ( int i = 0 ; i < MAXPLAYERS ; i++ ) {
			if ( playeringame[i] ) {
				if ( ps->consistancy[i] != consistancy[i][checkTic] ) {
					printf( "ConsistancyFailure for player %i on consistancyTic %i\n",
						   i, ps->consistancyTic );
					netGameFailure = NF_CONSISTANCY;
				}
			}
		}
	}	
}
Beispiel #5
0
void iphoneStartup() {
	int		start = SysIphoneMilliseconds();
	
	// microseconds will be plenty random for playerID and localGameID
	playerID = localGameID = SysIphoneMicroseconds();
	
	InitImmediateModeGL();

	// init OpenAL before pak file, so the pak file can
	// make all the al static buffers
	Sound_Init();
	
	char buffer[1028];
	sprintf( buffer, "%s/base.iPack", SysIphoneGetAppDir() );
	// get our new-style pak file
	PK_Init( buffer );

	// register console commands
	Cmd_AddCommand( "listcvars", Cvar_List_f );
	Cmd_AddCommand( "resetcvars", Cvar_Reset_f );
	Cmd_AddCommand( "resetmaps", ResetMaps_f );
	Cmd_AddCommand( "listcmds", Cmd_ListCommands_f );
	Cmd_AddCommand( "give", Give_f );
	Cmd_AddCommand( "god", God_f );
	Cmd_AddCommand( "mail", EmailConsole );  //gsh, mails the console to id

	// register console variables
	Cvar_Get( "version", va( "%3.1f %s %s", DOOM_IPHONE_VERSION, __DATE__, __TIME__ ), 0 );
    
    freeLevelOfWeek = Cvar_Get("freeLevelOfWeek", "0", 0 );
	skill = Cvar_Get( "skill", "1", CVAR_ARCHIVE );
	episode = Cvar_Get( "episode", "0", CVAR_ARCHIVE );

	controlScheme = Cvar_Get( "controlScheme", "0", CVAR_ARCHIVE );
	stickTurn = Cvar_Get( "stickTurn", "128", CVAR_ARCHIVE );
	stickMove = Cvar_Get( "stickMove", "128", CVAR_ARCHIVE );
	stickDeadBand = Cvar_Get( "stickDeadBand", "0.05", CVAR_ARCHIVE );
	rotorTurn = Cvar_Get( "rotorTurn", "50000", CVAR_ARCHIVE );
	tiltTurn = Cvar_Get( "tiltTurn", "0", CVAR_ARCHIVE );
	tiltMove = Cvar_Get( "tiltMove", "0", CVAR_ARCHIVE );
	tiltDeadBand = Cvar_Get( "tiltDeadBand", "0.08", CVAR_ARCHIVE );
	tiltAverages = Cvar_Get( "tiltAverages", "3", CVAR_ARCHIVE );
	centerSticks = Cvar_Get( "centerSticks", "1", CVAR_ARCHIVE );
	rampTurn = Cvar_Get( "rampTurn", "1", CVAR_ARCHIVE );

	music = Cvar_Get( "music", "1", CVAR_ARCHIVE );
	cropSprites = Cvar_Get( "cropSprites", "1", 0 );
	mapScale = Cvar_Get( "mapScale", "10", CVAR_ARCHIVE );
	drawControls = Cvar_Get( "drawControls", "1", CVAR_ARCHIVE );
	autoUse = Cvar_Get( "autoUse", "1", CVAR_ARCHIVE );
	statusBar = Cvar_Get( "statusBar", "1", CVAR_ARCHIVE );
	touchClick = Cvar_Get( "touchClick", "1", CVAR_ARCHIVE );
	messages = Cvar_Get( "messages", "1", CVAR_ARCHIVE );
	mapSelectY = Cvar_Get( "mapSelectY", "0", CVAR_ARCHIVE );
	miniNet = Cvar_Get( "miniNet", "1", CVAR_ARCHIVE );

	// multiplayer setup
	timeLimit = Cvar_Get( "timeLimit", "0", CVAR_ARCHIVE );
	fragLimit = Cvar_Get( "fragLimit", "5", CVAR_ARCHIVE );
	mpDeathmatch = Cvar_Get( "mpDeathmatch", "0", CVAR_ARCHIVE );
	mpDataset = Cvar_Get( "mpDataset", "0", CVAR_ARCHIVE );
	mpEpisode = Cvar_Get( "mpEpisode", "1", CVAR_ARCHIVE );
	mpSkill = Cvar_Get( "mpSkill", "1", CVAR_ARCHIVE );
	mpMap = Cvar_Get( "mpMap", "1", CVAR_ARCHIVE );
	mpExpansion = Cvar_Get( "mpExpansion", "0", CVAR_ARCHIVE | CVAR_NOSET );
	
	// debug tools
	showTilt = Cvar_Get( "showTilt", "-1", 0 );
	showTime = Cvar_Get( "showTime", "0", 0 );
	showNet = Cvar_Get( "showNet", "0", 0 );
	showSound = Cvar_Get( "showSound", "0", 0 );
	noBlend = Cvar_Get( "noBlend", "0", 0 );	// disable the damae blends for screenshots
	glfinish = Cvar_Get( "glfinish", "0", 0 );
	throttle = Cvar_Get( "throttle", "0", 0 );	// network packet throttle enable
	
	// Was origiinally 4. Trying different values to help internet play.
	netBuffer = Cvar_Get( "netBuffer", "12", 0 );	// max tics to buffer ahead
	
	// load the archived cvars
	Cmd_ExecuteFile( va( "%s/config.cfg", SysIphoneGetDocDir() ) );
	
	// start the intro music if it wasn't disabled with the music cvar
	iphonePlayMusic( "intro" );
//	iphonePlayMusic( "e1m1" );
	
	// these should get overwritten by the config loading
	memset( &playState, 0, sizeof( playState ) );
	playState.map.skill = 1;
	playState.map.episode = 1;
	playState.map.map = 1;
	HudSetForScheme( 0 );
	
	// load the binary config file
	FILE *f = fopen( va( "%s/binaryConfig.bin", SysIphoneGetDocDir() ), "rb" );
	if ( f ) {
		int version;
		
		version = 0;
		fread( &version, 1, sizeof( version ), f );
		if ( version != VERSION_BCONFIG ) {
			Com_Printf( "Binary config file bad version.\n" );
		} else {
			fread( &playState, 1, sizeof( playState ), f );
			fread( &huds, 1, sizeof( huds ), f );

			version = 0;
			fread( &version, 1, sizeof( version ), f );
			if ( version != VERSION_BCONFIG ) {
				Com_Error( "Binary config file bad trailing version.\n" );
			}
		}
		fclose( f );
	}

	
	Com_Printf( "startup time: %i msec\n", SysIphoneMilliseconds() - start );

	start = SysIphoneMilliseconds();
	
	// the texnums might have been different in the savegame
	HudSetTexnums();
	
	arialFontTexture = PK_FindTexture( "iphone/arialImageLAL.tga" );
	
	Com_Printf( "preloadBeforePlay(): %i msec\n", SysIphoneMilliseconds() - start );	

	// prBoom seems to draw the static pic screens without setting up 2D, causing
	// a bad first frame
	iphoneSet2D();	
	
	menuState = IPM_MAIN;
    lastState = IPM_MAIN;
	
#if 0
	// jump right to the save spot for debugging
	ResumeGame();
#endif
}