source: abuse/trunk/src/imlib/specs.cpp @ 544

Last change on this file since 544 was 544, checked in by Sam Hocevar, 10 years ago

imlib: get rid of fast path loading, it was no longer used. We also
delete the old cache file we put in the user's directory.

File size: 18.3 KB
Line 
1/*
2 *  Abuse - dark 2D side-scrolling platform game
3 *  Copyright (c) 1995 Crack dot Com
4 *  Copyright (c) 2005-2011 Sam Hocevar <sam@hocevar.net>
5 *
6 *  This software was released into the Public Domain. As with most public
7 *  domain software, no warranty is made or implied by Crack dot Com or
8 *  Jonathan Clark.
9 */
10
11#include "config.h"
12
13#include <stdio.h>
14#include <stdlib.h>
15#include <stdint.h>
16#include <ctype.h>
17#include <fcntl.h>
18#include <math.h>
19#include <unistd.h>
20#include <sys/types.h>
21#include <sys/stat.h>
22
23#include "common.h"
24
25#include "image.h"
26#include "palette.h"
27#include "specs.h"
28#include "dprint.h"
29
30char const *spec_types[] =
31{
32    "Invalid type", // 0
33    "Color table",  // 1
34    "Palette",      // 2
35    "Invalid Type", // 3
36    "Image",        // 4
37    "Fore Tile",
38    "Back Tile",
39    "Character",
40    "8 Morph",
41    "16 Morph",
42    "Grue objs",
43    "Extern WAV",
44    "DMX MUS",
45    "Patched morph",
46    "Normal file",
47    "Compress1 file",
48    "Vector Image",
49    "Light list",
50    "Grue fgmap",
51    "Grue bgmap",
52    "Data array",
53    "Character2",
54    "Particle",
55    "Extern lcache"
56};
57
58
59int total_files_open=0;
60char spec_main_file[100];
61
62static char *spec_prefix=NULL;
63static char *save_spec_prefix=NULL;
64
65static jFILE spec_main_jfile((FILE*)0);
66static int spec_main_fd = -1;
67static long spec_main_offset = -1;
68static spec_directory spec_main_sd;
69
70void set_filename_prefix(char const *prefix)
71{
72    if( spec_prefix )
73    {
74        free( spec_prefix );
75    }
76
77    if( prefix )
78    {
79        spec_prefix = strcpy( (char *)malloc( strlen( prefix ) + 2 ), prefix );
80        int len = strlen( prefix );
81        if( prefix[len - 1] != '\\' && prefix[len - 1] != '/')
82        {
83            spec_prefix[len] = '/';
84            spec_prefix[len + 1] = 0;
85        }
86    }
87    else
88    {
89        spec_prefix = NULL;
90    }
91}
92
93char *get_filename_prefix()
94{
95    return spec_prefix;
96}
97
98
99void set_save_filename_prefix(char const *save_prefix)
100{
101    if( save_spec_prefix )
102    {
103        free( save_spec_prefix );
104    }
105
106    if( save_prefix )
107    {
108        int len = strlen( save_prefix );
109        save_spec_prefix = (char *)malloc( len + 1 );
110        strcpy( save_spec_prefix, save_prefix );
111/* AK - Commented this out as it may cause problems
112        if( save_prefix[len - 1] != '\\' && save_prefix[len - 1] != '/' )
113        {
114            save_spec_prefix[len] = '/';
115            save_spec_prefix[len + 1] = '\0';
116        } */
117    }
118    else
119    {
120        save_spec_prefix = NULL;
121    }
122}
123
124char *get_save_filename_prefix()
125{
126  return save_spec_prefix;
127}
128
129int search_order=SPEC_SEARCH_OUTSIDE_INSIDE;
130
131static void (*no_space_handle_fun)()=NULL;
132
133void set_no_space_handler(void (*handle_fun)())
134{
135  no_space_handle_fun=handle_fun;
136}
137
138
139bFILE::bFILE()
140{
141  rbuf_size=8192;
142  rbuf=(unsigned char *)malloc(rbuf_size);
143  rbuf_start=rbuf_end=0;
144
145  wbuf_size=8192;
146  wbuf=(unsigned char *)malloc(wbuf_size);
147  wbuf_end=0;
148}
149
150bFILE::~bFILE()
151{
152  if (rbuf) free(rbuf);
153  flush_writes();
154  if (wbuf) free(wbuf);
155}
156
157int bFILE::flush_writes()
158{
159  if (wbuf_end!=0)
160  {
161    unsigned long ret=unbuffered_write(wbuf,wbuf_end);
162    if (ret!=wbuf_end && no_space_handle_fun)
163      no_space_handle_fun();
164
165    wbuf_end=0;
166    return ret;
167  }
168  return 0;
169}
170
171int bFILE::read(void *buf, size_t count)       // returns number of bytes read, calls unbuffer_read
172{
173  if (!allow_read_buffering())
174    return unbuffered_read(buf,count);
175
176  int total_read=0,error=0;
177  if (!count) return 0;
178  while (count && !error)
179  {
180    if (rbuf_start<rbuf_end)
181    {
182      unsigned int avail_size=rbuf_end-rbuf_start;
183      int copy_size=avail_size>count ? count : avail_size;
184      memcpy(buf,rbuf+rbuf_start,copy_size);
185      buf=(void *)(((unsigned char *)buf)+copy_size);
186      rbuf_start+=copy_size;
187      if (rbuf_start>=rbuf_end)
188      {
189                if (rbuf_end!=rbuf_size)  // buffer wasn't full before so there is no way we can complete read
190                  error=1;
191                rbuf_start=rbuf_end=0;
192      }
193      total_read+=copy_size;
194      count-=copy_size;
195    } else
196    {
197      rbuf_end=unbuffered_read(rbuf,rbuf_size);
198      if (rbuf_end==0) error=1;
199      rbuf_start=0;
200    }
201  }
202  return total_read;
203}
204
205
206int bFILE::write(void const *buf, size_t count)      // returns number of bytes written
207{
208  if (allow_write_buffering())
209  {
210    int total_written=0;
211    while (count)
212    {
213      int copy_size=wbuf_end+count<=wbuf_size ? count :  wbuf_size-wbuf_end;
214      memcpy(wbuf+wbuf_end,buf,copy_size);
215      wbuf_end+=copy_size;
216      count-=copy_size;
217      buf=(void *)(((char *)buf)+copy_size);
218      if (wbuf_end==wbuf_size)
219        if ((unsigned int)flush_writes()!=wbuf_size)
220      return total_written;
221
222      total_written+=copy_size;
223    }
224    return total_written;
225  } else
226  {
227    unsigned long ret=unbuffered_write(buf,count);
228    if (ret!=count && no_space_handle_fun)
229      no_space_handle_fun();
230  }
231  return 0;
232}
233
234int bFILE::seek(long offset, int whence) // whence=SEEK_SET, SEEK_CUR, SEEK_END, ret=0=success
235{
236//    rbuf_start=rbuf_end=0;
237//    unbuffered_seek(offset,SEEK_SET);
238
239  long realpos=unbuffered_tell();
240  long curpos=realpos-rbuf_end+rbuf_start;
241  if (whence==SEEK_CUR) offset+=curpos;
242  else if (whence==SEEK_END) offset=file_size()-offset;
243
244  if (offset<realpos-(long)rbuf_end || offset>=realpos)
245  {
246    rbuf_start=rbuf_end=0;
247    unbuffered_seek(offset,SEEK_SET);
248  } else
249    rbuf_start=rbuf_end-(realpos-offset);
250  return 1;
251}
252
253int bFILE::tell()
254{
255  return unbuffered_tell()-rbuf_end+rbuf_start+
256         wbuf_end;    // if this a write file, add on how much we've written
257}
258
259int bFILE::allow_read_buffering() { return 1; }
260int bFILE::allow_write_buffering() { return 1; }
261
262void set_spec_main_file(char const *filename, int Search_order)
263{
264  dprintf("Specs : main file set to %s\n",filename);
265  strcpy(spec_main_file,filename);
266  search_order=Search_order;
267
268#if (defined(__APPLE__) && !defined(__MACH__))
269  spec_main_jfile.open_external(filename,"rb",O_BINARY|O_RDONLY);
270#else
271  spec_main_jfile.open_external(filename,"rb",O_RDONLY);
272#endif
273  spec_main_fd = spec_main_jfile.get_fd();
274  if (spec_main_fd==-1)
275    return;
276  spec_main_sd.startup(&spec_main_jfile);
277}
278
279jFILE::jFILE(FILE *file_pointer)                       // assumes fp is at begining of file
280{
281  access=0;
282  fd=-1;
283  file_length=0;
284  start_offset=0;
285  flags=JFILE_CLONED;
286}
287
288void jFILE::open_external(char const *filename, char const *mode, int flags)
289{
290  int skip_size=0;
291  char tmp_name[200];
292  if (spec_prefix && filename[0] != '/')
293    sprintf(tmp_name,"%s%s",spec_prefix,filename);
294  else strcpy(tmp_name,filename);
295
296//  int old_mask=umask(S_IRWXU | S_IRWXG | S_IRWXO);
297  if (flags&O_WRONLY)
298  {
299    if ((flags&O_APPEND)==0)
300    {
301      skip_size=1;
302      //int errval = unlink(tmp_name);
303    }
304
305    flags-=O_WRONLY;
306    flags|=O_CREAT|O_RDWR;
307
308    fd=open(tmp_name,flags,S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
309  } else
310    fd=open(tmp_name,flags);
311
312//  umask(old_mask);
313  if (fd>=0 && !skip_size)
314  {
315    file_length=lseek(fd,0,SEEK_END);
316    if ((flags&O_APPEND)==0)
317      lseek(fd,0,SEEK_SET);
318    else
319        current_offset = file_length;
320    start_offset=0;
321  } else
322  {
323    file_length=0;
324    start_offset=0;
325  }
326}
327
328
329class null_file : public bFILE     // this file type will use virtual opens inside of a spe
330{
331  public :
332  virtual int open_failure() { return 1; }
333  virtual int unbuffered_read(void *buf, size_t count)   { return 0; }
334  virtual int unbuffered_write(void const *buf, size_t count)  { return 0; }
335  virtual int unbuffered_seek(long offset, int whence)   { return 0; }
336
337  virtual int unbuffered_tell() { return 0; }
338  virtual int file_size() { return 0; }
339  virtual ~null_file() { ; }
340} ;
341
342
343static bFILE *(*open_file_fun)(char const *,char const *)=NULL;
344int (*verify_file_fun)(char const *,char const *)=NULL;
345
346void set_file_opener(bFILE *(*open_fun)(char const *, char const *))
347{
348  open_file_fun=open_fun;
349}
350
351bFILE *open_file(char const *filename, char const *mode)
352{
353  if (!verify_file_fun || verify_file_fun(filename,mode))
354  {
355    if (open_file_fun)
356      return open_file_fun(filename,mode);
357    else return new jFILE(filename,mode);
358  } else return new null_file;
359}
360
361void jFILE::open_internal(char const *filename, char const *mode, int flags)
362{
363  int wr=0;
364  for (char const *s=mode; *s; s++)
365    if (toupper(*s)=='A' || toupper(*s)=='W')
366      wr=1;
367
368  if (wr)
369    fd=-1;                 // only allow extern file openings for writing
370  else
371  {
372       fd = spec_main_fd;
373    if (fd>=0)                    // if we were able to open the main file, see if it's in there
374    {
375      start_offset=0;
376      spec_entry *se=spec_main_sd.find(filename);
377      if (se)
378      {
379    start_offset=se->offset;
380    current_offset = 0;
381    file_length=se->size;
382    rbuf_start=rbuf_end=0;
383      } else
384      {
385    close(fd);
386    fd=-1;
387      }
388    }
389  }
390}
391
392jFILE::jFILE(char const *filename, char const *access_string)      // same as fopen parameters
393{
394 flags=access=0;
395 char const *s=access_string;
396  for (; *s; s++)
397    if (toupper(*s)=='R') access=O_RDONLY;
398
399  for (s=access_string; *s; s++)
400    if (toupper(*s)=='W')
401    {
402      if (access)
403        access=O_RDWR;
404      else access=O_WRONLY;
405    }
406
407  for (s=access_string; *s; s++)
408    if (toupper(*s)=='A')
409      access|=O_APPEND|O_WRONLY;
410
411  file_length=start_offset=-1;
412  current_offset = 0;
413
414  fd=-1;
415  if (search_order==SPEC_SEARCH_OUTSIDE_INSIDE)
416    open_external(filename,access_string,access);
417
418  if (fd<0)
419    open_internal(filename,access_string,access);
420
421  if (fd<0 && search_order==SPEC_SEARCH_INSIDE_OUTSIDE)
422    open_external(filename,access_string,access);
423
424  total_files_open++;
425}
426
427jFILE::~jFILE()
428{
429  flush_writes();
430  if (fd>=0 && !(flags&JFILE_CLONED))
431  {
432    total_files_open--;
433    if (fd != spec_main_fd)
434        close(fd);
435  }
436}
437
438int jFILE::unbuffered_tell()
439{
440//    int ret = ::lseek(fd,0,SEEK_CUR) - start_offset;
441//    if (ret != current_offset)
442//        fprintf(stderr,"Bad tell %d\n",current_offset);
443    return current_offset;
444}
445
446int jFILE::unbuffered_read(void *buf, size_t count)
447{
448    unsigned long len;
449
450    if (fd == spec_main_fd)
451    {
452        if (current_offset+start_offset != spec_main_offset)
453            spec_main_offset = lseek(fd, start_offset+current_offset, SEEK_SET);
454
455        len = ::read(fd,(char*)buf,count);
456        spec_main_offset += len;
457    }
458    else
459    {
460      len = ::read(fd,(char*)buf,count);
461    }
462    current_offset += len;
463    return len;
464}
465
466int jFILE::unbuffered_write(void const *buf, size_t count)
467{
468  long ret = ::write(fd,(char*)buf,count);
469    current_offset += ret;
470    return ret;
471}
472
473int jFILE::unbuffered_seek(long offset, int whence) // whence=SEEK_SET, SEEK_CUR, SEEK_END, ret=0=success
474{
475  long ret;
476
477  switch (whence)
478  {
479    case SEEK_SET :
480    { ret = lseek(fd,start_offset+offset,SEEK_SET); } break;
481    case SEEK_END :
482    { ret = lseek(fd,start_offset+file_length-offset,SEEK_SET); } break;
483    case SEEK_CUR :
484    { ret = lseek(fd,offset,SEEK_CUR); } break;
485    default:
486        ret = -1;
487        break;
488  }
489  if (ret>=0)
490  {
491    current_offset = ret - start_offset;
492    if (spec_main_fd == fd)
493      spec_main_offset = ret;
494    return ret;
495  }
496  else
497    return -1;  // if a bad whence, then failure
498}
499
500
501uint8_t bFILE::read_uint8()
502{ uint8_t x;
503  read(&x,1);
504  return x;
505}
506
507uint16_t bFILE::read_uint16()
508{
509  uint16_t x;
510  read(&x,2);
511  return lstl(x);
512}
513
514
515uint32_t bFILE::read_uint32()
516{
517  uint32_t x;
518  read(&x,4);
519  return lltl(x);
520}
521
522void bFILE::write_uint8(uint8_t x)
523{
524  write(&x,1);
525}
526
527void bFILE::write_uint16(uint16_t x)
528{
529  x=lstl(x);
530  write(&x,2);
531}
532
533
534void bFILE::write_uint32(uint32_t x)
535{
536  x=lltl(x);
537  write(&x,4);
538}
539
540void bFILE::write_double(double x)
541{
542  double a;
543  write_uint32((long)(modf(x,&a)*(double)(1<<31)));
544  write_uint32((long)a);
545}
546
547double bFILE::read_double()
548{
549  long a,b;
550  a=read_uint32();
551  b=read_uint32();
552  return (double)b+a/(double)(1<<31);
553}
554
555spec_directory::~spec_directory()
556{
557
558  if (total)
559  {
560    free(data);
561    free(entries);
562  }
563}
564
565void spec_entry::print()
566{
567  printf("%15s%25s%8ld%8ld\n",spec_types[type],name,size,offset);
568}
569
570void spec_directory::calc_offsets()
571{
572  spec_entry **e;
573  int i;
574  long o=SPEC_SIG_SIZE+2;
575  if (total)
576  {
577    for (i=0,e=entries; i<total; i++,e++)          // calculate the size of directory info
578    {
579      o+=1+1+strlen((*e)->name)+1 +1 +8;
580    }
581
582    for (i=0,e=entries; i<total; i++,e++)          // calculate offset for each entry
583    {
584      (*e)->offset=o;
585      o+=(*e)->size;
586    }
587  }
588}
589
590spec_entry *spec_directory::find(char const *name, int type)
591{
592  int i;
593  spec_entry **e;
594  for (i=0,e=entries; i<total; i++,e++)
595    if (!strcmp((*e)->name,name) && (*e)->type==type)
596      return (*e);
597  return NULL;
598}
599
600spec_entry *spec_directory::find(char const *name)
601{
602  int i;
603  spec_entry **e;
604  for (i=0,e=entries; i<total; i++,e++)
605    if (!strcmp((*e)->name,name))
606      return (*e);
607  return NULL;
608}
609
610long spec_directory::find_number(char const *name)
611{
612  int i;
613  spec_entry **e;
614  for (i=0,e=entries; i<total; i++,e++)
615    if (!strcmp((*e)->name,name))
616      return i;
617  return -1;
618}
619
620spec_entry *spec_directory::find(int type)
621{
622  int i;
623  spec_entry **e;
624  for (i=0,e=entries; i<total; i++,e++)
625    if ((*e)->type==type)
626      return (*e);
627  return NULL;
628}
629
630long spec_directory::type_total(int type)
631{
632  int i,x=0;
633  spec_entry **e;
634  for (i=0,e=entries; i<total; i++,e++)
635    if ((*e)->type==type) x++;
636  return x;
637}
638
639long spec_directory::find_number(int type)
640{
641  int i;
642  spec_entry **e;
643  for (i=0,e=entries; i<total; i++,e++)
644    if ((*e)->type==type)
645      return i;
646  return -1;
647}
648
649void spec_directory::print()
650{
651  spec_entry **se;
652  int i;
653  printf("[   Entry type   ][   Entry name   ][  Size  ][ Offset ]\n");
654  for (i=0,se=entries; i<total; i++,se++)
655    (*se)->print();
656}
657
658
659void spec_directory::startup(bFILE *fp)
660{
661  char buf[256];
662  memset(buf,0,256);
663  fp->read(buf,8);
664  buf[9]=0;
665  size=0;
666  if (!strcmp(buf,SPEC_SIGNATURE))
667  {
668    total=fp->read_uint16();
669    entries=(spec_entry **)malloc(sizeof(spec_entry *)*total);
670    long start=fp->tell();
671
672    int i;
673    for (i=0; i<total; i++)
674    {
675      fp->read(buf,2);
676      long entry_size=sizeof(spec_entry)+(unsigned char)buf[1];
677      entry_size=(entry_size+3)&(~3);
678      fp->read(buf,(unsigned char)buf[1]);
679      fp->read(buf,9);
680
681      size+=entry_size;
682    }
683    data=malloc(size);
684    char *dp=(char *)data;
685    fp->seek(start,SEEK_SET);
686    for (i=0; i<total; i++)
687    {
688      spec_entry *se=(spec_entry *)dp;
689      entries[i]=se;
690
691      unsigned char len,flags,type;
692      fp->read(&type,1);
693      fp->read(&len,1);
694      se->type=type;
695      se->name=dp+sizeof(spec_entry);
696      fp->read(se->name,len);
697      fp->read(&flags,1);
698
699      se->size=fp->read_uint32();
700      se->offset=fp->read_uint32();
701      dp+=((sizeof(spec_entry)+len)+3)&(~3);
702    }
703  }
704  else
705  {
706    total=0;
707    data=NULL;
708    entries=NULL;
709  }
710}
711
712
713spec_directory::spec_directory(bFILE *fp)
714{ startup(fp); }
715
716spec_directory::spec_directory(FILE *fp)
717{
718  jFILE jfp(fp);
719  startup(&jfp);
720}
721
722spec_directory::spec_directory()
723{
724  size=0;
725  total=0;
726  data=NULL;
727  entries=NULL;
728}
729
730/*
731spec_directory::spec_directory(char *filename)
732{
733  jFILE *fp;
734  if (filename)
735  {
736    fp=new jFILE(filename,"rb");
737    if (!fp->open_failure())
738      startup(fp);
739    else
740    {
741      total=0;
742      entries=NULL;
743    }
744    delete fp;
745  } else printf("NULL filename to spec_directory::spec_directory\n");
746}*/
747
748int write_string(bFILE *fp, char const *st)
749{
750  unsigned char length=strlen(st)+1;
751  if (fp->write(&length,1)!=1) return 0;
752  if (fp->write(st,length)!=length) return 0;
753  return 1;
754}
755
756long spec_directory::data_start_offset()
757{
758    /* FIXME: no need for a for loop here! */
759    long i;
760    for(i = 0; i < total; i++)
761        return entries[i]->offset;
762
763    // If no entries, then no data, but return where it would start anyway
764    return SPEC_SIG_SIZE + 2;
765}
766
767long spec_directory::data_end_offset()
768{
769    /* FIXME: no need for a for loop here! */
770  spec_entry **e;
771  long i;
772  for (i=total-1,e=entries; i>=0; i--,e++)
773    return (*e)->offset+(*e)->size;
774
775  return SPEC_SIG_SIZE+2;
776}
777
778int spec_directory::write(bFILE *fp)
779{
780
781  char sig[SPEC_SIG_SIZE];
782  unsigned char flags=0;
783  unsigned long offset,data_size;
784  spec_entry **e;
785  strcpy(sig,SPEC_SIGNATURE);
786
787  if (fp->write(sig,sizeof(sig))!=sizeof(sig))    return 0;
788  fp->write_uint16(total);
789
790
791  int i;
792  for (i=0,e=entries; i<total; i++,e++)
793  {
794    if (fp->write(&(*e)->type,1)!=1)                 return 0;
795    if (!write_string(fp,(*e)->name))                return 0;
796    flags=0;
797    if (fp->write(&flags,1)!=1)                     return 0;
798
799    data_size=lltl((*e)->size);
800    if (fp->write((char *)&data_size,4)!=4)              return 0;
801    offset=lltl((*e)->offset);
802    if (fp->write((char *)&offset,4)!=4)                  return 0;
803
804  }
805  return 1;
806}
807
808jFILE *spec_directory::write(char const *filename)
809{
810  jFILE *fp;
811  fp=new jFILE(filename,"wb");
812  if (fp->open_failure()) { delete fp; return NULL; }
813  if (!write(fp))
814  {
815    delete fp;
816    return NULL;
817  } else return fp;
818
819}
820
821uint16_t read_uint16(FILE *fp)
822{
823  uint16_t x;
824  fread(&x,1,2,fp);
825  return lstl(x);
826}
827
828uint32_t read_uint32(FILE *fp)
829{
830  uint32_t x;
831  fread(&x,1,4,fp);
832  return lltl(x);
833}
834void write_uint16(FILE *fp, uint16_t x)
835{
836  x=lstl(x);
837  fwrite(&x,1,2,fp);
838}
839
840void write_uint32(FILE *fp, uint32_t x)
841{
842  x=lltl(x);
843  fwrite(&x,1,4,fp);
844}
845
846uint8_t read_uint8(FILE *fp) { return fgetc(fp)&0xff; }
847void write_uint8(FILE *fp, uint8_t x) { fputc((unsigned char)x,fp); }
848
849void spec_directory::remove(spec_entry *e)
850{
851  int i;
852  for (i=0; i<total && entries[i]!=e; i++);            // find the entry in the array first
853
854  if (entries[i]==e)                                 // make sre it was found
855  {
856    delete e;
857    total--;
858    for (; i<total; i++)                               // compact the pointer array
859      entries[i]=entries[i+1];
860    entries=(spec_entry **)realloc(entries,sizeof(spec_entry *)*total);
861  }
862  else
863    printf("Spec_directory::remove bad entry pointer\n");
864}
865
866
867
868void spec_directory::add_by_hand(spec_entry *e)
869{
870  total++;
871  entries=(spec_entry **)realloc(entries,sizeof(spec_entry *)*total);
872  entries[total-1]=e;
873}
874
875void spec_directory::delete_entries()   // if the directory was created by hand instead of by file
876{
877  int i;
878  for (i=0; i<total; i++)
879    delete entries[i];
880
881  if (total)
882    free(entries);
883}
884
885void note_open_fd(int fd, char const *str)
886{
887    total_files_open++;
888}
889
890void note_close_fd(int fd)
891{
892    total_files_open--;
893}
894
895void list_open_fds()
896{
897    printf("Total open file descriptors: %d\n", total_files_open);
898}
899
Note: See TracBrowser for help on using the repository browser.