Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: orxonox.OLD/branches/proxy/src/lib/network/synchronizeable.cc @ 9589

Last change on this file since 9589 was 9588, checked in by patrick, 18 years ago

found another bug: forward permissions from proxy to master

File size: 18.8 KB
RevLine 
[5523]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
[5547]11
[5523]12### File Specific:
[9406]13   main-programmer: Christoph Renner (rennerc@ee.ethz.ch)
14   co-programmer: Patrick Boenzli (patrick@orxonox.ethz.ch)
[5547]15*/
[5523]16
[6139]17#define DEBUG_MODULE_NETWORK
18
[6695]19#include "shared_network_data.h"
20#include "network_stream.h"
[5547]21#include "netdefs.h"
[7954]22#include "network_log.h"
[8068]23#include "network_game_manager.h"
[5529]24
[6695]25#include "state.h"
[5996]26
[6753]27#include <cassert>
[6695]28
29#include "synchronizeable.h"
30
[9406]31#include "converter.h"
[6695]32
33
[9406]34
[5547]35/**
[5807]36 *  default constructor
[5547]37 */
[5996]38Synchronizeable::Synchronizeable()
[5997]39{
[6341]40  this->setClassID(CL_SYNCHRONIZEABLE, "Synchronizeable");
[8068]41  this->owner = 0;
[9406]42//   this->setIsServer(SharedNetworkData::getInstance()->getHostID() == 0);
[6695]43  this->uniqueID = NET_UID_UNASSIGNED;
[6145]44  this->networkStream = NULL;
[6695]45  this->bSynchronize = false;
[9406]46
[6695]47  if( State::isOnline())
48  {
49    NetworkStream* nd = SharedNetworkData::getInstance()->getDefaultSyncStream();
50    assert(nd != NULL);
51    nd->connectSynchronizeable(*this);
52    this->setUniqueID(SharedNetworkData::getInstance()->getNewUniqueID());
53  }
[7954]54
55  /* make sure loadClassId is first synced var because this is read by networkStream */
56  assert( syncVarList.size() == 0 );
57  mLeafClassId = this->registerVarId( new SynchronizeableInt( (int*)&this->getLeafClassID(), (int*)&this->getLeafClassID(), "leafClassId" ) );
[9406]58
[7954]59  this->registerVar( new SynchronizeableInt( &this->owner, &this->owner, "owner" ) );
60  this->registerVar( new SynchronizeableString( &this->objectName, &this->objectName, "objectName" ) );
[5997]61}
62
[5523]63
[5996]64
[5547]65/**
[5807]66 *  default destructor deletes all unneded stuff
[5547]67 */
68Synchronizeable::~Synchronizeable()
[6139]69{
70  if ( this->networkStream )
[9110]71  {
[6139]72    this->networkStream->disconnectSynchronizeable(*this);
[9406]73
[9494]74    // remove the message manager only by the server
75    if ( (SharedNetworkData::getInstance()->isMasterServer() )
[9406]76           && this->beSynchronized() && this->getUniqueID() > 0 && !this->isA( CL_MESSAGE_MANAGER ) )
[9110]77      NetworkGameManager::getInstance()->removeSynchronizeable( this->getUniqueID() );
78  }
[9406]79
[8623]80  for ( SyncVarList::iterator it = syncVarList.begin(); it != syncVarList.end(); it++ )
81  {
82    delete *it;
83  }
84  syncVarList.clear();
[9406]85
[8623]86  for ( UserStateHistory::iterator it = recvStates.begin(); it != recvStates.end(); it++ )
87  {
88    for ( StateHistory::iterator it2 = it->begin(); it2 != it->end(); it2++ )
89    {
90      if ( (*it2)->data )
91      {
92        delete [] (*it2)->data;
93        (*it2)->data = NULL;
94      }
95      delete *it2;
96    }
97
98  }
[9406]99
[8623]100  for ( UserStateHistory::iterator it = sentStates.begin(); it != sentStates.end(); it++ )
101  {
102    for ( StateHistory::iterator it2 = it->begin(); it2 != it->end(); it2++ )
103    {
104      if ( (*it2)->data )
105      {
106        delete [] (*it2)->data;
107        (*it2)->data = NULL;
108      }
109      delete *it2;
110    }
111  }
[6139]112}
[5523]113
114
[6695]115
[5547]116/**
[9406]117 * creates a diff image from two states
118 * @param userId: the userid of the user where the image will be sent to
119 * @param data: the binary data array to write to
120 * @param maxLength: maximal length of the data written (length of available space in the array)
121 * @param stateId: the state id that this diff will represent
122 * @param priorityTH: the priority threshold: all syncs below this threshold won't be synchronized
123 *
124 * @todo check for permissions
[5547]125 */
[7954]126int Synchronizeable::getStateDiff( int userId, byte* data, int maxLength, int stateId, int fromStateId, int priorityTH )
127{
128  //make sure this user has his history
129  if ( sentStates.size() <= userId )
130    sentStates.resize( userId+1 );
[5547]131
[7954]132  //calculate needed memory
133  int neededSize = 0;
[5997]134
[7954]135  for ( SyncVarList::iterator it = syncVarList.begin(); it != syncVarList.end(); it++ )
[8147]136  {
137    //PRINTF(0)("SIZE = %d %s\n", (*it)->getSize(), (*it)->getName().c_str());
[7954]138    neededSize += (*it)->getSize();
[8147]139  }
[5997]140
[7954]141  if ( !( neededSize <= maxLength ) )
142  {
143    PRINTF(0)( "%d > %d\n", neededSize, maxLength );
144    assert(false);
145  }
146
147  //remove older states from history than fromStateId
148  StateHistory::iterator it = sentStates[userId].begin();
149
150  while ( it != sentStates[userId].end() && (*it)->stateId < fromStateId )
151    it++;
152
153  if ( it != sentStates[userId].begin() )
154  {
155    for ( StateHistory::iterator it2 = sentStates[userId].begin(); it2 != it; it2++ )
156    {
157      if ( (*it2)->data != NULL )
158      {
159        delete [] (*it2)->data;
160        (*it2)->data = NULL;
161      }
[9406]162
[8623]163      delete *it2;
[7954]164    }
165    sentStates[userId].erase( sentStates[userId].begin(), it );
166  }
167
168  //find state to create diff from
169  StateHistoryEntry * stateFrom = NULL;
170
171  it = sentStates[userId].begin();
172  while ( it != sentStates[userId].end() && (*it)->stateId != fromStateId )
173    it++;
174
175  if ( it == sentStates[userId].end() )
176  {
177    StateHistoryEntry * initialEntry = new StateHistoryEntry();
178
179    initialEntry->stateId = fromStateId;
180    initialEntry->dataLength = 0;
181    initialEntry->data = NULL;
182
183    stateFrom = initialEntry;
[9406]184
[8623]185    sentStates[userId].push_back( stateFrom );
[7954]186  }
187  else
188    stateFrom = (*it);
189
[8623]190  StateHistoryEntry * stateTo = new StateHistoryEntry;
[7954]191
[8623]192  sentStates[userId].push_back( stateTo );
[9406]193
[7954]194  stateTo->stateId = stateId;
195  stateTo->dataLength = neededSize;
196  stateTo->data = new byte[ neededSize ];
197
198  std::list<int>::iterator sizeIter = stateFrom->sizeList.begin();
199
200  int i = 0;
201  int n;
[9406]202
203  bool hasPermission = false;
[8623]204  bool sizeChanged = false;
[7954]205
206  // now do the actual synchronization: kick all variables to write into a common buffer
207  for ( SyncVarList::iterator it = syncVarList.begin(); it != syncVarList.end(); it++ )
208  {
[9406]209    // DATA PERMISSIONS
210    // check if this synchronizeable has the permissions to write the data
211
212    // first check MASTER_SERVER permissions
213    if( SharedNetworkData::getInstance()->isMasterServer() && (*it)->checkPermission( PERMISSION_MASTER_SERVER ))
214      hasPermission = true;
215    // now check PROXY_SERVER permissions
[9494]216    else if( SharedNetworkData::getInstance()->isProxyServerActive() && (*it)->checkPermission( PERMISSION_PROXY_SERVER ))
[9406]217      hasPermission = true;
218    // now check OWNER permissions
219    else if( this->owner == SharedNetworkData::getInstance()->getHostID() && (*it)->checkPermission( PERMISSION_OWNER ))
220      hasPermission = true;
221    // now check ALL permissions
222    else if( (*it)->checkPermission( PERMISSION_ALL ))
223      hasPermission = true;
224    // SPECIAL: get write permissions if i am master server and i am able to overwrite the client stuff
[9494]225#warning this could probably override also clients that are connected to another proxy: the master server overwrites it
[9406]226    else if( SharedNetworkData::getInstance()->isMasterServer() && this->owner != userId && (*it)->checkPermission( PERMISSION_OWNER ))
227      hasPermission = true;
[9588]228    // SPECIAL: permission to forward syncs from the master server to the clients
[9494]229    else if( SharedNetworkData::getInstance()->isProxyServerActive() && this->networkStream->isUserClient(userId)
[9581]230             && (*it)->checkPermission( PERMISSION_MASTER_SERVER) )
231      hasPermission = true;
[9588]232    // SPECIAL: permission to forward to the master server
233    else if( SharedNetworkData::getInstance()->isProxyServerActive() && this->networkStream->isUserMasterServer(userId)
234             && (*it)->checkPermission( PERMISSION_MASTER_SERVER) )
235      hasPermission = true;
[9581]236    else if( SharedNetworkData::getInstance()->isProxyServerActive() && this->networkStream->isUserClient(userId)
[9494]237             && this->owner != userId && (*it)->checkPermission( PERMISSION_OWNER ) )
[9406]238      hasPermission = true;
239    else
240      hasPermission = false;
241
242
243    if ( sizeIter == stateFrom->sizeList.end() || *sizeIter != (*it)->getSize() )
[8623]244      sizeChanged = true;
[9406]245
[8623]246    if ( ( hasPermission && (*it)->getPriority() >= priorityTH ) || sizeChanged )
[7954]247    {
248      n = (*it)->writeToBuf( stateTo->data+i, stateTo->dataLength - i );
[9579]249      //NETPRINTF(0)("getvar %s %d\n", (*it)->getName().c_str(), n);
[9406]250      //PRINTF(0)("getvar %s %d\n", (*it)->getName().c_str(), n);
[7954]251      stateTo->sizeList.push_back( n );
[9406]252      // this is only for very hardcore debug sessions
253      // (*it)->debug();
[7954]254      i += n;
255    }
256    else
257    {
258      for ( int j = 0; j<(*sizeIter); j++ )
259      {
260        assert( i < stateFrom->dataLength );
261        stateTo->data[i] = stateFrom->data[i];
262        i++;
263      }
264      //NETPRINTF(0)("getvar %s %d\n", (*it)->getName().c_str(), *sizeIter);
265      stateTo->sizeList.push_back( (*sizeIter) );
266    }
267
268    if ( sizeIter != stateFrom->sizeList.end() )
269      sizeIter++;
270  }
271
[8147]272  if ( i != neededSize )
273  {
[9406]274    PRINTF(0)("strange error: (%s) %d != %d\n", this->getClassCName(), i, neededSize);
[8147]275    assert(false);
276  }
[7954]277
278  //write diff to data
279  for ( i = 0; i<neededSize; i++ )
280  {
281    if ( i < stateFrom->dataLength )
282      data[i] = stateTo->data[i] - stateFrom->data[i];
283    else
284      data[i] = stateTo->data[i];
285  }
286
287  return neededSize;
288}
289
[5997]290/**
[7954]291 * sets a new state out of a diff created on another host
292 * @param userId hostId of user who send me that diff
293 * @param data pointer to diff
294 * @param length length of diff
295 * @param stateId id of current state
296 * @param fromStateId id of the base state id
297 * @return number bytes read
[9406]298 *
[7954]299 * @todo check for permissions
[5997]300 */
[7954]301int Synchronizeable::setStateDiff( int userId, byte* data, int length, int stateId, int fromStateId )
[5997]302{
[7954]303  //make sure this user has his history
304  if ( recvStates.size() <= userId )
305    recvStates.resize( userId+1 );
306
307  //create new state
308  StateHistoryEntry * stateTo = new StateHistoryEntry();
309  stateTo->stateId = stateId;
310  stateTo->dataLength = length;
311  stateTo->data = new byte[ length ];
312
313
314  //find state to apply diff to
315  StateHistoryEntry * stateFrom = NULL;
316
[9406]317  // search the state from wich the diff is made of
[7954]318  StateHistory::iterator it = recvStates[userId].begin();
319  while ( it != recvStates[userId].end() && (*it)->stateId != fromStateId )
320    it++;
321
[9406]322  // if this is the first state to receive
[7954]323  if ( it == recvStates[userId].end() )
324  {
325    StateHistoryEntry * initialEntry = new StateHistoryEntry();
326
327    initialEntry->stateId = fromStateId;
328    initialEntry->dataLength = 0;
329    initialEntry->data = NULL;
330
331    stateFrom = initialEntry;
[9406]332
[8623]333    recvStates[userId].push_back( stateFrom );
[7954]334  }
[5997]335  else
[7954]336    stateFrom = (*it);
[9406]337
338
339  // apply diff
[7954]340  for ( int i = 0; i<length; i++ )
341  {
342    if ( i < stateFrom->dataLength )
343      stateTo->data[i] = stateFrom->data[i] + data[i];
344    else
345      stateTo->data[i] = data[i];
346  }
[9406]347
[7954]348  //add state to state history
349  recvStates[userId].push_back( stateTo );
[9406]350
[7954]351  int i = 0;
352  int n = 0;
353  std::list<int> changes;
[9406]354  bool hasPermission = false;
355
356  // extract the new state for every client
[7954]357  for ( SyncVarList::iterator it = syncVarList.begin(); it != syncVarList.end(); it++ )
358  {
[9406]359    // DATA PERMISSIONS
360    // check if this synchronizeable has the permissions to write the data
361
362    // first check MASTER_SERVER permissions
363    if(  this->networkStream->isUserMasterServer( userId ) && (*it)->checkPermission( PERMISSION_MASTER_SERVER ))
364      hasPermission = true;
365    // now check PROXY_SERVER permissions
[9494]366    else if( this->networkStream->isUserProxyServerActive( userId )  && (*it)->checkPermission( PERMISSION_MASTER_SERVER )
367             && SharedNetworkData::getInstance()->isClient())
[9406]368      hasPermission = true;
369    // now check OWNER permissions
370    else if( this->owner == userId && (*it)->checkPermission( PERMISSION_OWNER ))
371      hasPermission = true;
372    // now check ALL permissions
373    else if( (*it)->checkPermission( PERMISSION_ALL ))
374      hasPermission = true;
375    // SPECIAL: get write permissions if im sending to a master server that does not own this sync
376    else if( this->networkStream->isUserMasterServer( userId ) && this->owner != SharedNetworkData::getInstance()->getHostID() && (*it)->checkPermission( PERMISSION_OWNER ))
377      hasPermission = true;
378    // SPECIAL: get write permissions if im sending to a proxy server that does not own this sync
[9494]379    else if( this->networkStream->isUserProxyServerActive( userId ) && SharedNetworkData::getInstance()->isClient()
380              && this->owner != SharedNetworkData::getInstance()->getHostID() && (*it)->checkPermission( PERMISSION_OWNER ))
[9406]381      hasPermission = true;
382    else
383      hasPermission = false;
384
385
386
387    // if it has the permission to write do it
388    if( hasPermission)
[7954]389    {
390      n = (*it)->readFromBuf( stateTo->data + i, stateTo->dataLength - i );
391      i += n;
[9406]392      //NETPRINTF(0)("%s::setvar %s %d\n", getClassCName(), (*it)->getName().c_str(), n);
[9579]393      //PRINTF(0)("%s::setvar %s %d\n", getClassCName(), (*it)->getName().c_str(), n);
[7954]394      //(*it)->debug();
395      if ( (*it)->getHasChanged() )
396      {
397        changes.push_back( (*it)->getVarId() );
398      }
399    }
400    else
401    {
[9494]402//       PRINTF(0)("DONT SET VAR BECAUSE OF PERMISSION: %s perm: %d %d %d - %d %d %d\n", (*it)->getName().c_str(), (*it)->checkPermission( PERMISSION_MASTER_SERVER ), (*it)->checkPermission( PERMISSION_OWNER ), (*it)->checkPermission( PERMISSION_ALL ), networkStream->isUserMasterServer( userId ), this->owner, userId );
[7954]403      n = (*it)->getSizeFromBuf( stateTo->data + i, stateTo->dataLength - i );
[9406]404      //NETPRINTF(0)("%s::setvar %s %d\n", getClassCName(), (*it)->getName().c_str(), n);
[7954]405      //(*it)->debug();
406      i += n;
407    }
408  }
409
410  this->varChangeHandler( changes );
[9406]411
[7954]412  return i;
[5997]413}
414
[7954]415 /**
416 * override this function to be notified on change
417 * of your registred variables.
418 * @param id id's which have changed
419 */
420void Synchronizeable::varChangeHandler( std::list<int> & id )
421{
422}
[6695]423
[5997]424/**
[7954]425 * registers a varable to be synchronized over network
426 * @param var see src/lib/network/synchronizeable_var/ for available classes
[5997]427 */
[7954]428void Synchronizeable::registerVar( SynchronizeableVar * var )
[5997]429{
[7954]430  syncVarList.push_back( var );
[5997]431}
432
433/**
[7954]434 * registers a varable to be synchronized over network
435 * return value is passed to varChangeHandler on change
436 * @param var see src/lib/network/synchronizeable_var/ for available classes
437 * @return handle passed to varChangeHandler on changes
[5997]438 */
[7954]439int Synchronizeable::registerVarId( SynchronizeableVar * var )
[5997]440{
[7954]441  syncVarList.push_back( var );
442  var->setWatched( true );
443  var->setVarId( syncVarList.size()-1 );
444  return syncVarList.size()-1;
[5997]445}
446
447/**
[7954]448 * removed user's states from memory
449 * @param userId user to clean
[5997]450 */
[7954]451void Synchronizeable::cleanUpUser( int userId )
[5997]452{
[8228]453  if ( recvStates.size() > userId )
[7954]454  {
[8228]455    for ( std::list<StateHistoryEntry*>::iterator it = recvStates[userId].begin(); it != recvStates[userId].end(); it++ )
[7954]456    {
[8228]457      if ( (*it)->data )
[8623]458      {
[8228]459        delete [] (*it)->data;
[8623]460        (*it)->data = NULL;
461      }
[9406]462
[8228]463      delete *it;
[7954]464    }
[8228]465    recvStates[userId].clear();
[7954]466  }
[9406]467
[8228]468  if ( sentStates.size() > userId )
[7954]469  {
[9406]470
[8228]471    for ( std::list<StateHistoryEntry*>::iterator it = sentStates[userId].begin(); it != sentStates[userId].end(); it++ )
[7954]472    {
[8228]473      if ( (*it)->data )
[8623]474      {
[8228]475        delete [] (*it)->data;
[8623]476        (*it)->data = NULL;
477      }
[9406]478
[8228]479      delete *it;
[7954]480    }
[8228]481    sentStates[userId].clear();
[7954]482  }
[5997]483}
[6139]484
[6341]485/**
[7954]486 * this function is called after recieving a state.
[9406]487 * @param userId
488 * @param stateId
489 * @param fromStateId
[6341]490 */
[7954]491void Synchronizeable::handleRecvState( int userId, int stateId, int fromStateId )
[6341]492{
[7954]493   //make sure this user has his history
494  if ( recvStates.size() <= userId )
495    recvStates.resize( userId+1 );
[9406]496
[7954]497  //remove old states
498  StateHistory::iterator it = recvStates[userId].begin();
499
500#if 0
501  while ( it != recvStates[userId].end() && (*it)->stateId < fromStateId )
502    it++;
503
504  if ( it != recvStates[userId].begin() )
505  {
506    for ( StateHistory::iterator it2 = recvStates[userId].begin(); it2 != it; it2++ )
507    {
508      if ( (*it2)->data != NULL )
509      {
510        delete [] (*it2)->data;
511        (*it2)->data = NULL;
512      }
513    }
514    recvStates[userId].erase( recvStates[userId].begin(), it );
515  }
516#endif
517
518  for ( it = recvStates[userId].begin(); it != recvStates[userId].end();  )
519  {
520    if ( (*it)->stateId < fromStateId )
521    {
522      StateHistory::iterator delIt = it;
523      it ++;
[9406]524
[7954]525      if ( (*delIt)->data )
[8623]526      {
[7954]527        delete [] (*delIt)->data;
[8623]528        (*delIt)->data = NULL;
529      }
530      delete *delIt;
[7954]531      recvStates[userId].erase( delIt );
[9406]532
[7954]533      continue;
534    }
535    it++;
536  }
[9406]537
[7954]538  StateHistory::iterator fromState = recvStates[userId].end();
539  StateHistory::iterator toState = recvStates[userId].end();
[9406]540
[7954]541  for ( it = recvStates[userId].begin(); it != recvStates[userId].end(); it++ )
542  {
543    if ( (*it)->stateId == stateId )
544      toState = it;
545    if ( (*it)->stateId == fromStateId )
546      fromState = it;
[9406]547
[7954]548    if ( fromState != recvStates[userId].end() && toState != recvStates[userId].end() )
549      break;
550  }
[9406]551
[7954]552  // setStateDiff was not called and i know fromStateId
553  if ( fromState != recvStates[userId].end() && toState == recvStates[userId].end() )
554  {
555    StateHistoryEntry * entry = new StateHistoryEntry;
[9406]556
[7954]557    entry->dataLength = (*fromState)->dataLength;
558    if ( entry->dataLength > 0 )
559    {
560      entry->data = new byte[entry->dataLength];
[9406]561
[7954]562      assert( (*fromState)->data );
563      memcpy( entry->data, (*fromState)->data, entry->dataLength );
564    }
565    else
566      entry->data = NULL;
[9406]567
[7954]568    entry->sizeList = (*fromState)->sizeList;
569    entry->stateId = stateId;
[9406]570
[7954]571    recvStates[userId].push_back(entry);
572  }
[6341]573}
[6139]574
[6341]575/**
[7954]576 * this function is called after sending a state
[9406]577 * @param userId
578 * @param stateId
579 * @param fromStateId
[6341]580 */
[7954]581void Synchronizeable::handleSentState( int userId, int stateId, int fromStateId )
[6341]582{
[7954]583   //make sure this user has his history
584  if ( sentStates.size() <= userId )
585    sentStates.resize( userId+1 );
586
587   //remove old states
588  StateHistory::iterator it = sentStates[userId].begin();
589
590  for ( it = sentStates[userId].begin(); it != sentStates[userId].end();  )
591  {
592    if ( (*it)->stateId < fromStateId )
593    {
594      StateHistory::iterator delIt = it;
595      it ++;
[9406]596
[7954]597      if ( (*delIt)->data )
[8623]598      {
[7954]599        delete [] (*delIt)->data;
[8623]600        (*delIt)->data = NULL;
601      }
602      delete *delIt;
[7954]603      sentStates[userId].erase( delIt );
[9406]604
[7954]605      continue;
606    }
607    it++;
608  }
609
[9406]610
[7954]611  StateHistory::iterator fromState = sentStates[userId].end();
612  StateHistory::iterator toState = sentStates[userId].end();
[9406]613
[7954]614  for ( it = sentStates[userId].begin(); it != sentStates[userId].end(); it++ )
615  {
616    if ( (*it)->stateId == stateId )
617      toState = it;
618    if ( (*it)->stateId == fromStateId )
619      fromState = it;
[9406]620
[7954]621    if ( fromState != sentStates[userId].end() && toState != sentStates[userId].end() )
622      break;
623  }
624
[9406]625
[7954]626  // getStateDiff was not called and i know fromStateId
627  if ( fromState != sentStates[userId].end() && toState == sentStates[userId].end() )
628  {
629    StateHistoryEntry * entry = new StateHistoryEntry;
[9406]630
[7954]631    entry->dataLength = (*fromState)->dataLength;
632    if ( entry->dataLength > 0 )
633    {
634      entry->data = new byte[entry->dataLength];
[9406]635
[7954]636      assert( (*fromState)->data );
637      memcpy( entry->data, (*fromState)->data, entry->dataLength );
638    }
639    else
640      entry->data = NULL;
[9406]641
[7954]642    entry->sizeList = (*fromState)->sizeList;
643    entry->stateId = stateId;
[9406]644
[7954]645    sentStates[userId].push_back(entry);
646  }
[9406]647
[6341]648}
[6139]649
[6341]650
651
Note: See TracBrowser for help on using the repository browser.