/* This file is part of Warzone 2100. Copyright (C) 1999-2004 Eidos Interactive Copyright (C) 2005-2007 Warzone Resurrection Project Warzone 2100 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. Warzone 2100 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 Warzone 2100; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /* * MultiSync.c * * synching issues * This file handles the constant backstream of net info, checking that recvd info * is concurrent with the local world, and correcting as required. Magic happens here. * * All conflicts due to non-guaranteed messaging are detected/resolved here. * * Alex Lee, pumpkin Studios, bath. */ #include "lib/framework/frame.h" #include "stats.h" #include "lib/gamelib/gtime.h" #include "map.h" #include "objects.h" #include "display.h" // for checking if droid in view. #include "order.h" #include "action.h" #include "hci.h" // for byte packing funcs. #include "display3ddef.h" // tile size constants. #include "console.h" #include "text.h" // to access strings #include "geometry.h" // for gettilestructure #include "mapgrid.h" // for move droids directly. #include "lib/netplay/netplay.h" #include "multiplay.h" #include "frontend.h" // for titlemode #include "multistat.h" #include "power.h" // for power checks // //////////////////////////////////////////////////////////////////////////// // function definitions BOOL sendCheck (VOID); // send/recv check info BOOL recvDroidCheck (NETMSG *pMsg); BOOL recvStructureCheck (NETMSG *pMsg); BOOL recvPowerCheck (NETMSG *pMsg); BOOL recvPing (NETMSG *pMsg); BOOL sendScoreCheck (VOID); //score static BOOL sendStructureCheck (VOID); //Structure BOOL sendPowerCheck (BOOL now); //power static void packageCheck (UDWORD i, NETMSG *pMsg, DROID *pD); static BOOL sendDroidCheck (VOID); //droids UDWORD averagePing (VOID); BOOL sendPing (VOID); // send/recv Ping information static void highLevelDroidUpdate(DROID *psDroid, UDWORD x, UDWORD y, UDWORD state, UDWORD order, //UDWORD orderX, //UDWORD orderY, BASE_OBJECT *psTarget, UDWORD numKills); static void onscreenUpdate (DROID *pDroid,UDWORD dam, // the droid and its damage UDWORD x, UDWORD y, // the ideal position FRACT fx,FRACT fy, // the ideal fractional position UWORD dir, // direction it should facing DROID_ORDER order); // what it should be doing static void offscreenUpdate (DROID *pDroid,UDWORD dam, UDWORD x, UDWORD y, FRACT fx,FRACT fy, UWORD dir, DROID_ORDER order); // //////////////////////////////////////////////////////////////////////////// // Defined numeric values #define AV_PING_FREQUENCY 45000 // how often to update average pingtimes. in approx millisecs. #define PING_FREQUENCY 12000 // how often to update pingtimes. in approx millisecs. #define STRUCT_FREQUENCY 700 // how often (ms) to send a structure check. #define DROID_FREQUENCY 1000 // how ofter (ms) to send droid checks #define POWER_FREQUENCY 10000 // how often to send power levels #define SCORE_FREQUENCY 25000 // how often to update global score. #define SYNC_PANIC 40000 // maximum time before doing a dirty fix. static UDWORD PingSend[MAX_PLAYERS]; //stores the time the ping was called. // //////////////////////////////////////////////////////////////////////////// // test traffic level. static BOOL okToSend() { //update checks & go no further if any exceeded. if((NETgetRecentBytesSent() + NETgetRecentBytesRecvd()) >= game.bytesPerSec) { return FALSE; } if( NETgetRecentPacketsSent() >= game.packetsPerSec ) { return FALSE; } return TRUE; } // //////////////////////////////////////////////////////////////////////////// // Droid checking info. keep position and damage in sync. BOOL sendCheck(VOID) { UDWORD i; NETgetBytesSent(); // update stats. NETgetBytesRecvd(); NETgetPacketsSent(); NETgetPacketsRecvd(); // dont send checks till all players are present. for(i=0;ipsNext; } if (pD == NULL) // droid is no longer there or list end. { player ++; // next player next time. player = player%MAX_PLAYERS; droidnum=0; if(maxtrys gameTime)lastSent= 0; if((gameTime-lastSent) < DROID_FREQUENCY) // only send a struct send if not done recently. { return TRUE; } lastSent = gameTime; msg.size = 0; switch(game.bytesPerSec) { case IPXBYTESPERSEC: toSend = 6; break; case INETBYTESPERSEC: case CABLEBYTESPERSEC: case DEFAULTBYTESPERSEC: case MODEMBYTESPERSEC: default: toSend = 4; break; } while(count < toSend) // send x droids. { pD = pickADroid(); if(pD) { packageCheck(i,&msg,pD); } i = msg.size; count = count +1; } msg.type = NET_CHECK_DROID; NETbcast(&msg,FALSE); return TRUE; } // //////////////////////////////////////////////////////////////////////////// // Send a Single Droid Check message static void packageCheck(UDWORD i, NETMSG *pMsg, DROID *pD) { // UDWORD packtemp; UWORD numkills; // pMsg->body[ i+0] = (CHAR)pD->player; pMsg->body[ i+1] = (CHAR)pD->order; // order being executed NetAdd2( pMsg, i+2, pD->id); // droid id NetAdd2( pMsg, i+6, pD->secondaryOrder ); NetAdd2( pMsg, i+10, pD->body); // damage points NetAdd2( pMsg, i+14, pD->direction); // direction if( pD->order == DORDER_ATTACK || pD->order == DORDER_MOVE || pD->order == DORDER_RTB || pD->order == DORDER_RTR) { NetAdd2(pMsg, i+16, pD->sMove.fx); //fraction move pos NetAdd2(pMsg, i+20, pD->sMove.fy); } else { NetAdd2(pMsg, i+16, pD->x); //non fractional move pos NetAdd2(pMsg, i+20, pD->y); } if(pD->order == DORDER_ATTACK) { NetAdd2(pMsg, i+24, pD->psTarget->id); // target id } else if(pD->order == DORDER_MOVE) { NetAdd2(pMsg, i+24, pD->orderX); NetAdd2(pMsg, i+26, pD->orderY); } numkills = pD->numKills; NetAdd2(pMsg, i+28, numkills); // droid kills pMsg->size =(UWORD)( pMsg->size + 30); return ; } // //////////////////////////////////////////////////////////////////////////// // receive a check and update the local world state accordingly BOOL recvDroidCheck(NETMSG *m) { FRACT fx=0,fy=0; UDWORD ref,player,x = 0,y = 0,bod,target=0;//,dir; UWORD dir,numkills; DROID_ORDER ord; BOOL onscreen; DROID *pD; BASE_OBJECT *psTarget=NULL; UDWORD i,state, tx=0,ty=0; i = 0; while(i < m->size) { // obtain information about remote droid. player = m->body[i+0]; ord = (DROID_ORDER)m->body[i+1]; // droid id. NetGet(m, i+2,ref); NetGet(m, i+6,state); NetGet(m, i+10,bod); // Damage update. NetGet(m, i+14,dir); if( ord == DORDER_ATTACK || ord == DORDER_MOVE || ord == DORDER_RTB || ord == DORDER_RTR) // detailed position info mode { NetGet(m, i+16,fx); NetGet(m, i+20,fy); } else { NetGet(m, i+16,x); // dont pack since could be sending fractional vals anyway. NetGet(m, i+20,y); } if(ord == DORDER_ATTACK) { NetGet(m, i+24,target); } else if(ord == DORDER_MOVE) { NetGet(m, i+24,tx); NetGet(m, i+26,ty); } NetGet(m, i+28,numkills); i = i + 30; ////////////////////////////////////// if ( !(IdToDroid(ref,player,&pD)) ) // find the droid in question { NETlogEntry("Recvd Unknown droid info. val=player",0,player); debug( LOG_NEVER, "Received Checking Info for an unknown (As yet) droid player:%d ref:%d\n", player, ref ); return TRUE; //Recvd checking info for an unknown droid } if(target) { psTarget = IdToPointer(target,ANYPLAYER); // get the target in question. } ////////////////////////////////////// // decide how to sync it. if( DrawnInLastFrame(pD->sDisplay.frameNumber) && (pD->sDisplay.screenX < DISP_WIDTH) && (pD->sDisplay.screenY < DISP_HEIGHT) ) // check for onscreen { if(pD->visible[selectedPlayer]) { onscreen = TRUE; // onscreen and visible } else { onscreen = FALSE; // onscreen, but not visible. } }else{ onscreen = FALSE; // not onscreen. } if(ingame.PingTimes[player] > PING_LIMIT) // if it's a big ping then don't do a smooth move. { onscreen = FALSE; } // if( pD->lastSync > gameTime)pD->lastSync =0; // if( (gameTime - pD->lastSync) > SYNC_PANIC ) // if it's been a while then jump it. // { // onscreen = FALSE; // } ////////////////////////////////////// /// now do the update. if ( onscreen || vtolDroid(pD)) { onscreenUpdate(pD,bod,x,y,fx,fy,dir,ord); } else { offscreenUpdate(pD,bod,x,y,fx,fy,dir,ord); } ////////////////////////////////////// // now make note of how accurate the world model is for this droid. // if droid is close then remember. if( abs(x-pD->x)<(TILE_UNITS*2) || abs(y- pD->y)<(TILE_UNITS*2)) { pD->lastSync = gameTime; // note we did a reasonable job. } ////////////////////////////////////// // now update the higher level stuff if(!vtolDroid(pD)) { highLevelDroidUpdate(pD,x,y,state, ord, psTarget,numkills); } // done this droid, move on to next. } return TRUE; } // //////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////// // higher order droid updating. Works mainly at the order level. comes after the main sync. static void highLevelDroidUpdate(DROID *psDroid,UDWORD x, UDWORD y, //UDWORD state, UDWORD order,UDWORD orderX,UDWORD orderY, UDWORD state, UDWORD order, BASE_OBJECT *psTarget,UDWORD numKills) { // update kill rating. psDroid->numKills = (UWORD)numKills; // remote droid is attacking, not here tho! if(order == DORDER_ATTACK && psDroid->order != DORDER_ATTACK && psTarget) { turnOffMultiMsg(TRUE); orderDroidObj(psDroid, DORDER_ATTACK, psTarget); turnOffMultiMsg(FALSE); } // secondary orders. if(psDroid->secondaryOrder != state) { psDroid->secondaryOrder = state; } // see how well the sync worked, optionally update. // offscreen updates will make this ok each time. if(psDroid->order == DORDER_NONE && order == DORDER_NONE) { if( (abs(x- psDroid->x)>(TILE_UNITS*2)) // if more than 2 tiles wrong. ||(abs(y- psDroid->y)>(TILE_UNITS*2)) ) { turnOffMultiMsg(TRUE); orderDroidLoc(psDroid, DORDER_MOVE,x,y); turnOffMultiMsg(FALSE); } } } // //////////////////////////////////////////////////////////////////////////// // droid on screen needs modifying static void onscreenUpdate(DROID *psDroid, UDWORD dam, UDWORD x, UDWORD y, FRACT fx, FRACT fy, UWORD dir, DROID_ORDER order) { BASE_OBJECT *psClickedOn; BOOL bMouseOver = FALSE; psClickedOn = mouseTarget(); if( psClickedOn != NULL && psClickedOn->type == OBJ_DROID) { if(psClickedOn->id == psDroid->id && mouseDown(MOUSE_RMB)) { bMouseOver = TRUE; // override, so you dont see the updates. } } if(!bMouseOver) { psDroid->body = dam; // update damage } // if(psDroid->order == DORDER_NONE || (psDroid->order == DORDER_GUARD && psDroid->action == DACTION_NONE) ) // { // psDroid->direction = dir %360; //update rotation // } return; } // //////////////////////////////////////////////////////////////////////////// // droid offscreen needs modyfying. static void offscreenUpdate(DROID *psDroid, UDWORD dam, UDWORD x, UDWORD y, FRACT fx, FRACT fy, UWORD dir, DROID_ORDER order) { UDWORD oldx,oldy; PROPULSION_STATS *psPropStats; SDWORD xdiff,ydiff, distSq; // stage one, update the droids position & info, LOW LEVEL STUFF. if( order == DORDER_ATTACK || order == DORDER_MOVE || order == DORDER_RTB || order == DORDER_RTR) // move order { // calculate difference between remote and local xdiff = psDroid->x - (UWORD)fx; ydiff = psDroid->y - (UWORD)fy; distSq = (xdiff*xdiff) + (ydiff*ydiff); // if more than 2 squares, jump it. if(distSq > (2*TILE_UNITS)*(2*TILE_UNITS) ) { if( ((UDWORD)fx != 0) && ((UDWORD)fy != 0) ) { oldx = psDroid->x; oldy = psDroid->y; psDroid->sMove.fx = fx; //update x psDroid->sMove.fy = fy; //update y psDroid->x = (UWORD) fx; //update move progress psDroid->y = (UWORD) fy; gridMoveObject((BASE_OBJECT *)psDroid, (SDWORD)oldx,(SDWORD)oldy); psDroid->direction = (UWORD)(dir %360); // update rotation // reroute the droid. turnOffMultiMsg(TRUE); moveDroidTo(psDroid, psDroid->sMove.DestinationX,psDroid->sMove.DestinationY); turnOffMultiMsg(FALSE); } } } else { oldx = psDroid->x; oldy = psDroid->y; psDroid->x = (UWORD)x; //update x psDroid->y = (UWORD)y; //update y gridMoveObject((BASE_OBJECT *)psDroid, (SDWORD)oldx,(SDWORD)oldy); psDroid->direction = (UWORD)(dir %360); // update rotation } psDroid->body = dam; // update damage // stop droid if remote droid has stopped. if( (order == DORDER_NONE || order == DORDER_GUARD) && !(psDroid->order == DORDER_NONE || psDroid->order == DORDER_GUARD ) ) { turnOffMultiMsg(TRUE); moveStopDroid(psDroid); turnOffMultiMsg(FALSE); } // snap droid(if on ground) to terrain level at x,y. psPropStats = asPropulsionStats + psDroid->asBits[COMP_PROPULSION].nStat; ASSERT( PTRVALID(psPropStats, sizeof(PROPULSION_STATS)),"offscreenUpdate: invalid propulsion stats pointer" ); if( psPropStats->propulsionType != LIFT ) // if not airborne. { psDroid->z = map_Height(psDroid->x, psDroid->y); } return; } // //////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////// // Structure Checking, to ensure smoke and stuff is consistent across machines. // this func is recursive! static STRUCTURE *pickAStructure(void) { static UDWORD player=0; // player currently checking. static UDWORD snum=0; // structure index for this player. STRUCTURE *pS=NULL; static UDWORD maxtrys = 0; // don't loop forever if failing/. UDWORD i; if ( !myResponsibility(player) ) // dont send stuff that's not our problem. { player ++; // next player next time. player = player%MAX_PLAYERS; snum =0; if(maxtryspsNext; } if (pS == NULL) // last structure or no structures at all { player ++; // go onto the next player player = player%MAX_PLAYERS; snum=0; if(maxtrys gameTime)lastSent= 0; if((gameTime-lastSent) < STRUCT_FREQUENCY) // only send a struct send if not done recently. { return TRUE; } lastSent = gameTime; pS = pickAStructure(); if(pS && (pS->status == SS_BUILT)) // only send info about complete buildings. { m.body[0] = (char)pS->player; // send struct details NetAdd(m,1,pS->id); NetAdd(m,5,pS->body); // damage NetAdd(m,7,pS->pStructureType->ref); // building type. NetAdd(m,11,pS->x); //position NetAdd(m,13,pS->y); NetAdd(m,15,pS->z); NetAdd(m,17,pS->direction); m.type = NET_CHECK_STRUCT; m.size = 19; // functionality. if (pS->pStructureType->type == REF_RESEARCH) { capacity = (UBYTE)((RESEARCH_FACILITY*)pS->pFunctionality)->capacity; NetAdd(m,19,capacity); m.size +=1; } if (pS->pStructureType->type == REF_FACTORY OR // pS->pStructureType->type == REF_CYBORG_FACTORY OR pS->pStructureType->type == REF_VTOL_FACTORY) { capacity = (UBYTE)((FACTORY*)pS->pFunctionality)->capacity; NetAdd(m,19,capacity); m.size +=1; } if (pS->pStructureType->type == REF_POWER_GEN) { capacity = (UBYTE)((POWER_GEN*)pS->pFunctionality)->capacity; NetAdd(m,19,capacity); m.size +=1; } NETbcast(&m,FALSE); } return TRUE; } // receive checking info about a structure and update local world state BOOL recvStructureCheck( NETMSG *m) { UWORD x,y,z,dir; UDWORD ref,type,j; UBYTE i,player; UBYTE cap; STRUCTURE *pS; STRUCTURE_STATS *psStats; player = m->body[0]; NetGet(m,1,ref); pS = IdToStruct(ref,player); if(pS) { NetGet(m,5,pS->body); // Damage update. NetGet(m,17,pS->direction); } else // structure wasn't found, create it. { NetGet(m,7,type); NetGet(m,11,x); NetGet(m,13,y); NetGet(m,15,z); NetGet(m,17,dir); NETlogEntry("scheck:structure check failed, adding struct. val=type",0,type-REF_STRUCTURE_START); for(i=0; ( i>TILE_SHIFT,y>>TILE_SHIFT)) ) { NETlogEntry("scheck:Tile has structure val=player",0,player); // if correct type && player then complete & modify pS = getTileStructure(x>>TILE_SHIFT, y>>TILE_SHIFT); if( pS && (pS->pStructureType->type == type ) && (pS->player == player ) ) { pS->direction = dir; pS->id = ref; if(pS->status != SS_BUILT) { pS->status = SS_BUILT; buildingComplete(pS); } NETlogEntry("scheck: fixed?",0,player); } // wall becoming a cornerwall else if(pS->pStructureType->type == REF_WALL ) { if( psStats->type == REF_WALLCORNER) { NETlogEntry("scheck: fixed wall->cornerwall",0,0); removeStruct(pS, TRUE); powerCalc(FALSE); // turn off power pS=buildStructure((STRUCTURE_STATS *)psStats, x , y ,player,TRUE); powerCalc(TRUE); //turn on power if(pS) { pS->id = ref; } else { NETlogEntry("scheck: failed to upgrade wall!",0,player); return FALSE; } } } else { NETlogEntry("scheck:Tile did not have correct type or player val=player",0,player); return FALSE; } // else remove local copy. with a bang (make it look like an explosion, itll update next time around). // ?? dunno if we should do this! // return TRUE; // structure exists already there.... } else { NETlogEntry("scheck: didn't find structure at all, building it",0,0); // buildFlatten(psStats, x,y,z); // levelGround(x,y,z, // ((STRUCTURE_STATS *)psStats)->baseWidth, // ((STRUCTURE_STATS *)psStats)->baseBreadth); powerCalc(FALSE); // turn off power pS = buildStructure((STRUCTURE_STATS *)psStats, x , y ,player,TRUE); powerCalc(TRUE); //turn on power } } if(pS) { if( pS->status != SS_BUILT) // check its finished { pS->direction = dir; pS->id = ref; pS->status = SS_BUILT; buildingComplete(pS); } if(m->size > 19) // capacity { NetGet(m,19,i); switch(pS->pStructureType->type) // current capacity { case REF_RESEARCH: cap = (UBYTE)((RESEARCH_FACILITY*)pS->pFunctionality)->capacity; break; case REF_FACTORY: case REF_VTOL_FACTORY: // case REF_CYBORG_FACTORY: cap = (UBYTE)((FACTORY*)pS->pFunctionality)->capacity; break; case REF_POWER_GEN: cap = (UBYTE)((POWER_GEN*)pS->pFunctionality)->capacity; break; default: NETlogEntry("unknown upgrade error in recv struct check",0,0); cap =0; break; } if(cap != i) // compare and upgrade { switch(pS->pStructureType->type) { case REF_RESEARCH: //for (j = 0; (j0) { buildStructure(&asStructureStats[j],pS->x,pS->y,pS->player,FALSE); if(pS && pS->status != SS_BUILT) // check its finished again. { pS->id = ref; pS->status = SS_BUILT; buildingComplete(pS); } i = (UBYTE)(i - 1); } } } } return TRUE; } // //////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////// // Power Checking. Send a power level check every now and again. BOOL sendPowerCheck(BOOL now) { NETMSG m; static UDWORD lastsent = 0; if(!now) { if(lastsent > gameTime)lastsent= 0; if((gameTime-lastsent) < POWER_FREQUENCY ) // only send if not done recently. { return TRUE; } } lastsent = gameTime; // ok send a power check. m.body[0] = (char)selectedPlayer; NetAdd(m,1, asPower[selectedPlayer]->currentPower ); m.size = 5; m.type = NET_CHECK_POWER; return(NETbcast(&m,FALSE)); } BOOL recvPowerCheck(NETMSG *pMsg) { UDWORD player,b; player = pMsg->body[0]; NetGet(pMsg,1, b); setPower(player,b); return TRUE; } // //////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////// // Score BOOL sendScoreCheck(VOID) { static UDWORD lastsent = 0; UDWORD i; PLAYERSTATS stats; NETMSG m; if(lastsent > gameTime)lastsent= 0; if((gameTime-lastsent) < SCORE_FREQUENCY ) // only send if not done recently. { return TRUE; } lastsent = gameTime; // syncronise scores. // update local score stats = getMultiStats(selectedPlayer,TRUE); stats.recentKills += stats.killsToAdd; // add recently scored points. stats.totalKills += stats.killsToAdd; stats.recentScore += stats.scoreToAdd; stats.totalScore += stats.scoreToAdd; stats.killsToAdd =0; stats.scoreToAdd =0; setMultiStats(player2dpid[selectedPlayer],stats,TRUE); // store local version. setMultiStats(player2dpid[selectedPlayer],stats,FALSE); // send score to the ether // broadcast any changes in other players, but not in FRONTEND!!! if(titleMode != MULTIOPTION && titleMode != MULTILIMIT) { m.size =0; for(i = 0; ibody[index]; index+=1; if(player != ANYPLAYER) { NetGet(pMsg,index,kil); index+=sizeof(kil); NetGet(pMsg,index,sco); index+=sizeof(sco); // do the update. if(player == selectedPlayer) { stats = getMultiStats(player,TRUE); stats.killsToAdd += kil; stats.scoreToAdd += sco; setMultiStats(player2dpid[player],stats,TRUE); // store local version. } } } return TRUE; } // //////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////// // Pings UDWORD averagePing(VOID) { UDWORD i,count,total; count =0; total =0; for(i=0;i gameTime)lastPing= 0; if((gameTime-lastPing) < PING_FREQUENCY) { return TRUE; } lastPing = gameTime; /// if host, also update the average ping stat for joiners. if(NetPlay.bHost) { if(lastav > gameTime)lastav= 0; if((gameTime-lastav) > AV_PING_FREQUENCY) { NETsetGameFlags(2,averagePing()); lastav=gameTime; } } // before we send the ping, if any player failed to respond to the last one // we should re-enumerate the players. for (i=0; ibody[1] ==1) // if this is a new ping { reply.body[0] = (char) selectedPlayer; reply.body[1] = 0; reply.size = 2; reply.type = NET_PING; NETsend(&reply, player2dpid[(int) ping->body[0]], FALSE); // reply to it } else // else it's returned, so store it. { ingame.PingTimes[(int) ping->body[0]] = (gameTime2 - PingSend[(int) ping->body[0]] ) /2 ; PingSend[(int) ping->body[0]] = 0; // note we've recvd it! } return TRUE; }