- Timestamp:
- Sep 12, 2008, 5:36:57 PM (16 years ago)
- Location:
- code/branches/network
- Files:
-
- 2 added
- 13 edited
Legend:
- Unmodified
- Added
- Removed
-
code/branches/network/TODO
r1534 r1775 1 - should we use enet_peer_ping to test if a client is still alive ? 2 - enet_host_broadcast ? (to all peers) 1 todo: 2 !!! check that enet does not cause a packet traffic jam when a reliable packet gets missed !!! 3 spaceship of client vanishes on server 4 new gamestate concept 5 -
code/branches/network/src/network/CMakeLists.txt
r1740 r1775 17 17 packet/Gamestate.cc 18 18 packet/Welcome.cc 19 packet/DeleteObjects.cc 19 20 ) 20 21 -
code/branches/network/src/network/Client.cc
r1769 r1775 170 170 if(!isSynched_) 171 171 isSynched_=true; 172 }else 173 COUT(3) << "gamestate has not been processed sucessfully" << std::endl; 172 } 174 173 gamestate.cleanup(); 175 174 return; -
code/branches/network/src/network/GamestateClient.cc
r1769 r1775 156 156 if(myShip_){ 157 157 // unsigned char *data = new unsigned char[myShip_->getSize()]; 158 int size=myShip_->getSize 2(0, 0x1);158 int size=myShip_->getSize(0, 0x1); 159 159 if(size==0) 160 160 return false; -
code/branches/network/src/network/Server.cc
r1755 r1775 56 56 #include "packet/Packet.h" 57 57 #include "packet/Welcome.h" 58 #include "packet/DeleteObjects.h" 58 59 #include <util/Convert.h> 59 60 … … 229 230 //std::cout << "updated gamestate, sending it" << std::endl; 230 231 //if(clients->getGamestateID()!=GAMESTATEID_INITIAL) 232 sendObjectDeletes(); 231 233 sendGameState(); 232 234 COUT(5) << "Server: one sendGameState turn complete, repeat in next tick" << std::endl; … … 276 278 // gs gets automatically deleted by enet callback 277 279 } 278 /*if(added) {279 //std::cout << "send gamestates from server.cc in sendGameState" << std::endl;280 return connection->sendPackets();281 }*/282 //COUT(5) << "Server: had no gamestates to send" << std::endl;283 280 return true; 284 281 } 285 282 283 bool Server::sendObjectDeletes(){ 284 ClientInformation *temp = ClientInformation::getBegin(); 285 packet::DeleteObjects *del = new packet::DeleteObjects(); 286 if(!del->fetchIDs()) 287 return true; //everything ok (no deletes this tick) 288 while(temp != NULL){ 289 if( !(temp->getSynched()) ){ 290 COUT(5) << "Server: not sending gamestate" << std::endl; 291 temp=temp->next(); 292 continue; 293 } 294 int cid = temp->getID(); //get client id 295 packet::DeleteObjects *cd = new packet::DeleteObjects(*del); 296 assert(cd); 297 cd->setClientID(cid); 298 if ( !cd->send() ) 299 COUT(3) << "Server: packet with client id (cid): " << cid << " not sended: " << temp->getFailures() << std::endl; 300 temp=temp->next(); 301 // gs gets automatically deleted by enet callback 302 } 303 return true; 304 } 305 286 306 // void Server::processChat( chat *data, int clientId){ 287 307 // char *message = new char [strlen(data->message)+10+1]; -
code/branches/network/src/network/Server.h
r1735 r1775 85 85 bool processPacket( ENetPacket *packet, ENetPeer *peer ); 86 86 bool sendGameState(); 87 87 bool sendObjectDeletes(); 88 88 89 89 //void processChat( chat *data, int clientId); -
code/branches/network/src/network/Synchronisable.cc
r1758 r1775 52 52 { 53 53 54 std::map<unsigned int, Synchronisable *> Synchronisable::objectMap_; 55 std::queue<unsigned int> Synchronisable::deletedObjects_; 54 56 55 57 int Synchronisable::state_=0x1; // detemines wheter we are server (default) or client … … 61 63 Synchronisable::Synchronisable(){ 62 64 RegisterRootObject(Synchronisable); 63 static int idCounter=0;65 static unsigned int idCounter=0; 64 66 datasize=0; 65 67 objectFrequency_=1; … … 67 69 objectID=idCounter++; 68 70 syncList = new std::list<synchronisableVariable *>; 69 //registerAllVariables();70 71 } 71 72 72 73 Synchronisable::~Synchronisable(){ 73 74 // delete callback function objects 74 if(!orxonox::Identifier::isCreatingHierarchy()) 75 if(!orxonox::Identifier::isCreatingHierarchy()){ 75 76 for(std::list<synchronisableVariable *>::iterator it = syncList->begin(); it!=syncList->end(); it++) 76 77 delete (*it)->callback; 78 assert(objectMap_[objectID]->objectID==objectID); 79 objectMap_.erase(objectID); 80 } 77 81 } 78 82 79 83 bool Synchronisable::create(){ 84 objectMap_[objectID]=this; 85 assert(objectMap_[objectID]==this); 80 86 this->classID = this->getIdentifier()->getNetworkID(); 81 87 COUT(4) << "creating synchronisable: setting classid from " << this->getIdentifier()->getName() << " to: " << classID << std::endl; … … 91 97 } 92 98 93 boolSynchronisable::fabricate(unsigned char*& mem, int mode)99 Synchronisable *Synchronisable::fabricate(unsigned char*& mem, int mode) 94 100 { 95 101 unsigned int size, objectID, classID; … … 98 104 classID = *(unsigned int*)(mem+2*sizeof(unsigned int)); 99 105 100 if(size==3*sizeof(unsigned int)){ //not our turn, dont do anything101 mem+=3*sizeof(unsigned int);102 return true;103 }104 105 106 orxonox::Identifier* id = GetIdentifier(classID); 106 if(!id){ 107 COUT(3) << "We could not identify a new object; classid: " << classID << " uint: " << (unsigned int)classID << " objectID: " << objectID << " size: " << size << std::endl; 108 assert(0); 109 return false; // most probably the gamestate is corrupted 110 } 107 assert(id); 111 108 orxonox::BaseObject *bo = id->fabricate(); 112 109 Synchronisable *no = dynamic_cast<Synchronisable *>(bo); … … 116 113 COUT(3) << "fabricate objectID: " << no->objectID << " classID: " << no->classID << std::endl; 117 114 // update data and create object/entity... 118 if( !no->updateData(mem, mode) ){ 119 COUT(1) << "We couldn't update the object: " << objectID << std::endl; 120 return false; 121 } 122 if( !no->create() ) 123 { 124 COUT(1) << "We couldn't manifest (create() ) the object: " << objectID << std::endl; 125 return false; 126 } 115 assert(no->updateData(mem, mode)); 116 assert( no->create() ); 117 return no; 118 } 119 120 121 bool Synchronisable::deleteObject(unsigned int objectID){ 122 assert(getSynchronisable(objectID)); 123 assert(getSynchronisable(objectID)->objectID==objectID); 124 delete objectMap_[objectID]; 127 125 return true; 128 126 } 129 127 128 Synchronisable* Synchronisable::getSynchronisable(unsigned int objectID){ 129 std::map<unsigned int, Synchronisable *>::iterator i = objectMap_.find(objectID); 130 if(i==objectMap_.end()) 131 return NULL; 132 assert(i->second->objectID==objectID); 133 return (*i).second; 134 } 135 136 130 137 /** 131 138 * This function is used to register a variable to be synchronized … … 161 168 */ 162 169 bool Synchronisable::getData(unsigned char*& mem, unsigned int id, int mode){ 170 //if this tick is we dont synchronise, then abort now 171 if(!isMyTick(id)) 172 return true; 163 173 //std::cout << "inside getData" << std::endl; 164 174 unsigned int tempsize = 0; … … 170 180 std::list<synchronisableVariable *>::iterator i; 171 181 unsigned int size; 172 size=getSize 2(id, mode);182 size=getSize(id, mode); 173 183 174 184 // start copy header … … 182 192 // end copy header 183 193 184 //if this tick is we dont synchronise, then abort now185 if(!isMyTick(id))186 return true;187 194 188 195 COUT(5) << "Synchronisable getting data from objectID: " << objectID << " classID: " << classID << " length: " << size << std::endl; … … 222 229 */ 223 230 bool Synchronisable::updateData(unsigned char*& mem, int mode){ 224 unsigned char *data = mem;225 231 if(mode==0x0) 226 232 mode=state_; … … 230 236 return false; 231 237 } 232 238 unsigned char *data=mem; 233 239 // start extract header 240 if(!isMyData(mem)) 241 return true; 234 242 unsigned int objectID, classID, size; 235 243 size = *(int *)mem; … … 242 250 assert(this->objectID==objectID); 243 251 assert(this->classID==classID); 244 if(size==3*sizeof(unsigned int)) //if true, only the header is available245 return true;246 //assert(0);247 252 248 253 COUT(5) << "Synchronisable: objectID " << objectID << ", classID " << classID << " size: " << size << " synchronising data" << std::endl; … … 288 293 if(!isMyTick(id)) 289 294 return 0; 290 int tsize= 0;295 int tsize=sizeof(synchronisableHeader); 291 296 if(mode==0x0) 292 297 mode=state_; … … 311 316 312 317 /** 313 * This function returns the total amount of bytes needed by getData to save the whole content of the variables314 * @return amount of bytes315 */316 int Synchronisable::getSize2(unsigned int id, int mode){317 return sizeof(synchronisableHeader) + getSize( id, mode );318 }319 320 /**321 318 * 322 319 * @param id -
code/branches/network/src/network/Synchronisable.h
r1751 r1775 33 33 34 34 #include <list> 35 #include <map> 36 #include <queue> 35 37 #include "core/OrxonoxClass.h" 36 38 #include "util/XMLIncludes.h" … … 74 76 void registerVar(void *var, int size, variableType t, int mode=1, NetworkCallbackBase *cb=0); 75 77 bool getData(unsigned char*& men, unsigned int id, int mode=0x0); 76 int getSize 2(unsigned int id, int mode=0x0);78 int getSize(unsigned int id, int mode=0x0); 77 79 bool updateData(unsigned char*& mem, int mode=0x0); 78 80 bool isMyData(unsigned char* mem); … … 84 86 static void setClient(bool b); 85 87 86 static bool fabricate(unsigned char*& mem, int mode=0x0); 88 static Synchronisable *fabricate(unsigned char*& mem, int mode=0x0); 89 static bool deleteObject(unsigned int objectID); 90 static Synchronisable *getSynchronisable(unsigned int objectID); 91 static unsigned int getNumberOfDeletedObject(){ return deletedObjects_.size(); } 92 static unsigned int popDeletedObject(){ unsigned int i = deletedObjects_.front(); deletedObjects_.pop(); return i; } 93 94 87 95 protected: 88 96 Synchronisable(); 89 97 private: 90 int getSize(unsigned int id, int mode=0x0);91 98 bool isMyTick(unsigned int id); 92 99 std::list<synchronisableVariable *> *syncList; … … 96 103 unsigned int objectFrequency_; 97 104 int objectMode_; 105 static std::map<unsigned int, Synchronisable *> objectMap_; 106 static std::queue<unsigned int> deletedObjects_; 98 107 }; 99 108 } -
code/branches/network/src/network/packet/Gamestate.cc
r1767 r1775 46 46 #define HEADER GAMESTATE_HEADER(data_) 47 47 48 48 49 Gamestate::Gamestate() 49 50 { … … 80 81 orxonox::ObjectList<Synchronisable>::iterator it; 81 82 for(it = orxonox::ObjectList<Synchronisable>::begin(); it; ++it){ 82 tempsize=it->getSize 2(id, mode);83 tempsize=it->getSize(id, mode); 83 84 84 85 if(currentsize+tempsize > size){ … … 88 89 int addsize=tempsize; 89 90 while(++temp) 90 addsize+=temp->getSize 2(id, mode);91 addsize+=temp->getSize(id, mode); 91 92 data_ = (unsigned char *)realloc(data_, sizeof(GamestateHeader) + currentsize + addsize); 92 93 if(!data_) … … 105 106 HEADER->packetType = ENUM::Gamestate; 106 107 assert( *(ENUM::Type *)(data_) == ENUM::Gamestate); 107 HEADER-> normsize = currentsize;108 HEADER->datasize = currentsize; 108 109 HEADER->id = id; 109 110 HEADER->diffed = false; … … 125 126 unsigned char *mem=data_+sizeof(GamestateHeader); 126 127 // get the start of the Synchronisable list 127 orxonox::ObjectList<Synchronisable>::iterator it=orxonox::ObjectList<Synchronisable>::begin(); 128 129 while(mem < data_+sizeof(GamestateHeader)+HEADER->normsize){ 130 // extract synchronisable header 128 //orxonox::ObjectList<Synchronisable>::iterator it=orxonox::ObjectList<Synchronisable>::begin(); 129 Synchronisable *s; 130 131 // update the data of the objects we received 132 while(mem < data_+sizeof(GamestateHeader)+HEADER->datasize){ 131 133 size = *(unsigned int *)mem; 132 134 objectID = *(unsigned int*)(mem+sizeof(unsigned int)); 133 135 classID = *(unsigned int*)(mem+2*sizeof(unsigned int)); 134 136 135 if(!it || it->objectID!=objectID || it->classID!=classID){ 136 // bad luck ;) 137 // delete the synchronisable (obviously seems to be deleted on the server) 138 while(it && it->objectID!=objectID) 139 removeObject(it); 140 141 if(!it){ 142 //fabricate the new synchronisable 143 if(!Synchronisable::fabricate(mem, mode)) 144 return false; 145 it=orxonox::ObjectList<Synchronisable>::end(); 146 }else{ 147 if(! it->updateData(mem, mode)) 148 { 149 COUT(1) << "We couldn't update objectID: " \ 150 << objectID << "; classID: " << classID << std::endl; 151 } 152 } 153 } else 137 s = Synchronisable::getSynchronisable( objectID ); 138 if(!s) 154 139 { 155 // we have our object 156 if(! it->updateData(mem, mode)) 157 { 158 COUT(1) << "We couldn't update objectID: " \ 159 << objectID << "; classID: " << classID << std::endl; 160 } 140 s = Synchronisable::fabricate(mem, mode); 141 if(!s) 142 return false; 161 143 } 162 ++it; 163 } 164 165 return true; 166 } 144 else 145 { 146 if(!s->updateData(mem, mode)) 147 return false; 148 } 149 } 150 151 return true; 152 } 153 154 167 155 168 156 int Gamestate::getID(){ … … 177 165 else 178 166 { 179 return HEADER-> normsize+sizeof(GamestateHeader);167 return HEADER->datasize+sizeof(GamestateHeader); 180 168 } 181 169 } … … 186 174 assert(!isCompressed()); 187 175 assert(!gs.isCompressed()); 188 while(d1<data_+HEADER-> normsize)176 while(d1<data_+HEADER->datasize) 189 177 { 190 178 if(*d1!=*d2) … … 205 193 assert(HEADER); 206 194 assert(!HEADER->compressed); 207 uLongf buffer = (uLongf)(((HEADER-> normsize + 12)*1.01)+1);195 uLongf buffer = (uLongf)(((HEADER->datasize + 12)*1.01)+1); 208 196 if(buffer==0) 209 197 return false; … … 214 202 unsigned char *source = GAMESTATE_START(data_); 215 203 int retval; 216 retval = compress( dest, &buffer, source, (uLong)(HEADER-> normsize) );204 retval = compress( dest, &buffer, source, (uLong)(HEADER->datasize) ); 217 205 switch ( retval ) { 218 206 case Z_OK: COUT(5) << "G.St.Man: compress: successfully compressed" << std::endl; break; … … 223 211 #ifndef NDEBUG 224 212 //decompress and compare the start and the decompressed data 225 unsigned char *rdata = new unsigned char[HEADER-> normsize+sizeof(GamestateHeader)];213 unsigned char *rdata = new unsigned char[HEADER->datasize+sizeof(GamestateHeader)]; 226 214 unsigned char *d2 = GAMESTATE_START(rdata); 227 uLongf length2 = HEADER-> normsize;215 uLongf length2 = HEADER->datasize; 228 216 uncompress(d2, &length2, dest, buffer); 229 for(unsigned int i=0; i<HEADER-> normsize; i++){217 for(unsigned int i=0; i<HEADER->datasize; i++){ 230 218 assert(*(source+i)==*(d2+i)); 231 219 } … … 235 223 //copy and modify header 236 224 #ifndef NDEBUG 237 HEADER->crc32 = calcCRC(data_+sizeof(GamestateHeader), HEADER-> normsize);225 HEADER->crc32 = calcCRC(data_+sizeof(GamestateHeader), HEADER->datasize); 238 226 #endif 239 227 *GAMESTATE_HEADER(ndata) = *HEADER; … … 245 233 HEADER->compressed = true; 246 234 assert(HEADER->compressed); 247 COUT(3) << "gamestate compress normsize: " << HEADER->normsize << " compsize: " << HEADER->compsize << std::endl;235 COUT(3) << "gamestate compress datasize: " << HEADER->datasize << " compsize: " << HEADER->compsize << std::endl; 248 236 return true; 249 237 } … … 252 240 assert(HEADER); 253 241 assert(HEADER->compressed); 254 COUT(3) << "GameStateClient: uncompressing gamestate. id: " << HEADER->id << ", baseid: " << HEADER->base_id << ", normsize: " << HEADER->normsize << ", compsize: " << HEADER->compsize << std::endl;255 unsigned int normsize = HEADER->normsize;242 COUT(3) << "GameStateClient: uncompressing gamestate. id: " << HEADER->id << ", baseid: " << HEADER->base_id << ", datasize: " << HEADER->datasize << ", compsize: " << HEADER->compsize << std::endl; 243 unsigned int datasize = HEADER->datasize; 256 244 unsigned int compsize = HEADER->compsize; 257 245 unsigned int bufsize; 258 assert(compsize<= normsize);259 bufsize = normsize;246 assert(compsize<=datasize); 247 bufsize = datasize; 260 248 assert(bufsize!=0); 261 249 unsigned char *ndata = new unsigned char[bufsize + sizeof(GamestateHeader)]; … … 272 260 } 273 261 #ifndef NDEBUG 274 assert(HEADER->crc32==calcCRC(ndata+sizeof(GamestateHeader), HEADER-> normsize));262 assert(HEADER->crc32==calcCRC(ndata+sizeof(GamestateHeader), HEADER->datasize)); 275 263 #endif 276 264 … … 282 270 data_ = ndata; 283 271 HEADER->compressed = false; 284 assert(HEADER-> normsize==normsize);272 assert(HEADER->datasize==datasize); 285 273 assert(HEADER->compsize==compsize); 286 274 return true; … … 296 284 unsigned int of=0; // pointers offset 297 285 unsigned int dest_length=0; 298 dest_length=HEADER-> normsize;286 dest_length=HEADER->datasize; 299 287 if(dest_length==0) 300 288 return NULL; 301 289 unsigned char *ndata = new unsigned char[dest_length*sizeof(unsigned char)+sizeof(GamestateHeader)]; 302 290 unsigned char *dest = ndata + sizeof(GamestateHeader); 303 while(of < GAMESTATE_HEADER(base->data_)-> normsize && of < HEADER->normsize){291 while(of < GAMESTATE_HEADER(base->data_)->datasize && of < HEADER->datasize){ 304 292 *(dest+of)=*(basep+of)^*(gs+of); // do the xor 305 293 ++of; 306 294 } 307 if(GAMESTATE_HEADER(base->data_)-> normsize!=HEADER->normsize){295 if(GAMESTATE_HEADER(base->data_)->datasize!=HEADER->datasize){ 308 296 unsigned char n=0; 309 if(GAMESTATE_HEADER(base->data_)-> normsize < HEADER->normsize){297 if(GAMESTATE_HEADER(base->data_)->datasize < HEADER->datasize){ 310 298 while(of<dest_length){ 311 299 *(dest+of)=n^*(gs+of); … … 334 322 unsigned int of=0; // pointers offset 335 323 unsigned int dest_length=0; 336 dest_length=HEADER-> normsize;324 dest_length=HEADER->datasize; 337 325 if(dest_length==0) 338 326 return NULL; 339 327 unsigned char *ndata = new unsigned char[dest_length*sizeof(unsigned char)+sizeof(GamestateHeader)]; 340 328 unsigned char *dest = ndata + sizeof(GamestateHeader); 341 while(of < GAMESTATE_HEADER(base->data_)-> normsize && of < HEADER->normsize){329 while(of < GAMESTATE_HEADER(base->data_)->datasize && of < HEADER->datasize){ 342 330 *(dest+of)=*(basep+of)^*(gs+of); // do the xor 343 331 ++of; 344 332 } 345 if(GAMESTATE_HEADER(base->data_)-> normsize!=HEADER->normsize){333 if(GAMESTATE_HEADER(base->data_)->datasize!=HEADER->datasize){ 346 334 unsigned char n=0; 347 if(GAMESTATE_HEADER(base->data_)-> normsize < HEADER->normsize){335 if(GAMESTATE_HEADER(base->data_)->datasize < HEADER->datasize){ 348 336 while(of < dest_length){ 349 337 *(dest+of)=n^*(gs+of); … … 370 358 // get total size of gamestate 371 359 for(it = orxonox::ObjectList<Synchronisable>::begin(); it; ++it) 372 size+=it->getSize 2(id, mode); // size of the actual data of the synchronisable360 size+=it->getSize(id, mode); // size of the actual data of the synchronisable 373 361 // size+=sizeof(GamestateHeader); 374 362 return size; -
code/branches/network/src/network/packet/Gamestate.h
r1763 r1775 33 33 #endif 34 34 35 35 36 #ifndef NETWORK_PACKETGAMESTATE_H 36 37 #define NETWORK_PACKETGAMESTATE_H … … 44 45 int id; // id of the gamestate 45 46 unsigned int compsize; 46 unsigned int normsize;47 unsigned int datasize; 47 48 int base_id; // id of the base-gamestate diffed from 48 49 bool diffed; // wheter diffed or not … … 55 56 56 57 /** 57 @author 58 @author Oliver Scheuss 58 59 */ 59 60 class Gamestate: public Packet{ … … 84 85 void removeObject(orxonox::ObjectListIterator<Synchronisable> &it); 85 86 86 87 //Bytestream *bs_; 88 //GamestateHeader *header_; 87 private: 89 88 }; 90 89 -
code/branches/network/src/network/packet/Packet.cc
r1763 r1775 42 42 #include "Gamestate.h" 43 43 #include "Welcome.h" 44 #include "DeleteObjects.h" 44 45 #include "network/Host.h" 45 46 #include "core/CoreIncludes.h" … … 141 142 unsigned int clientID = ClientInformation::findClient(&peer->address)->getID(); 142 143 Packet *p; 143 COUT( 3) << "packet type: " << *(ENUM::Type *)&data[_PACKETID] << std::endl;144 COUT(5) << "packet type: " << *(ENUM::Type *)&data[_PACKETID] << std::endl; 144 145 switch( *(ENUM::Type *)(data + _PACKETID) ) 145 146 { 146 147 case ENUM::Acknowledgement: 147 COUT( 3) << "ack" << std::endl;148 COUT(4) << "ack" << std::endl; 148 149 p = new Acknowledgement( data, clientID ); 149 150 break; 150 151 case ENUM::Chat: 151 COUT( 3) << "chat" << std::endl;152 COUT(4) << "chat" << std::endl; 152 153 p = new Chat( data, clientID ); 153 154 break; 154 155 case ENUM::ClassID: 155 COUT( 3) << "classid" << std::endl;156 COUT(4) << "classid" << std::endl; 156 157 p = new ClassID( data, clientID ); 157 158 break; 158 159 case ENUM::Gamestate: 159 COUT( 3) << "gamestate" << std::endl;160 COUT(4) << "gamestate" << std::endl; 160 161 // TODO: remove brackets 161 162 p = new Gamestate( data, clientID ); 162 163 break; 163 164 case ENUM::Welcome: 164 COUT( 3) << "welcome" << std::endl;165 COUT(4) << "welcome" << std::endl; 165 166 p = new Welcome( data, clientID ); 167 break; 168 case ENUM::DeleteObjects: 169 COUT(4) << "deleteobjects" << std::endl; 170 p = new DeleteObjects( data, clientID ); 166 171 break; 167 172 default: -
code/branches/network/src/network/packet/Packet.h
r1763 r1775 47 47 ClassID, 48 48 Chat, 49 Welcome 49 Welcome, 50 DeleteObjects 50 51 }; 51 52 } -
code/branches/network/src/orxonox/objects/Model.cc
r1769 r1775 86 86 } 87 87 if(this->isExactlyA(Class(Model))) 88 setObjectFrequency( 300); //sync all 10 seconds (this only applies to asteroids and other isExactlyA(Model)88 setObjectFrequency(1); //sync all 10 seconds (this only applies to asteroids and other isExactlyA(Model) 89 89 return true; 90 90 }
Note: See TracChangeset
for help on using the changeset viewer.