Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: orxonox.OLD/trunk/src/lib/network/network_game_manager.cc @ 6450

Last change on this file since 6450 was 6424, checked in by bensch, 19 years ago

orxonox/trunk: merged the branche network back to the trunk
merged with command:
svn merge https://svn.orxonox.net/orxonox/branches/network . -r 6351:HEAD
no conflicts

File size: 19.2 KB
Line 
1/*
2   orxonox - the future of 3D-vertical-scrollers
3
4   Copyright (C) 2004 orx
5
6   This program is free software; you can redistribute it and/or modify
7   it under the terms of the GNU General Public License as published by
8   the Free Software Foundation; either version 2, or (at your option)
9   any later version.
10
11### File Specific:
12   main-programmer: Benjamin Wuest
13   co-programmer: ...
14*/
15
16
17/* this is for debug output. It just says, that all calls to PRINT() belong to the DEBUG_MODULE_NETWORK module
18   For more information refere to https://www.orxonox.net/cgi-bin/trac.cgi/wiki/DebugOutput
19*/
20#define DEBUG_MODULE_NETWORK
21
22#include "factory.h"
23#include "network_stream.h"
24#include "converter.h"
25
26#include "p_node.h"
27
28
29/* include your own header */
30#include "network_game_manager.h"
31
32
33/* using namespace std is default, this needs to be here */
34using namespace std;
35
36NetworkGameManager* NetworkGameManager::singletonRef = NULL;
37
38/*!
39 * Standard constructor
40 */
41NetworkGameManager::NetworkGameManager()
42{
43  PRINTF(0)("START\n");
44
45  /* set the class id for the base object */
46  this->setClassID(CL_NETWORK_GAME_MANAGER, "NetworkGameManager");
47
48  allOutBuffer.length = 0;
49
50  allOutBuffer.maxLength = 10*1024;
51
52  allOutBuffer.buffer = new byte[10*1024];
53
54  newUniqueID = MAX_CONNECTIONS + 2;
55
56  hasRequestedWorld = false;
57}
58
59/*!
60 * Standard destructor
61 */
62NetworkGameManager::~NetworkGameManager()
63{
64  for ( int i = 0; i<outBuffer.size(); i++)
65  {
66    if ( outBuffer[i].buffer )
67      delete outBuffer[i].buffer;
68  }
69
70  if ( allOutBuffer.buffer )
71    delete allOutBuffer.buffer;
72}
73
74
75int NetworkGameManager::writeBytes(const byte* data, int length, int sender)
76{
77  int i = 0;
78  byte b;
79
80  while ( i<length )
81  {
82    b = data[i++];
83
84    PRINTF(0)("WriteBytes: b = %d\n", b);
85
86    if ( isServer() )
87    {
88      if ( b == REQUEST_CREATE )
89      {
90        if ( !handleRequestCreate( i, data, length, sender ) )
91          return i;
92        continue;
93      }
94      if ( b == REQUEST_REMOVE )
95      {
96        if ( !handleRequestRemove( i, data, length, sender ) )
97          return i;
98        continue;
99      }
100    }
101    else
102    {
103      if ( b == CREATE_ENTITY )
104      {
105        if ( !handleCreateEntity( i, data, length, sender ) )
106          return i;
107        continue;
108      }
109      if ( b == REMOVE_ENTITY )
110      {
111        if ( !handleRemoveEntity( i, data, length, sender ) )
112          return i;
113        continue;
114      }
115      if ( b == CREATE_ENTITY_LIST )
116      {
117        if ( !handleCreateEntityList( i, data, length, sender ) )
118          return i;
119        continue;
120      }
121      if ( b == REMOVE_ENTITY_LIST )
122      {
123        if ( !handleRemoveEntityList( i, data, length, sender ) )
124          return i;
125        continue;
126      }
127      if ( b == YOU_ARE_ENTITY )
128      {
129        if ( !handleYouAreEntity( i, data, length, sender ) )
130          return i;
131        continue;
132      }
133    }
134
135    if ( b == REQUEST_SYNC )
136    {
137      if ( !handleRequestSync( i, data, length, sender ) )
138        return i;
139      continue;
140    }
141
142    if ( b == REQUEST_ENTITY_LIST )
143    {
144      PRINTF(0)("sending THE list\n");
145      sendEntityList( sender );
146      continue;
147    }
148
149    //if we get her something with data is wrong
150    PRINTF(1)("Data is not in the right format! i=%d\n", i);
151    return i;
152  }
153
154  return i;
155}
156
157int NetworkGameManager::readBytes(byte* data, int maxLength, int * reciever)
158{
159  if ( !isServer() && !hasRequestedWorld )
160  {
161    SYNCHELP_WRITE_BEGIN();
162    byte b = REQUEST_ENTITY_LIST;
163    SYNCHELP_WRITE_BYTE( b );
164    hasRequestedWorld = true;
165    PRINTF(0)("the world is enough! id=%d\n", this->getUniqueID());
166    return SYNCHELP_WRITE_N;
167  }
168  for ( int i = 0; i<outBuffer.size(); i++ )
169  {
170    *reciever = i;
171    if ( outBuffer[i].length>0 )
172    {
173      int nbytes = outBuffer[i].length;
174      outBuffer[i].length = 0;
175
176      if ( nbytes > maxLength )
177      {
178        PRINTF(1)("OutBuffer.length (%d) > (%d) networkStreamBuffer.maxLength\n", nbytes, maxLength);
179        return 0;
180      }
181
182      memcpy(data, outBuffer[i].buffer, nbytes);
183      return nbytes;
184    }
185  }
186
187  *reciever = 0;
188  int nbytes = allOutBuffer.length;
189  allOutBuffer.length = 0;
190
191  if ( nbytes <=0 )
192    return 0;
193
194  if ( nbytes > maxLength )
195  {
196    PRINTF(1)("OutBuffer.length (%d) > (%d) networkStreamBuffer.length\n", nbytes, maxLength);
197    return 0;
198  }
199
200  memcpy( data, allOutBuffer.buffer, nbytes );
201  return nbytes;
202}
203
204void NetworkGameManager::writeDebug() const
205{
206}
207
208void NetworkGameManager::readDebug() const
209{
210}
211
212
213/*!
214 * Checks whether this is connected to a server or a client
215 * and afterwards creates the needed entity
216 * @param classID: The ID of the class of which an entity should be created
217 */
218void NetworkGameManager::createEntity( ClassID classID, int owner )
219{
220  if ( this->isServer() )
221  {
222    if ( newUniqueID < 0 )
223    {
224      PRINTF(1)("Cannot create entity! There are no more uniqueIDs left!\n");
225      return;
226    }
227
228    this->executeCreateEntity( classID, newUniqueID++, owner );
229  }
230  else
231  {
232    this->requestCreateEntity( classID );
233  }
234}
235
236
237/*!
238 * Checks whether this is connected to a server or a client
239 * and afterwards creates the needed entity
240 * @param classID: The ID of the class of which an entity should be created
241 */
242BaseObject* NetworkGameManager::createEntity(const TiXmlElement* element)
243{
244  if ( this->isServer() )
245  {
246    if ( newUniqueID < 0 )
247    {
248      PRINTF(1)("Cannot create entity! There are no more uniqueIDs left!\n");
249      return NULL;
250    }
251    newUniqueID++;
252
253    BaseObject * b = Factory::fabricate( element );
254
255    if ( !b )
256    {
257      PRINTF(1)("Could not fabricate Object with className %s\n", element->Value() );
258      return NULL;
259    }
260
261
262    if ( b->isA(CL_SYNCHRONIZEABLE) )
263    {
264      Synchronizeable * s = dynamic_cast<Synchronizeable*>(b);
265      s->setUniqueID( newUniqueID );
266      s->setOwner( 0 );
267      this->networkStream->connectSynchronizeable( *s );
268      return b;
269    }
270    else
271    {
272      PRINTF(1)("Class %s is not a synchronizeable!\n", b->getClassName() );
273      delete b;
274    }
275  }
276  else
277  {
278    PRINTF(1)("This node is not a server and cannot create id %x\n", element->Value());
279  }
280  return NULL;
281}
282
283
284/*!
285 * Checks whether this is connected to a server or a client
286 * and afterwards removes the specified entity
287 * @param uniqueID: The ID of the entity object which should be removed
288 */
289void NetworkGameManager::removeEntity(int uniqueID)
290{
291  if ( this->isServer() )
292  {
293    this->executeRemoveEntity( uniqueID );
294  }
295  else
296  {
297    this->requestRemoveEntity( uniqueID );
298  }
299}
300
301
302
303/*!
304 * Creates the needed entity on the server if possible
305 * @param classID: The ID of the class of which an entity should be created
306 */
307void NetworkGameManager::requestCreateEntity(ClassID classID)
308{
309  if ( !writeToClientBuffer( allOutBuffer, (byte)REQUEST_CREATE ) )
310    return;
311  if ( !writeToClientBuffer( allOutBuffer, (int)classID ) )
312    return;
313}
314
315/*!
316 * Removes the specified entity on the server
317 * @param uniqueID: The ID of the entity object which should be removed
318 */
319void NetworkGameManager::requestRemoveEntity(int uniqueID)
320{
321  if ( !writeToClientBuffer( allOutBuffer, (byte)REQUEST_REMOVE ) )
322    return;
323  if ( !writeToClientBuffer( allOutBuffer, uniqueID ) )
324    return;
325}
326
327/*!
328 * Creates the needed entity if possible
329 * This function is called if this is a server
330 * @param classID: The ID of the class of which an entity should be created
331 */
332void NetworkGameManager::executeCreateEntity(ClassID classID, int uniqueID, int owner)
333{
334  if ( !writeToClientBuffer( allOutBuffer, (byte)CREATE_ENTITY ) )
335    return;
336  if ( !writeToClientBuffer( allOutBuffer, (int)classID ) )
337    return;
338  if ( !writeToClientBuffer( allOutBuffer, uniqueID ) )
339    return;
340  if ( !writeToClientBuffer( allOutBuffer, owner ) )
341    return;
342
343  doCreateEntity( classID, uniqueID, owner );
344}
345
346/*!
347 * Removes the specified entity
348 * This function is called if this is a server
349 * @param uniqueID: The ID of the entity object which should be removed
350 */
351void NetworkGameManager::executeRemoveEntity(int uniqueID)
352{
353  if ( !writeToClientBuffer( allOutBuffer, (byte)REMOVE_ENTITY ) )
354    return;
355  if ( !writeToClientBuffer( allOutBuffer, uniqueID ) )
356    return;
357
358  doRemoveEntity(uniqueID);
359}
360
361/*!
362 * Checks whether it is possible to create an entity of a given class
363 * @return: true if the entity can be created, false otherwise
364 */
365bool NetworkGameManager::canCreateEntity(ClassID classID)
366{
367  return true;
368}
369
370/*!
371 * Sends the Entities to the new connected client
372 * @param userID: The ID of the user
373 */
374void NetworkGameManager::sendEntityList( int userID )
375{
376  if ( !isServer() )
377    return;
378
379  if ( userID >= outBuffer.size() )
380    resizeBufferVector( userID );
381
382  SynchronizeableList::const_iterator it, e;
383
384  it = this->networkStream->getSyncBegin();
385  e = this->networkStream->getSyncEnd();
386
387  if ( !writeToClientBuffer( outBuffer[userID], (byte)CREATE_ENTITY_LIST ) )
388    return;
389
390  // -2 because you must not send network_game_manager and handshake
391  if ( !writeToClientBuffer( outBuffer[userID], networkStream->getSyncCount() ) )
392    return;
393
394  //PRINTF(0)("SendEntityList: n = %d\n", networkStream->getSyncCount()-2 );
395
396  int n = 0;
397
398  while ( it != e )
399  {
400
401    if ( !writeToClientBuffer( outBuffer[userID], (int)((*it)->getLeafClassID()) ) )
402      return;
403      //PRINTF(0)("SendEntityList: ClassID = %x\n", (*it)->getRealClassID());
404
405    if ( !writeToClientBuffer( outBuffer[userID], (int)((*it)->getUniqueID()) ) )
406      return;
407
408    if ( !writeToClientBuffer( outBuffer[userID], (int)((*it)->getOwner()) ) )
409      return;
410
411
412    PRINTF(0)("id = %x %s %s\n", (int)((*it)->getLeafClassID()), (*it)->getClassName(), (*it)->getName());
413
414
415    /*if ( (*it)->isA(CL_WORLD_ENTITY) )
416    {
417      n = dynamic_cast<WorldEntity*>((*it))->readState( outBuffer[userID].buffer, outBuffer[userID].maxLength-outBuffer[userID].length );
418      if ( n = 0 )
419      {
420        PRINTF(2)("n = 0\n");
421      }
422      outBuffer[userID].length += n;
423  }*/
424
425    it++;
426  }
427
428
429}
430
431/**
432 * Creates a buffer for user n
433 * @param n The ID of the user
434 */
435void NetworkGameManager::resizeBufferVector( int n )
436{
437  for ( int i = outBuffer.size(); i<=n; i++)
438  {
439    clientBuffer outBuf;
440
441    outBuf.length = 0;
442
443    outBuf.maxLength = 5*1024;
444
445    outBuf.buffer = new byte[5*1014];
446
447    outBuffer.push_back(outBuf);
448  }
449}
450
451/**
452 * Creates the entity on this host
453 * @param classID: ClassID of the entity to create
454 * @param uniqueID: Unique ID to assign to the synchronizeable
455 * @param owner: owner of this synchronizealbe
456 */
457BaseObject* NetworkGameManager::doCreateEntity( ClassID classID, int uniqueID, int owner )
458{
459  BaseObject * b = Factory::fabricate( classID );
460
461  if ( !b )
462  {
463    PRINTF(1)("Could not fabricate Object with classID %x\n", classID);
464    return NULL;
465  }
466
467  if ( b->isA(CL_SYNCHRONIZEABLE) )
468  {
469    Synchronizeable * s = dynamic_cast<Synchronizeable*>(b);
470    s->setUniqueID( uniqueID );
471    s->setOwner( owner );
472    this->networkStream->connectSynchronizeable( *s );
473    if ( !isServer() )
474      s->setIsOutOfSync( true );
475    PRINTF(0)("Fabricated %s with id %d\n", s->getClassName(), s->getUniqueID());
476    return b;
477  }
478  else
479  {
480    PRINTF(1)("Class with ID %x is not a synchronizeable!", (int)classID);
481    delete b;
482  }
483  return NULL;
484}
485
486/**
487 * Removes a entity on this host
488 * @param uniqueID: unique ID assigned with the entity to remove
489 */
490void NetworkGameManager::doRemoveEntity( int uniqueID )
491{
492  SynchronizeableList::const_iterator it,e;
493  it = this->networkStream->getSyncBegin();
494  e = this->networkStream->getSyncEnd();
495
496  while ( it != e )
497  {
498    if ( (*it)->getUniqueID() == uniqueID )
499    {
500      delete *it;
501      break;
502    }
503    it++;
504  }
505}
506
507/**
508 * Tell the synchronizeable that a user's synchronizeable is out of sync
509 * @param uniqueID: unique ID assigned with the entity which is out of sync
510 * @param userID: user ID who's synchronizeable is out of sync
511 */
512void NetworkGameManager::doRequestSync( int uniqueID, int userID )
513{
514  SynchronizeableList::const_iterator it,e;
515  it = this->networkStream->getSyncBegin();
516  e = this->networkStream->getSyncEnd();
517
518  while ( it != e )
519  {
520    if ( (*it)->getUniqueID() == uniqueID )
521    {
522      (*it)->requestSync( userID );
523      break;
524    }
525    it++;
526  }
527}
528
529/**
530 * Copies length bytes to the clientBuffer with error checking
531 * @param clientBuffer: the clientBuffer to write to
532 * @param data: buffer to the data
533 * @param length: length of data
534 * @return false on error true else
535 */
536bool NetworkGameManager::writeToClientBuffer( clientBuffer &cb, byte * data, int length )
537{
538  if ( length > cb.maxLength-cb.length )
539  {
540    PRINTF(1)("No space left in clientBuffer\n");
541    return false;
542  }
543
544  memcpy( cb.buffer+cb.length, data, length );
545  return true;
546}
547
548/**
549 * Reads data from clientBuffer with error checking
550 * @param clientBuffer: the clientBuffer to read from
551 * @param data: pointer to the buffer
552 * @param length:
553 * @return
554 */
555bool NetworkGameManager::readFromClientBuffer( clientBuffer &cb, byte * data, int length )
556{
557  if ( cb.length < length )
558  {
559    PRINTF(0)("There is not enough data in clientBuffer\n");
560    return 0;
561  }
562
563  memcpy( data, cb.buffer+cb.length-length, length );
564  return true;
565}
566
567/**
568 * Tells this client that he has to control this entity
569 * @param uniqueID: the entity's uniqeID
570 */
571void NetworkGameManager::doYouAre( int uniqueID )
572{
573  //TODO: what has to be done
574}
575
576/**
577 * Tells a remote client that he has to control this entity
578 * @param uniqueID: the entity's uniqeID
579 * @param userID: the users ID
580 */
581void NetworkGameManager::sendYouAre( int uniqueID, int userID )
582{
583  if ( !isServer() )
584    return;
585
586  if ( userID != 0 )
587  {
588    if ( !writeToClientBuffer( outBuffer[userID], (byte)YOU_ARE_ENTITY ) )
589      return;
590
591    if ( !writeToClientBuffer( outBuffer[userID], uniqueID ) )
592      return;
593  }
594  else
595  {
596    doYouAre(uniqueID);
597  }
598}
599
600bool NetworkGameManager::handleRequestCreate( int & i, const byte * data, int length, int sender )
601{
602  if ( INTSIZE > length-i )
603  {
604    PRINTF(1)("Cannot read classID from buffer! Not enough data left!\n");
605    return false;
606  }
607  int classID;
608  i += Converter::byteArrayToInt( &data[i], &classID );
609
610  createEntity( (ClassID)classID );
611
612  return true;
613}
614
615bool NetworkGameManager::handleRequestRemove( int & i, const byte * data, int length, int sender )
616{
617  if ( INTSIZE > length-i )
618  {
619    PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
620    return false;
621  }
622  int uniqueID;
623  i += Converter::byteArrayToInt( &data[i], &uniqueID );
624
625  removeEntity( uniqueID );
626
627  return true;
628}
629
630bool NetworkGameManager::handleCreateEntity( int & i, const byte * data, int length, int sender )
631{
632  if ( INTSIZE > length-i )
633  {
634    PRINTF(1)("Cannot read classID from buffer! Not enough data left!\n");
635    return false;
636  }
637  int classID;
638  i += Converter::byteArrayToInt( &data[i], &classID );
639
640  if ( INTSIZE > length-i )
641  {
642    PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
643    return false;
644  }
645  int uniqueID;
646  i += Converter::byteArrayToInt( &data[i], &uniqueID );
647
648  if ( INTSIZE > length-i )
649  {
650    PRINTF(1)("Cannot read owner from buffer! Not enough data left!\n");
651    return false;
652  }
653  int owner;
654  i += Converter::byteArrayToInt( &data[i], &owner );
655
656  doCreateEntity( (ClassID)classID, uniqueID, owner );
657
658  return true;
659}
660
661bool NetworkGameManager::handleRemoveEntity( int & i, const byte * data, int length, int sender )
662{
663  if ( INTSIZE > length-i )
664  {
665    PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
666    return false;
667  }
668  int uniqueID;
669  i += Converter::byteArrayToInt( &data[i], &uniqueID );
670
671  doRemoveEntity( uniqueID );
672
673  return true;
674}
675
676bool NetworkGameManager::handleCreateEntityList( int & i, const byte * data, int length, int sender )
677{
678  if ( INTSIZE > length-i )
679  {
680    PRINTF(1)("Cannot read n from buffer! Not enough data left!\n");
681    return false;
682  }
683
684  PRINTF(0)("HandleCreateEntityList:  data[i..i+3] = %d %d %d %d\n", data[i], data[i+1], data[i+2], data[i+3]);
685
686  int n;
687  i += Converter::byteArrayToInt( &data[i], &n );
688
689
690  PRINTF(0)("HandleCreateEntityList: n = %d\n", n);
691
692  int classID, uniqueID, owner;
693
694  for ( int j = 0; j<n; j++ )
695  {
696
697    if ( INTSIZE > length-i )
698    {
699      PRINTF(1)("Cannot read classID from buffer! Not enough data left!\n");
700      return false;
701    }
702    i += Converter::byteArrayToInt( &data[i], &classID );
703
704    if ( INTSIZE > length-i )
705    {
706      PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
707      return false;
708    }
709    i += Converter::byteArrayToInt( &data[i], &uniqueID );
710
711    if ( INTSIZE > length-i )
712    {
713      PRINTF(1)("Cannot read owner from buffer! Not enough data left!\n");
714      return false;
715    }
716    i += Converter::byteArrayToInt( &data[i], &owner );
717
718    PRINTF(0)("before fabricate\n");
719    if ( classID != CL_NETWORK_GAME_MANAGER && classID != CL_HANDSHAKE )
720    {
721      BaseObject* b = doCreateEntity( (ClassID)classID, uniqueID, owner );
722
723      /*if ( b != NULL )
724      {
725        if ( b->isA(CL_WORLD_ENTITY) )
726        {
727          int n = dynamic_cast<WorldEntity*>(b)->writeState( data, length, sender );
728
729          i += n;
730        }
731    }*/
732    }
733
734  }
735
736  return true;
737}
738
739bool NetworkGameManager::handleRemoveEntityList( int & i, const byte * data, int length, int sender )
740{
741  if ( INTSIZE > length-i )
742  {
743    PRINTF(1)("Cannot read n from buffer! Not enough data left!\n");
744    return false;
745  }
746  int n;
747  i += Converter::byteArrayToInt( &data[i], &n );
748
749  int uniqueID;
750
751  for ( int j = 0; j<n; j++ )
752  {
753
754    if ( INTSIZE > length-i )
755    {
756      PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
757      return false;
758    }
759    i += Converter::byteArrayToInt( &data[i], &uniqueID );
760
761    doRemoveEntity( uniqueID );
762  }
763
764  return true;
765}
766
767bool NetworkGameManager::handleYouAreEntity( int & i, const byte * data, int length, int sender )
768{
769  if ( INTSIZE > length-i )
770  {
771    PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
772    return false;
773  }
774
775  int uniqueID;
776  i += Converter::byteArrayToInt( &data[i], &uniqueID );
777
778  doYouAre( uniqueID );
779
780  return true;
781}
782
783bool NetworkGameManager::handleRequestSync( int & i, const byte * data, int length, int sender )
784{
785  if ( INTSIZE > length-i )
786  {
787    PRINTF(1)("Cannot read uniqueID from buffer! Not enough data left!\n");
788    return false;
789  }
790  int uniqueID;
791  i += Converter::byteArrayToInt( &data[i], &uniqueID );
792
793  doRequestSync( uniqueID, sender );
794
795  return true;
796}
797
798bool NetworkGameManager::writeToClientBuffer( clientBuffer & cb, byte b )
799{
800  if ( cb.maxLength-cb.length < 1 )
801  {
802    PRINTF(1)("Cannot write to clientBuffer! Not enough space for 1 byte\n");
803    return false;
804  }
805
806  cb.buffer[cb.length++] = b;
807
808  return true;
809}
810
811bool NetworkGameManager::writeToClientBuffer( clientBuffer & cb, int i )
812{
813  int n = Converter::intToByteArray( i, cb.buffer+cb.length, cb.maxLength-cb.length );
814  cb.length += n;
815
816  if ( n <= 0 )
817  {
818    PRINTF(1)("Cannot write to clientBuffer! Not enough space for 1 int\n");
819    return false;
820  }
821
822  return true;
823}
824
825void NetworkGameManager::sync( int uniqueID, int owner )
826{
827  if ( owner==this->getHostID() )
828    return;
829
830  if ( !isServer() )
831    executeRequestSync( uniqueID, 0 );
832  else
833    executeRequestSync( uniqueID, owner );
834}
835
836void NetworkGameManager::executeRequestSync( int uniqueID, int user )
837{
838  if ( user >= outBuffer.size() )
839    resizeBufferVector( user );
840
841  if ( !writeToClientBuffer( outBuffer[user], (byte)REQUEST_SYNC ) )
842    return;
843  if ( !writeToClientBuffer( outBuffer[user], uniqueID ) )
844    return;
845}
846
Note: See TracBrowser for help on using the repository browser.