Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: orxonox.OLD/orxonox/branches/sound/src/object.cc @ 3179

Last change on this file since 3179 was 2964, checked in by bensch, 20 years ago

orxonox/branches/sound: merged Trunk back into sound. and included all headers for real into the configure.ac-script.

File size: 18.6 KB
RevLine 
[2835]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 Grauer
13   co-programmer: ...
14*/
15
16int verbose = 0;
17
18#include "object.h"
19
[2853]20/**
21   \brief Creates a 3D-Object, but does not load any 3D-models
22   pretty useless
23*/
[2835]24Object::Object ()
25{
26
27  initialize();
28
[2853]29  BoxObject();
[2835]30
31  finalize();
32}
33
[2853]34/**
35   \brief Crates a 3D-Object and loads in a File
36   \param fileName file to parse and load (must be a .obj file)
37*/
[2835]38Object::Object(char* fileName)
39{
40  initialize();
41
42  importFile (fileName);
43
44  finalize();
45}
46
[2853]47/**
48   \brief Crates a 3D-Object, loads in a File and scales it.
49   \param fileName file to parse and load (must be a .obj file)
50   \param scaling The factor that the object will be scaled with.
51*/
52
[2835]53Object::Object(char* fileName, float scaling)
54{
55  initialize();
56  scaleFactor = scaling;
57
58  importFile (fileName);
59
60  finalize();
61}
62
[2853]63/**
64   \brief deletes an Object
65*/
66Object::~Object()
67{
68  if (verbose >= 2)
69    printf ("Deleting display List.\n");
70  Group* walker = firstGroup;
71  while (walker != NULL)
72    {
73      glDeleteLists (walker->listNumber, 1);
74      Group* lastWalker = walker;
75      walker = walker->nextGroup;
76      delete lastWalker;
77    } 
78}
79
80/**
81    \brief initializes the Object
82    This Function initializes all the needed arrays, Lists and clientStates
83*/
[2835]84bool Object::initialize (void)
85{
86  if (verbose >=3)
87    printf("new 3D-Object is being created\n"); 
88
[2853]89  // setting the start group;
90  firstGroup = new Group;
91  currentGroup = firstGroup;
92  groupCount = 0;
93 
94  initGroup (currentGroup);
[2835]95  mtlFileName = "";
96  scaleFactor = 1;
[2853]97  material = new Material();
[2835]98
99  glEnableClientState (GL_VERTEX_ARRAY);
[2853]100  //  glEnableClientState (GL_NORMAL_ARRAY);
[2835]101  //  glEnableClientState (GL_TEXTURE_COORD_ARRAY);
102
[2853]103
[2835]104  return true;
105}
106
[2853]107/**
108   \brief Imports a obj file and handles the the relative location
109   \param fileName The file to import
110*/
[2835]111bool Object::importFile (char* fileName)
112{
113  if (verbose >=3)
114    printf("preparing to read in file: %s\n", fileName);   
115  objFileName = fileName;
116  this->readFromObjFile (objFileName);
117  return true;
118}
119
[2853]120/**
[2866]121  \brief finalizes an Object.
[2853]122   This funcion is needed, to close the glList and all the other lists.
123*/
[2835]124bool Object::finalize(void)
125{
[2853]126  //  if (verbose >=3)
[2835]127    printf("finalizing the 3D-Object\n"); 
[2853]128  finalizeGroup (currentGroup);
129  if (material != NULL)
130    delete material;
[2835]131  return true;
132}
133
[2853]134/**
135   \brief Draws the Objects of all Groups.
136   It does this by just calling the Lists that must have been created earlier.
137*/
[2835]138void Object::draw (void)
139{
[2853]140  if (verbose >=2)
141    printf("drawing the 3D-Objects\n"); 
142  Group* walker = firstGroup;
143  while (walker != NULL)
144    {
145      if (verbose >= 3)
146        printf ("Drawing object %s\n", walker->name);
147      glCallList (walker->listNumber);
148      walker = walker->nextGroup;
149    }
[2835]150}
151
[2853]152/**
153   \brief Draws the Object number groupNumber
154   It does this by just calling the List that must have been created earlier.
155   \param groupNumber The number of the group that will be displayed.
156*/
157void Object::draw (int groupNumber)
158{
159  if (groupNumber >= groupCount)
160    {
161      if (verbose>=2)
162        printf ("You requested object number %i, but this File only contains of %i Objects.\n", groupNumber-1, groupCount);
163      return;
164    }
165  if (verbose >=2)
166    printf("drawing the requested 3D-Objects if found.\n"); 
167  Group* walker = firstGroup;
168  int counter = 0;
169  while (walker != NULL)
170    {
171      if (counter == groupNumber)
172        {
173          if (verbose >= 2)
174            printf ("Drawing object number %s named %s\n", counter, walker->name);
175          glCallList (walker->listNumber);
176          return;
177        }
178      ++counter;
179      walker = walker->nextGroup;
180    }
181  if (verbose >= 2)
182    printf("Object number %i in %s not Found.\n", groupNumber, objFileName);
183  return;
[2835]184
[2853]185}
186
187/**
188   \brief Draws the Object with a specific groupname
189   It does this by just calling the List that must have been created earlier.
190   \param groupName The name of the group that will be displayed.
191*/
192void Object::draw (char* groupName)
193{
194  if (verbose >=2)
195    printf("drawing the requested 3D-Objects if found.\n"); 
196  Group* walker = firstGroup;
197  while (walker != NULL)
198    {
199      if (!strcmp(walker->name, groupName))
200        {
201          if (verbose >= 2)
202            printf ("Drawing object %s\n", walker->name);
203          glCallList (walker->listNumber);
204          return;
205        }
206      walker = walker->nextGroup;
207    }
208  if (verbose >= 2)
209    printf("Object Named %s in %s not Found.\n", groupName, objFileName);
210  return;
211}
212
213/**
214   \returns Count of the Objects in this File
215*/
216int Object::getGroupCount (void)
217{
218  return groupCount;
219}
220
221/**
222   \brief initializes a new Group object
223*/
224bool Object::initGroup(Group* group)
225{
226  if (verbose >= 2)
227    printf("Adding new Group\n");
[2866]228  group->name = "";
[2853]229  group->faceMode = -1;
[2866]230  group->faceCount =0; 
[2853]231  if ((group->listNumber = glGenLists(1)) == 0 )
232    {
233      printf ("list could not be created for this Object\n");
234      return false;
235    }
236 
237  if (groupCount == 0)
238    {
239      group->firstVertex = 0;
240      group->firstNormal = 0;
241      group->firstNormal = 0;
242    }
243  else
244    {
245      group->firstVertex = currentGroup->firstVertex + currentGroup->vertices->getCount()/3;
246      group->firstNormal = currentGroup->firstNormal + currentGroup->normals->getCount()/3;
247      group->firstVertexTexture = currentGroup->firstVertexTexture + currentGroup->vTexture->getCount()/2;
248    }
[2866]249  if (verbose >=2)
250    printf ("Creating new Arrays, with starting points v:%i, vt:%i, vn:%i .\n", group->firstVertex, group->firstVertexTexture, group->firstNormal);
[2853]251  group->vertices = new Array();
252  group->normals = new Array();
253  group->vTexture = new Array();
254
255  glNewList (group->listNumber, GL_COMPILE);
256}
257
258/**
259   \brief finalizes a Group.
[2866]260   \param group the group to finalize.
[2853]261*/
262bool Object::finalizeGroup(Group* group)
263{
[2866]264  if (verbose >=2)
265    printf ("Finalize group %s.\n", group->name);
[2853]266  glEnd();
267  glEndList();
[2866]268}
269/**
270   \brief deletes the Arrays of the Group to save space.
271   \param group the group to delete the arrays from.
272*/
273bool Object::cleanupGroup(Group* group)
274{
275  if (verbose >=2)
276    printf ("cleaning up group %s.\n", group->name);
[2853]277 
278  delete group->vertices;
279  delete group->normals;
280  delete group->vTexture;
281}
[2866]282
[2853]283/**
284   \brief Reads in the .obj File and sets all the Values.
285   This function does read the file, parses it for the occurence of things like vertices, faces and so on, and executes the specific tasks
286   \param fileName the File that will be parsed (.obj-file)
287*/
[2835]288bool Object::readFromObjFile (char* fileName)
289{
290  OBJ_FILE = new ifstream(fileName);
291  if (!OBJ_FILE->is_open())
292    {
293      if (verbose >=1)
294        printf ("unable to open .OBJ file: %s\n Loading Box Object instead.\n", fileName);
295      BoxObject();
296      return false;
297    }
298  objFileName = fileName;
[2935]299  char Buffer[10000];
[2835]300  while(!OBJ_FILE->eof())
301    {
[2935]302      OBJ_FILE->getline(Buffer, 10000);
[2835]303      if (verbose >=4)
304        printf ("Read input line: %s\n",Buffer);
305     
306
307      // case vertice
308      if (!strncmp(Buffer, "v ", 2))
309        {
310          readVertex(Buffer+2);
311        }
312
313      // case face
314      else if (!strncmp(Buffer, "f ", 2))
315        {
316          readFace (Buffer+2);
317        }
318     
319      else if (!strncmp(Buffer, "mtllib", 6))
320        {
321          readMtlLib (Buffer+7);
322        }
323
324      else if (!strncmp(Buffer, "usemtl", 6))
325        {
326          readUseMtl (Buffer+7);
327        }
328
329      // case VertexNormal
330      else if (!strncmp(Buffer, "vn ", 2))
331      {
332        readVertexNormal(Buffer+3);
333      }
334
[2853]335      // case VertexTextureCoordinate
[2835]336      else if (!strncmp(Buffer, "vt ", 2))
337      {
338        readVertexTexture(Buffer+3);
339      }
[2853]340      // case group
341      else if (!strncmp(Buffer, "g", 1))
342        {
343          readGroup (Buffer+2);
344        }
[2835]345    }
[2853]346  OBJ_FILE->close();
[2866]347  return true;
[2835]348
349}
350
[2853]351/**
352   \brief parses a vertex-String
353   If a vertex line is found this function will inject it into the vertex-Array
354   \param vertexString The String that will be parsed.
355*/
[2835]356bool Object::readVertex (char* vertexString)
357{
[2853]358  readingVertices = true;
[2835]359  char subbuffer1[20];
360  char subbuffer2[20];
361  char subbuffer3[20];
362  sscanf (vertexString, "%s %s %s", subbuffer1, subbuffer2, subbuffer3);
363  if (verbose >= 3)
364    printf ("reading in a vertex: %s %s %s\n", subbuffer1, subbuffer2, subbuffer3);
[2853]365  currentGroup->vertices->addEntry(atof(subbuffer1)*scaleFactor, atof(subbuffer2)*scaleFactor, atof(subbuffer3)*scaleFactor);
[2835]366  return true;
367}
368
[2853]369/**
370   \brief parses a face-string
371   If a face line is found this function will add it to the glList.
372   The function makes a difference between QUADS and TRIANGLES, and will if changed re-open, set and re-close the gl-processe.
373   \param faceString The String that will be parsed.
374*/
[2835]375bool Object::readFace (char* faceString)
376{
[2853]377  // finalize the Arrays;
378  if (readingVertices == true)
[2835]379    {
[2853]380      currentGroup->vertices->finalizeArray();
[2935]381      //      glVertexPointer(3, GL_FLOAT, 0, currentGroup->vertices->getArray());
[2853]382      currentGroup->normals->finalizeArray();
[2935]383      //      glNormalPointer(GL_FLOAT, 0, currentGroup->normals->getArray());
[2853]384      currentGroup->vTexture->finalizeArray();
[2835]385    }
386
[2853]387  readingVertices = false;
[2866]388  currentGroup->faceCount++;
[2935]389
390  int elemCount = 0;
391 
392  FaceElement* firstElem = new FaceElement;
393  FaceElement* tmpElem = firstElem;
394
395 
396  while(strcmp (faceString, "\0"))
[2835]397    {
[2935]398      if (elemCount>0)
399          tmpElem = tmpElem->next = new FaceElement;
400      tmpElem->next = NULL;
401
402
403      sscanf (faceString, "%s", tmpElem->value);
404      faceString += strlen(tmpElem->value);
405      if (strcmp (faceString, "\0"))
406        faceString++;
407      elemCount++;
408
409
410    }
411 
412 
413  if (elemCount == 3)
414    {
[2853]415      if (currentGroup->faceMode != 3)
[2835]416        {
[2853]417          if (currentGroup->faceMode != -1)
[2835]418            glEnd();
419          glBegin(GL_TRIANGLES);
420        }
421     
[2853]422      currentGroup->faceMode = 3;
[2835]423      if (verbose >=3)
[2935]424        printf ("found triag.\n");
[2835]425    }
[2935]426 
427  else if (elemCount == 4)
[2835]428    {
[2853]429      if (currentGroup->faceMode != 4)
[2835]430        {
[2853]431          if (currentGroup->faceMode != -1)
[2835]432            glEnd();
433          glBegin(GL_QUADS);
434        }
[2853]435      currentGroup->faceMode = 4;
[2835]436      if (verbose >=3 )
[2935]437        printf ("found quad.\n");
[2835]438    }
[2935]439 
440  else if (elemCount > 4)
441    {
442      if (currentGroup->faceMode != -1)
443        glEnd();
444      glBegin(GL_POLYGON);
445      if (verbose >=3)
446        printf ("Polygon with %i faces found.", elemCount);
447      currentGroup->faceMode = elemCount;
448    }
449
450  tmpElem = firstElem;
451  while (tmpElem != NULL)
452    {
453      //      printf ("%s\n", tmpElem->value);
454      addGLElement(tmpElem->value);
455      tmpElem = tmpElem->next;
456    }
457
[2835]458}
459
[2853]460/**
461   \brief Adds a Face-element (one vertex of a face) with all its information.
462   It does this by searching:
463   1. The Vertex itself
464   2. The VertexNormale
465   3. The VertexTextureCoordinate
466   merging this information, the face will be drawn.
467
468*/
[2835]469bool Object::addGLElement (char* elementString)
470{
471  if (verbose >=3)
[2866]472    printf ("importing grafical Element to openGL\n");
[2835]473  char* vertex = elementString;
474
475  char* texture;
[2935]476  if ((texture = strstr (vertex, "/")) != NULL)
477    {
478      texture[0] = '\0';
479      texture ++;
480      if (verbose>=3)
481        printf ("includeing texture #%i, and mapping it to group texture #%i, textureArray has %i entries.\n", atoi(texture), (atoi(texture)-1 - currentGroup->firstVertexTexture)*3, currentGroup->vTexture->getCount());
482      glTexCoord2fv(currentGroup->vTexture->getArray()+(atoi(texture)-1 - currentGroup->firstVertexTexture)*2);
[2835]483
[2935]484      char* normal;
485      if ((normal = strstr (texture, "/")) !=NULL)
486        {
487          normal[0] = '\0';
488          normal ++;
489          //glArrayElement(atoi(vertex)-1);
490          glNormal3fv(currentGroup->normals->getArray() +(atoi(normal)-1 - currentGroup->firstNormal)*3);
491        }
[2835]492    }
[2866]493  if (verbose>=3)
494    printf ("includeing vertex #%i, and mapping it to group vertex #%i, vertexArray has %i entries.\n", atoi(vertex), (atoi(vertex)-1 - currentGroup->firstVertex)*3, currentGroup->vertices->getCount());
[2853]495  glVertex3fv(currentGroup->vertices->getArray() +(atoi(vertex)-1 - currentGroup->firstVertex)*3);
[2835]496
497}
498
[2853]499/**
500   \brief parses a vertexNormal-String
501   If a vertexNormal line is found this function will inject it into the vertexNormal-Array
502   \param normalString The String that will be parsed.
503*/
[2835]504bool Object::readVertexNormal (char* normalString)
505{
[2853]506  readingVertices = true;
[2835]507  char subbuffer1[20];
508  char subbuffer2[20];
509  char subbuffer3[20];
510  sscanf (normalString, "%s %s %s", subbuffer1, subbuffer2, subbuffer3);
511  if (verbose >=3 )
512    printf("found vertex-Normal %s, %s, %s\n", subbuffer1,subbuffer2,subbuffer3);
[2853]513  currentGroup->normals->addEntry(atof(subbuffer1), atof(subbuffer2), atof(subbuffer3));
[2835]514  return true;
515}
516
[2853]517/**
518   \brief parses a vertexTextureCoordinate-String
519   If a vertexTextureCoordinate line is found this function will inject it into the vertexTexture-Array
520   \param vTextureString The String that will be parsed.
521*/
[2835]522bool Object::readVertexTexture (char* vTextureString)
523{
[2853]524  readingVertices = true;
[2835]525  char subbuffer1[20];
526  char subbuffer2[20];
527  sscanf (vTextureString, "%s %s", subbuffer1, subbuffer2);
528  if (verbose >=3 )
529    printf("found vertex-Texture %s, %s\n", subbuffer1,subbuffer2);
[2853]530  currentGroup->vTexture->addEntry(atof(subbuffer1));
531  currentGroup->vTexture->addEntry(atof(subbuffer2));
[2835]532  return true;
533}
534
[2853]535/**
536   \brief parses a group String
537   This function initializes a new Group.
538   With it you should be able to import .obj-files with more than one Objects inside.
539   \param groupString the new Group to create
540*/
541bool Object::readGroup (char* groupString)
542{
[2866]543  // setting the group name if not default.
544  if (strcmp(currentGroup->name, "default"))
[2853]545    {
[2866]546      currentGroup->name = (char*) malloc ( strlen(groupString) * sizeof (char));
547      strcpy(currentGroup->name, groupString);
[2853]548    }
[2866]549  if (groupCount != 0 && currentGroup->faceCount>0)
[2853]550    {
[2866]551      Group* newGroup = new Group;
552      finalizeGroup(currentGroup);
553      currentGroup->nextGroup = newGroup;
554      initGroup(newGroup);
555      cleanupGroup(currentGroup); // deletes the arrays of the group; must be after initGroup.
556      currentGroup = newGroup; // must be after init see initGroup for more info
[2853]557    }
[2866]558
559  ++groupCount;
560
[2853]561}
[2835]562
[2853]563/**
564    \brief Function to read in a mtl File.
565    this Function parses all Lines of an mtl File
566    \param mtlFile The .mtl file to read
567*/
[2835]568bool Object::readMtlLib (char* mtlFile)
569{
570  MTL_FILE = new ifstream (mtlFile);
571  if (!MTL_FILE->is_open())
572    {
573      if (verbose >= 1)
574        printf ("unable to open file: %s\n", mtlFile);
575      return false;
576    }
577  mtlFileName = mtlFile;
578  if (verbose >=2)
579    printf ("Opening mtlFile: %s\n", mtlFileName);
580  char Buffer[500];
581  Material* tmpMat = material;
582  while(!MTL_FILE->eof())
583    {
584      MTL_FILE->getline(Buffer, 500);
585      if (verbose >= 4)
586        printf("found line in mtlFile: %s\n", Buffer);
587     
588
589      // create new Material
590      if (!strncmp(Buffer, "newmtl ", 2))
591        {
592          tmpMat = tmpMat->addMaterial(Buffer+7);
593          //      printf ("%s, %p\n", tmpMat->getName(), tmpMat);
594        }
595      // setting a illumMode
596      else if (!strncmp(Buffer, "illum", 5))
597        {
598          tmpMat->setIllum(Buffer+6);
599
600        }
601      // setting Diffuse Color
602      else if (!strncmp(Buffer, "Kd", 2))
603        {
604          tmpMat->setDiffuse(Buffer+3);
605        }
606      // setting Ambient Color
607      else if (!strncmp(Buffer, "Ka", 2))
608        {
609          tmpMat->setAmbient(Buffer+3);
610        }
611      // setting Specular Color
612      else if (!strncmp(Buffer, "Ks", 2))
613        {
614          tmpMat->setSpecular(Buffer+3);
615        }
[2853]616      // setting The Specular Shininess
617      else if (!strncmp(Buffer, "Ns", 2))
618        {
619          tmpMat->setShininess(Buffer+3);
620        }
621      // setting up transparency
622      else if (!strncmp(Buffer, "d", 1))
623        {
624          tmpMat->setTransparency(Buffer+2);
625        }
626      else if (!strncpy(Buffer, "Tf", 2))
627        {
628          tmpMat->setTransparency(Buffer+3);
629        }
630
[2835]631    }
632  return true;
633}
634
[2853]635/**
636   \brief Function that selects a material, if changed in the obj file.
637   \param matString the Material that will be set.
638*/
639
[2835]640bool Object::readUseMtl (char* matString)
641{
642  if (!strcmp (mtlFileName, ""))
643    {
644      if (verbose >= 1)
645        printf ("Not using new defined material, because no mtlFile found yet\n");
646      return false;
647    }
648     
[2853]649  if (currentGroup->faceMode != -1)
[2835]650    glEnd();
[2853]651  currentGroup->faceMode = 0;
[2835]652  if (verbose >= 2)
653    printf ("using material %s for coming Faces.\n", matString);
654  material->search(matString)->select();
655}
656
[2853]657/**
658   \brief Includes a default object
659   This will inject a Cube, because this is the most basic object.
660*/
[2835]661void Object::BoxObject(void)
662{
663  readVertex ("-0.500000 -0.500000 0.500000");
664  readVertex ("0.500000 -0.500000 0.500000");
665  readVertex ("-0.500000 0.500000 0.500000");
666  readVertex ("0.500000 0.500000 0.500000");
667  readVertex ("-0.500000 0.500000 -0.500000");
668  readVertex ("0.500000 0.500000 -0.500000");
669  readVertex ("-0.500000 -0.500000 -0.500000");
670  readVertex ("0.500000 -0.500000 -0.500000");
671  readVertexTexture ("0.000000 0.000000");
672  readVertexTexture ("1.000000 0.000000");
673  readVertexTexture ("0.000000 1.000000");
674  readVertexTexture ("1.000000 1.000000");
675  readVertexTexture ("0.000000 2.000000");
676  readVertexTexture ("1.000000 2.000000");
677  readVertexTexture ("0.000000 3.000000");
678  readVertexTexture ("1.000000 3.000000");
679  readVertexTexture ("0.000000 4.000000");
680  readVertexTexture ("1.000000 4.000000");
681  readVertexTexture ("2.000000 0.000000");
682  readVertexTexture ("2.000000 1.000000");
683  readVertexTexture ("-1.000000 0.000000");
684  readVertexTexture ("-1.000000 1.000000");
685 
686  readVertexNormal ("0.000000 0.000000 1.000000");
687  readVertexNormal ("0.000000 0.000000 1.000000");
688  readVertexNormal ("0.000000 0.000000 1.000000");
689  readVertexNormal ("0.000000 0.000000 1.000000");
690  readVertexNormal ("0.000000 1.000000 0.000000");
691  readVertexNormal ("0.000000 1.000000 0.000000");
692  readVertexNormal ("0.000000 1.000000 0.000000");
693  readVertexNormal ("0.000000 1.000000 0.000000");
694  readVertexNormal ("0.000000 0.000000 -1.000000");
695  readVertexNormal ("0.000000 0.000000 -1.000000");
696  readVertexNormal ("0.000000 0.000000 -1.000000");
697  readVertexNormal ("0.000000 0.000000 -1.000000");
698  readVertexNormal ("0.000000 -1.000000 0.000000");
699  readVertexNormal ("0.000000 -1.000000 0.000000");
700  readVertexNormal ("0.000000 -1.000000 0.000000");
701  readVertexNormal ("0.000000 -1.000000 0.000000");
702  readVertexNormal ("1.000000 0.000000 0.000000");
703  readVertexNormal ("1.000000 0.000000 0.000000");
704  readVertexNormal ("1.000000 0.000000 0.000000");
705  readVertexNormal ("1.000000 0.000000 0.000000");
706  readVertexNormal ("-1.000000 0.000000 0.000000");
707  readVertexNormal ("-1.000000 0.000000 0.000000");
708  readVertexNormal ("-1.000000 0.000000 0.000000");
709  readVertexNormal ("-1.000000 0.000000 0.000000");
710
711  readFace ("1/1/1 2/2/2 4/4/3 3/3/4");
712  readFace ("3/3/5 4/4/6 6/6/7 5/5/8");
713  readFace ("5/5/9 6/6/10 8/8/11 7/7/12");
714  readFace ("7/7/13 8/8/14 2/10/15 1/9/16");
715  readFace ("2/2/17 8/11/18 6/12/19 4/4/20");
716  readFace ("7/13/21 1/1/22 3/3/23 5/14/24");
717}
Note: See TracBrowser for help on using the repository browser.