doom-ios/code/iphone/iphone_async.c

993 lines
28 KiB
C

/*
* iphone_async.c
* doom
*
* Created by John Carmack on 7/2/09.
* Copyright 2009 id Software. All rights reserved.
*
*/
/*
Copyright (C) 2009 Id Software, Inc.
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "../doomiphone.h"
typedef struct {
int msecFromLast;
int msecToExecute;
int sent;
int received;
int latency;
} asyncStats_t;
#define MAX_ASYNC_LOGS 256
static asyncStats_t asyncStats[MAX_ASYNC_LOGS];
int asyncTicNum;
// we save this for the packet acknowledge, and also for debugging
static packetServer_t lastServerPacket;
/*
==================
ShowNet
Graph packet receives and transmits
==================
*/
void ShowNet() {
if ( !showNet->value ) {
return;
}
color4_t red = { 255, 0, 0, 255 };
color4_t green = { 0, 255, 0, 255 };
color4_t blue = { 0, 0, 255, 255 };
int now = asyncTicNum; // latch it in case it changes
for ( int i = 1 ; i < 30 ; i++ ) {
asyncStats_t *lt = &asyncStats[(now - i ) & (MAX_ASYNC_LOGS-1)];
R_Draw_Fill( 0, i * 4, lt->sent * 10, 2, red );
R_Draw_Fill( 100, i * 4, lt->received * 10, 2, green );
R_Draw_Fill( 200, i * 4, lt->latency * 10, 2, blue );
}
}
void ShowMiniNet() {
if ( !miniNet->value ) {
return;
}
color4_t red = { 255, 0, 0, 255 };
color4_t green = { 0, 255, 0, 255 };
color4_t blue = { 0, 0, 255, 255 };
int now = asyncTicNum; // latch it in case it changes
now--;
int x = huds.menu.x;
int y = huds.menu.y;
for ( int i = 0 ; i < 10 ; i++ ) {
asyncStats_t *lt = &asyncStats[(now - i ) & (MAX_ASYNC_LOGS-1)];
R_Draw_Fill( x, y+i * 4, lt->sent * 4, 2, red );
R_Draw_Fill( x+20, y+i * 4, lt->received * 4, 2, green );
R_Draw_Fill( x+40, y+i * 4, lt->latency * 4, 2, blue );
}
}
/*
==================
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 );
}
/*
==================
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;
// 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;
}
}
}
}
}
/*
==================
SendSetupPacketIfNecessary
the server sends out a setup packet to each joined client so they
can see the game options needed to start the game.
==================
*/
void SendSetupPacketIfNecessary() {
if ( setupPacket.gameID != localGameID ) {
// we aren't the server
return;
}
if ( gametic >= 2 ) {
// everyone has already started, so they don't need more setup packets
return;
}
setupPacket.sendCount++;
// player 0 is always the server, no need to send to ourselves
for ( int i = 1 ; i < MAXPLAYERS ; i++ ) {
if ( setupPacket.playerID[i] == 0 ) {
continue;
}
int r = sendto( gameSocket, &setupPacket, sizeof( setupPacket ), 0,
&netPlayers[i].peer.address, sizeof( netPlayers[i].peer.address ) );
if ( r == -1 ) {
Com_Printf( "UDP sendTo failed: %s\n", strerror( errno ) );
close( gameSocket );
gameSocket = -1;
}
}
}
/*
==================
DeadBandAdjust
Compresses the 0.0 - 1.0 range into deadband - 1.0
==================
*/
float DeadBandAdjust( float f, float deadBand ) {
if ( f < 0 ) {
return -DeadBandAdjust( -f, deadBand );
}
if ( f > 1.0 ) {
return 1.0;
}
if ( f < deadBand ) {
return 0;
}
return (f-deadBand) / (1.0 - deadBand);
}
/*
==================
AxisHit
Returns a -1 to 1 range
If activeFraction is less than 1.0, the range will clamp
to the limits before the edge of the box is hit.
==================
*/
float AxisHit( ibutton_t *hud ) {
// will be set true if -1 or 1
hud->drawAsLimit = false;
if ( hud->buttonFlags & BF_IGNORE ) {
return 0;
}
touch_t *t = hud->touch;
if ( !t ) {
return 0;
}
int centerX, centerY;
if ( centerSticks->value ) {
// center on each touch
centerX = hud->downX;
centerY = hud->downY;
} else {
centerX = hud->x + hud->drawWidth / 2;
centerY = hud->y + hud->drawHeight / 2;
}
float w = hud->drawWidth * 0.5 * hud->scale;
float h = hud->drawHeight * 0.5 * hud->scale;
int x = t->x - centerX;
int y = t->y - centerY;
float f;
int isXaxis = ( hud != &huds.forwardStick );
if ( isXaxis ) {
f = (float)x / w;
} else {
f = (float)y / h;
}
float deadBand = stickDeadBand->value;
if ( hud == &huds.turnStick ) {
deadBand = 0;
}
if ( f > deadBand ) {
f -= deadBand;
} else if ( f < -deadBand ) {
f += deadBand;
} else {
// inside the deadband
return 0;
}
// adjust so you can hit the limit even if the control is drawn at the very edge
// of the screen
f /= (0.95-deadBand);
if ( f > 1.0f ) {
f = 1.0f;
hud->drawAsLimit = true;
} else if ( f < -1.0f ) {
f = -1.0f;
hud->drawAsLimit = true;
}
if ( hud == &huds.turnStick && rampTurn->value ) {
// do "gamma corrected" movement, so changes are always proportional
if ( f > 0 ) {
f = 0.01 * pow( 1.047, f * 100 );
} else {
f = -0.01 * pow( 1.047, f * -100 );
}
}
return f;
}
static const float NOT_TOUCHED_STATE = 99999.0f;
float RotorControl( ibutton_t *hud ) {
if ( hud->buttonFlags & BF_IGNORE ) {
return 0;
}
touch_t *t = hud->touch;
if ( !t ) {
// no touches in the control
hud->touchState = NOT_TOUCHED_STATE;
return 0;
}
float delta[2];
int centerX = hud->x + hud->drawWidth / 2;
int centerY = hud->y + hud->drawHeight / 2;
delta[0] = t->x - centerX;
delta[1] = t->y - centerY;
float rotorAngle = atan2( delta[1], delta[0] );
if ( hud->touchState == NOT_TOUCHED_STATE ) {
// just touched, haven't moved yet
hud->touchState = rotorAngle;
return 0;
}
float deltaAngle = rotorAngle - hud->touchState;
// handle the wrap around cases
if ( deltaAngle >= M_PI ) {
deltaAngle = deltaAngle - 2*M_PI;
} else if ( deltaAngle <= -M_PI ) {
deltaAngle = 2*M_PI + deltaAngle;
}
hud->touchState = rotorAngle;
hud->drawState += deltaAngle;
return deltaAngle / (2*M_PI);
}
#define TURBOTHRESHOLD 0x32
static int ClampMove( int v ) {
if ( v > TURBOTHRESHOLD ) {
return TURBOTHRESHOLD;
}
if ( v < -TURBOTHRESHOLD ) {
return -TURBOTHRESHOLD;
}
return v;
}
/*
==================
iphoneBuildTiccmd
Use touch and tilt controls to set up a doom ticcmd_t
==================
*/
static void iphoneBuildTiccmd(ticcmd_t* cmd) {
memset(cmd,0,sizeof*cmd);
// cmd->consistancy = consistancy[consoleplayer][maketic & BACKUPTICMASK];
if ( menuState != IPM_GAME ) {
// if in the menus, always generate an empty event
return;
}
// the respawn button triggers a use
if ( respawnActive ) {
cmd->buttons |= BT_USE;
respawnActive = false;
}
if ( gamestate != GS_LEVEL ) {
// at intermissions, all taps equal attack
// FIXME: better latched value
if ( numTouches == numPrevTouches + 1 ) {
cmd->buttons |= BT_ATTACK;
}
return;
}
// don't allow movement control use during automap
if ( automapmode & am_active ) {
return;
}
// don't built a tic when dead, other than the respawn use
if ( players[consoleplayer].playerstate == PST_DEAD ) {
return;
}
//------------------------
// No controls during weapon-select screen
//------------------------
boolean weaponCycle = false;
if ( drawWeaponSelect ) {
// if the weaponSelect overlay is up, continue tracking held touches
// until the are released
for ( ibutton_t *hud = (ibutton_t *)&huds ; hud != (ibutton_t *)(&huds+1) ; hud++ ) {
if ( hud->touch || hud == &huds.weaponSelect ) {
UpdateHudTouch( hud );
}
}
// Re-tapping in the weapon select area will cycle to the next weapon.
// The action happens on initial touch.
touch_t *t = huds.weaponSelect.touch;
if ( t && t->down && t->stateCount == 1 ) {
drawWeaponSelect = false;
t->stateCount++; // ensure it won't bring it back up
weaponCycle = true;
} else {
return;
}
}
//------------------------
// gameplay controls
//------------------------
// update all the hud touch states
if ( menuState == IPM_GAME ) {
UpdateHudTouch( &huds.forwardStick );
UpdateHudTouch( &huds.sideStick );
UpdateHudTouch( &huds.turnStick );
UpdateHudTouch( &huds.turnRotor );
UpdateHudTouch( &huds.weaponSelect );
}
// tap in the lower center for weapon switch
touch_t *t = huds.weaponSelect.touch;
if ( t && t->down && t->stateCount == 1 ) {
drawWeaponSelect = true;
}
// hack to let a single touch control both hud elements on combo sticks
// This is dependent on the order in the structure, and probably not a good
// way to do things.
if ( huds.sideStick.x == huds.forwardStick.x && huds.sideStick.y == huds.forwardStick.y ) {
huds.sideStick.touch = huds.forwardStick.touch;
huds.sideStick.downX = huds.forwardStick.downX;
huds.sideStick.downY = huds.forwardStick.downY;
}
if ( huds.turnStick.x == huds.forwardStick.x && huds.turnStick.y == huds.forwardStick.y ) {
huds.turnStick.touch = huds.forwardStick.touch;
huds.turnStick.downX = huds.forwardStick.downX;
huds.turnStick.downY = huds.forwardStick.downY;
}
// the fire button doesn't grab touches
{
int x = huds.fire.x - ( huds.fire.drawWidth >> 1 );
int y = huds.fire.y - ( huds.fire.drawHeight >> 1 );
int w = huds.fire.drawWidth << 1;
int h = huds.fire.drawHeight << 1;
if ( AnyTouchInBounds( x, y, w, h ) ) {
cmd->buttons |= BT_ATTACK;
huds.fire.buttonFlags |= BF_DRAW_ACTIVE; // draw with color
} else {
huds.fire.buttonFlags &= ~BF_DRAW_ACTIVE;
}
}
int forwardmove;
int sidemove;
// the edge of the drawn control should give the maximum
// legal doom movement speed
huds.forwardStick.scale = stickMove->value / 128.0f;
huds.sideStick.scale = stickMove->value / 128.0f;
forwardmove = -TURBOTHRESHOLD * AxisHit( &huds.forwardStick );
sidemove = TURBOTHRESHOLD * AxisHit( &huds.sideStick );
huds.turnStick.scale = stickTurn->value / 128.0f;
cmd->angleturn = -1500.0f * AxisHit( &huds.turnStick );
// rotary wheel
cmd->angleturn -= rotorTurn->value * RotorControl( &huds.turnRotor );
// accelerometer tilting
sidemove += tiltMove->value * DeadBandAdjust( tilt, tiltDeadBand->value );
cmd->angleturn -= tiltTurn->value * DeadBandAdjust( tilt, tiltDeadBand->value );
// clamp movements
cmd->forwardmove = ClampMove( forwardmove );
cmd->sidemove = ClampMove( sidemove );
// tap in the upper center for use
if ( TouchPressed( 140, 0, 240, 200 ) ) {
cmd->buttons |= BT_USE;
}
// auto-use if the game thread found a usable line in front of the player
if ( autoUse->value && autoUseActive ) {
if ( cmd->buttons & BT_USE ) {
// Allow a tap to briefly cancel the auto-use, which works around
// some issues with incorrectly started auto-uses preventing
// a real door from opening.
cmd->buttons &= ~BT_USE;
} else {
cmd->buttons |= BT_USE;
}
}
if ( weaponSelected != -1 ) {
cmd->buttons |= BT_CHANGE;
cmd->buttons |= weaponSelected<<BT_WEAPONSHIFT;
weaponSelected = -1;
} else {
// auto-cycle weapons when firing on empty
if ( players[consoleplayer].attackdown && !P_CheckAmmo(&players[consoleplayer]) ) {
weaponCycle = true;
}
// weapon switch
int newweapon = wp_nochange;
if ( weaponCycle ) {
// witch to next weapon when out of ammo
newweapon = P_SwitchWeapon(&players[consoleplayer]);
}
if (newweapon != wp_nochange) {
cmd->buttons |= BT_CHANGE;
cmd->buttons |= newweapon<<BT_WEAPONSHIFT;
}
}
}
/*
==================
ShouldSendPacket
WiFi can suffer from bad "pileup" because of the link-level
retransmit behavior. What we would dearly love is an ioctl or
something that returned a value of how many packets are backed
up in the transmit queue, but that would be terribly driver
specific, and we are unlikely to ever see it.
We are going to infer that there is a pileup by looking at the
round trip latency. If we ever support internet connections with
real latencies that are this large, we will have to use something
else.
==================
*/
// while 1 is usual, some combinations of AsyncTic phases will give 2 commonly,
// and we don't want to throttle back in those cases.
static const int okPacketLatency = 4;
static const int okOneWayLatency = 70;
static boolean ShouldSendPacket( netPeer_t *peer, int packetLatency ) {
if ( throttle->value == 0 ) {
// disabled completely, always send
return true;
}
if ( peer == &netServer ) {
if ( throttle->value == 2 ) {
return false; // don't send client messages at all
}
} else {
if ( throttle->value == 3 ) { // don't send server messages at all
return false;
}
}
// immediately fire back a packet if it looks like we just got one through
// clearly
if ( peer->lastPacketAsyncTic == asyncTicNum && peer->oneWayLatency < okOneWayLatency ) {
return true;
}
if ( packetLatency <= okPacketLatency ) {
return true;
}
// limit from 1 to 4, in worst case only transmit a packet
// every 16 frames, or about half a second
packetLatency -= okPacketLatency;
if ( packetLatency > 4 ) {
packetLatency = 4;
}
if ( asyncTicNum & ((1<<packetLatency)-1) ) {
return false; // inhibit transmit this frame
}
printf( "Sending on packetLatency %i\n", packetLatency );
return true; // we are throttling back, but transmit this frame
}
/*
==================
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;
// listen for changes to available servers
ProcessDNSMessages();
// send out the setup packets if we are just starting the game
SendSetupPacketIfNecessary();
// don't generate any commands while loading levels
if ( iphoneFrameNum == levelLoadFrameNum ) {
return;
}
// don't let the game thread mess with touches during the async tic execution
pthread_mutex_lock( &eventMutex );
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++;
}
}
//---------------------------------
// read network packets
//
// we may receive multiple packets in one frame due to timer skew
// even with only one other player
//---------------------------------
if ( !netGameFailure ) {
byte packet[1500];
while( gameSocket > 0 ) {
struct sockaddr from;
unsigned senderLen = sizeof( from );
int r = recvfrom( gameSocket, &packet, sizeof( packet ), 0, &from, &senderLen );
if ( r == -1 ) {
if ( errno != EAGAIN ) {
perror( "recvfrom" );
}
break;
}
stats->received++;
iphoneProcessPacket( &from, packet, r );
}
}
//---------------------------------
// 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;
int r = sendto( gameSocket, &cp, sizeof( cp ), 0, &netServer.address, sizeof( netServer.address ) );
stats->sent++;
if ( r == -1 ) {
printf( "UDP sendTo failed: %s\n", strerror( errno ) );
close( gameSocket );
gameSocket = -1;
}
}
}
} 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;
memcpy( gp.netcmds, netcmds, sizeof( gp.netcmds ) );
//---------------------------------
// Send network packets to the clients
//---------------------------------
for ( int i = 1 ; i < MAXPLAYERS ; i++ ) {
if ( !playeringame[i] ) {
continue;
}
if ( gameSocket <= 0 ) {
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 i = gp.starttic ; i < gp.maketic ; i++ ) {
for ( int j = 0 ; j < MAXPLAYERS ; j++ ) {
if ( playeringame[j] ) {
*cmd_p++ = netcmds[j][i&BACKUPTICMASK];
}
}
}
int packetSize = (byte *)cmd_p - (byte *)&gp;
// 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
stats->sent++;
int r = sendto( gameSocket, &gp, packetSize, 0, &np->peer.address, sizeof( np->peer.address ) );
if ( r == -1 ) {
printf( "UDP sendTo failed: %s\n", strerror( errno ) );
close( gameSocket );
gameSocket = -1;
}
}
}
}
}
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 );
}
// the game thread can now swap touches
pthread_mutex_unlock( &eventMutex );
// signal the main thread that is probably blocked on this semaphore
if ( sem_post( ticSemaphore ) == -1 ) {
perror( "sem_post");
}
}