Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: code/trunk/src/libraries/core/GraphicsManager.cc @ 8336

Last change on this file since 8336 was 8079, checked in by landauf, 14 years ago

merged usability branch back to trunk

incomplete summary of the changes in this branch:

  • enhanced keyboard navigation in GUIs
  • implemented new graphics menu and changeable window size at runtime
  • added developer mode
  • HUD shows if game is paused, game pauses if ingame menu is opened
  • removed a few obsolete commands and hid some that are more for internal use
  • numpad works in console and gui
  • faster loading of level info
  • enhanced usage of compositors (Shader class)
  • improved camera handling, configurable FOV and aspect ratio
  • Property svn:eol-style set to native
File size: 23.0 KB
Line 
1/*
2 *   ORXONOX - the hottest 3D action shooter ever to exist
3 *                    > www.orxonox.net <
4 *
5 *
6 *   License notice:
7 *
8 *   This program is free software; you can redistribute it and/or
9 *   modify it under the terms of the GNU General Public License
10 *   as published by the Free Software Foundation; either version 2
11 *   of the License, or (at your option) any later version.
12 *
13 *   This program is distributed in the hope that it will be useful,
14 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 *   GNU General Public License for more details.
17 *
18 *   You should have received a copy of the GNU General Public License
19 *   along with this program; if not, write to the Free Software
20 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21 *
22 *   Author:
23 *      Reto Grieder
24 *      Benjamin Knecht <beni_at_orxonox.net>, (C) 2007
25 *   Co-authors:
26 *      Felix Schulthess
27 *
28 */
29
30#include "GraphicsManager.h"
31
32#include <fstream>
33#include <sstream>
34#include <boost/filesystem.hpp>
35#include <boost/shared_array.hpp>
36
37#include <OgreArchiveFactory.h>
38#include <OgreArchiveManager.h>
39#include <OgreFrameListener.h>
40#include <OgreRoot.h>
41#include <OgreLogManager.h>
42#include <OgreRenderWindow.h>
43#include <OgreRenderSystem.h>
44#include <OgreResourceGroupManager.h>
45#include <OgreTextureManager.h>
46#include <OgreViewport.h>
47#include <OgreWindowEventUtilities.h>
48
49#include "SpecialConfig.h"
50#include "util/Clock.h"
51#include "util/Convert.h"
52#include "util/Exception.h"
53#include "util/StringUtils.h"
54#include "util/SubString.h"
55#include "ConfigValueIncludes.h"
56#include "CoreIncludes.h"
57#include "Core.h"
58#include "Game.h"
59#include "GameMode.h"
60#include "GUIManager.h"
61#include "Loader.h"
62#include "MemoryArchive.h"
63#include "PathConfig.h"
64#include "ViewportEventListener.h"
65#include "WindowEventListener.h"
66#include "XMLFile.h"
67#include "command/ConsoleCommand.h"
68#include "input/InputManager.h"
69
70namespace orxonox
71{
72    static const std::string __CC_GraphicsManager_group = "GraphicsManager";
73    static const std::string __CC_setScreenResolution_name = "setScreenResolution";
74    static const std::string __CC_setFSAA_name = "setFSAA";
75    static const std::string __CC_setVSync_name = "setVSync";
76    DeclareConsoleCommand(__CC_GraphicsManager_group, __CC_setScreenResolution_name, &prototype::string__uint_uint_bool);
77    DeclareConsoleCommand(__CC_GraphicsManager_group, __CC_setFSAA_name, &prototype::string__string);
78    DeclareConsoleCommand(__CC_GraphicsManager_group, __CC_setVSync_name, &prototype::string__bool);
79
80    static const std::string __CC_printScreen_name = "printScreen";
81    DeclareConsoleCommand(__CC_printScreen_name, &prototype::void__void);
82
83    class OgreWindowEventListener : public Ogre::WindowEventListener
84    {
85    public:
86        void windowResized     (Ogre::RenderWindow* rw)
87            { orxonox::WindowEventListener::resizeWindow(rw->getWidth(), rw->getHeight()); }
88        void windowFocusChange (Ogre::RenderWindow* rw)
89            { orxonox::WindowEventListener::changeWindowFocus(rw->isActive()); }
90        void windowClosed      (Ogre::RenderWindow* rw)
91            { orxonox::Game::getInstance().stop(); }
92        void windowMoved       (Ogre::RenderWindow* rw)
93            { orxonox::WindowEventListener::moveWindow(); }
94    };
95
96    GraphicsManager* GraphicsManager::singletonPtr_s = 0;
97
98    /**
99    @brief
100        Non-initialising constructor.
101    */
102    GraphicsManager::GraphicsManager(bool bLoadRenderer)
103        : ogreWindowEventListener_(new OgreWindowEventListener())
104#if OGRE_VERSION < 0x010600
105        , memoryArchiveFactory_(new MemoryArchiveFactory())
106#endif
107        , renderWindow_(0)
108        , viewport_(0)
109        , lastFrameStartTime_(0.0f)
110        , lastFrameEndTime_(0.0f)
111    {
112        RegisterObject(GraphicsManager);
113
114        this->setConfigValues();
115
116        // Ogre setup procedure (creating Ogre::Root)
117        this->loadOgreRoot();
118
119        // At first, add the root paths of the data directories as resource locations
120        Ogre::ResourceGroupManager::getSingleton().addResourceLocation(PathConfig::getDataPathString(), "FileSystem");
121        // Load resources
122        resources_.reset(new XMLFile("DefaultResources.oxr"));
123        resources_->setLuaSupport(false);
124        Loader::open(resources_.get());
125
126        // Only for development runs
127        if (PathConfig::isDevelopmentRun())
128        {
129            Ogre::ResourceGroupManager::getSingleton().addResourceLocation(PathConfig::getExternalDataPathString(), "FileSystem");
130            extResources_.reset(new XMLFile("resources.oxr"));
131            extResources_->setLuaSupport(false);
132            Loader::open(extResources_.get());
133        }
134
135        if (bLoadRenderer)
136        {
137            // Reads the ogre config and creates the render window
138            this->upgradeToGraphics();
139        }
140    }
141
142    /**
143    @brief
144        Destruction is done by the member scoped_ptrs.
145    */
146    GraphicsManager::~GraphicsManager()
147    {
148        Loader::unload(debugOverlay_.get());
149
150        Ogre::WindowEventUtilities::removeWindowEventListener(renderWindow_, ogreWindowEventListener_.get());
151        ModifyConsoleCommand(__CC_printScreen_name).resetFunction();
152        ModifyConsoleCommand(__CC_GraphicsManager_group, __CC_setScreenResolution_name).resetFunction();
153        ModifyConsoleCommand(__CC_GraphicsManager_group, __CC_setFSAA_name).resetFunction();
154        ModifyConsoleCommand(__CC_GraphicsManager_group, __CC_setVSync_name).resetFunction();
155
156        // Undeclare the resources
157        Loader::unload(resources_.get());
158        if (PathConfig::isDevelopmentRun())
159            Loader::unload(extResources_.get());
160    }
161
162    void GraphicsManager::setConfigValues()
163    {
164        SetConfigValue(ogreConfigFile_,  "ogre.cfg")
165            .description("Location of the Ogre config file");
166        SetConfigValue(ogrePluginsDirectory_, specialConfig::ogrePluginsDirectory)
167            .description("Folder where the Ogre plugins are located.");
168        SetConfigValue(ogrePlugins_, specialConfig::ogrePlugins)
169            .description("Comma separated list of all plugins to load.");
170        SetConfigValue(ogreLogFile_,     "ogre.log")
171            .description("Logfile for messages from Ogre. Use \"\" to suppress log file creation.");
172        SetConfigValue(ogreLogLevelTrivial_ , 5)
173            .description("Corresponding orxonox debug level for ogre Trivial");
174        SetConfigValue(ogreLogLevelNormal_  , 4)
175            .description("Corresponding orxonox debug level for ogre Normal");
176        SetConfigValue(ogreLogLevelCritical_, 2)
177            .description("Corresponding orxonox debug level for ogre Critical");
178    }
179
180    /**
181    @brief
182        Loads the renderer and creates the render window if not yet done so.
183    @remarks
184        This operation is irreversible without recreating the GraphicsManager!
185        So if it throws you HAVE to recreate the GraphicsManager!!!
186        It therefore offers almost no exception safety.
187    */
188    void GraphicsManager::upgradeToGraphics()
189    {
190        if (renderWindow_ != NULL)
191            return;
192
193        // load all the required plugins for Ogre
194        this->loadOgrePlugins();
195
196        this->loadRenderer();
197
198#if OGRE_VERSION < 0x010600
199        // WORKAROUND: There is an incompatibility for particle scripts when trying
200        // to support both Ogre 1.4 and 1.6. The hacky solution is to create
201        // scripts for the 1.6 version and then remove the inserted "particle_system"
202        // keyword. But we need to supply these new scripts as well, which is why
203        // there is an extra Ogre::Archive dealing with it in the memory.
204        using namespace Ogre;
205        ArchiveManager::getSingleton().addArchiveFactory(memoryArchiveFactory_.get());
206        const StringVector& groups = ResourceGroupManager::getSingleton().getResourceGroups();
207        // Travers all groups
208        for (StringVector::const_iterator itGroup = groups.begin(); itGroup != groups.end(); ++itGroup)
209        {
210            FileInfoListPtr files = ResourceGroupManager::getSingleton().findResourceFileInfo(*itGroup, "*.particle");
211            for (FileInfoList::const_iterator itFile = files->begin(); itFile != files->end(); ++itFile)
212            {
213                // open file
214                Ogre::DataStreamPtr input = ResourceGroupManager::getSingleton().openResource(itFile->filename, *itGroup, false);
215                std::stringstream output;
216                // Parse file and replace "particle_system" with nothing
217                while (!input->eof())
218                {
219                    std::string line = input->getLine();
220                    size_t pos = line.find("particle_system");
221                    if (pos != std::string::npos)
222                    {
223                        // 15 is the length of "particle_system"
224                        line.replace(pos, 15, "");
225                    }
226                    output << line << std::endl;
227                }
228                // Add file to the memory archive
229                shared_array<char> data(new char[output.str().size()]);
230                // Debug optimisations
231                const std::string& outputStr = output.str();
232                char* rawData = data.get();
233                for (unsigned i = 0; i < outputStr.size(); ++i)
234                    rawData[i] = outputStr[i];
235                MemoryArchive::addFile("particle_scripts_ogre_1.4_" + *itGroup, itFile->filename, data, output.str().size());
236            }
237            if (!files->empty())
238            {
239                // Declare the files, but using a new group
240                ResourceGroupManager::getSingleton().addResourceLocation("particle_scripts_ogre_1.4_" + *itGroup,
241                    "Memory", "particle_scripts_ogre_1.4_" + *itGroup);
242            }
243        }
244#endif
245
246        // Initialise all resources (do this AFTER the renderer has been loaded!)
247        // Note: You can only do this once! Ogre will check whether a resource group has
248        // already been initialised. If you need to load resources later, you will have to
249        // choose another resource group.
250        Ogre::ResourceGroupManager::getSingleton().initialiseAllResourceGroups();
251    }
252
253    /**
254    @brief
255        Creates the Ogre Root object and sets up the ogre log.
256    */
257    void GraphicsManager::loadOgreRoot()
258    {
259        COUT(3) << "Setting up Ogre..." << std::endl;
260
261        if (ogreConfigFile_.empty())
262        {
263            COUT(2) << "Warning: Ogre config file set to \"\". Defaulting to config.cfg" << std::endl;
264            ModifyConfigValue(ogreConfigFile_, tset, "config.cfg");
265        }
266        if (ogreLogFile_.empty())
267        {
268            COUT(2) << "Warning: Ogre log file set to \"\". Defaulting to ogre.log" << std::endl;
269            ModifyConfigValue(ogreLogFile_, tset, "ogre.log");
270        }
271
272        boost::filesystem::path ogreConfigFilepath(PathConfig::getConfigPath() / this->ogreConfigFile_);
273        boost::filesystem::path ogreLogFilepath(PathConfig::getLogPath() / this->ogreLogFile_);
274
275        // create a new logManager
276        // Ogre::Root will detect that we've already created a Log
277        ogreLogger_.reset(new Ogre::LogManager());
278        COUT(4) << "Ogre LogManager created" << std::endl;
279
280        // create our own log that we can listen to
281        Ogre::Log *myLog;
282        myLog = ogreLogger_->createLog(ogreLogFilepath.string(), true, false, false);
283        COUT(4) << "Ogre Log created" << std::endl;
284
285        myLog->setLogDetail(Ogre::LL_BOREME);
286        myLog->addListener(this);
287
288        COUT(4) << "Creating Ogre Root..." << std::endl;
289
290        // check for config file existence because Ogre displays (caught) exceptions if not
291        if (!boost::filesystem::exists(ogreConfigFilepath))
292        {
293            // create a zero sized file
294            std::ofstream creator;
295            creator.open(ogreConfigFilepath.string().c_str());
296            creator.close();
297        }
298
299        // Leave plugins file empty. We're going to do that part manually later
300        ogreRoot_.reset(new Ogre::Root("", ogreConfigFilepath.string(), ogreLogFilepath.string()));
301
302        COUT(3) << "Ogre set up done." << std::endl;
303    }
304
305    void GraphicsManager::loadOgrePlugins()
306    {
307        // just to make sure the next statement doesn't segfault
308        if (ogrePluginsDirectory_.empty())
309            ogrePluginsDirectory_ = '.';
310
311        boost::filesystem::path folder(ogrePluginsDirectory_);
312        // Do some SubString magic to get the comma separated list of plugins
313        SubString plugins(ogrePlugins_, ",", " ", false, '\\', false, '"', false, '{', '}', false, '\0');
314        // Use backslash paths on Windows! file_string() already does that though.
315        for (unsigned int i = 0; i < plugins.size(); ++i)
316            ogreRoot_->loadPlugin((folder / plugins[i]).file_string());
317    }
318
319    void GraphicsManager::loadRenderer()
320    {
321        CCOUT(4) << "Configuring Renderer" << std::endl;
322
323        bool updatedConfig = Core::getInstance().getOgreConfigTimestamp() > Core::getInstance().getLastLevelTimestamp();
324        if (updatedConfig)
325            COUT(2) << "Ogre config file has changed, but no level was started since then. Displaying config dialogue again to verify the changes." << std::endl;
326
327        if (!ogreRoot_->restoreConfig() || updatedConfig)
328        {
329            if (!ogreRoot_->showConfigDialog())
330                ThrowException(InitialisationFailed, "OGRE graphics configuration dialogue canceled.");
331            else
332                Core::getInstance().updateOgreConfigTimestamp();
333        }
334
335        CCOUT(4) << "Creating render window" << std::endl;
336
337        this->renderWindow_ = ogreRoot_->initialise(true, "Orxonox");
338        // Propagate the size of the new winodw
339        this->ogreWindowEventListener_->windowResized(renderWindow_);
340
341        Ogre::WindowEventUtilities::addWindowEventListener(this->renderWindow_, ogreWindowEventListener_.get());
342
343        // create a full screen default viewport
344        // Note: This may throw when adding a viewport with an existing z-order!
345        //       But in our case we only have one viewport for now anyway, therefore
346        //       no ScopeGuards or anything to handle exceptions.
347        this->viewport_ = this->renderWindow_->addViewport(0, 0);
348
349        Ogre::TextureManager::getSingleton().setDefaultNumMipmaps(Ogre::MIP_UNLIMITED);
350
351        // add console commands
352        ModifyConsoleCommand(__CC_printScreen_name).setFunction(&GraphicsManager::printScreen, this);
353        ModifyConsoleCommand(__CC_GraphicsManager_group, __CC_setScreenResolution_name).setFunction(&GraphicsManager::setScreenResolution, this);
354        ModifyConsoleCommand(__CC_GraphicsManager_group, __CC_setFSAA_name).setFunction(&GraphicsManager::setFSAA, this);
355        ModifyConsoleCommand(__CC_GraphicsManager_group, __CC_setVSync_name).setFunction(&GraphicsManager::setVSync, this);
356    }
357
358    void GraphicsManager::loadDebugOverlay()
359    {
360        // Load debug overlay to show info about fps and tick time
361        COUT(4) << "Loading Debug Overlay..." << std::endl;
362        debugOverlay_.reset(new XMLFile("debug.oxo"));
363        Loader::open(debugOverlay_.get());
364    }
365
366    /**
367    @note
368        A note about the Ogre::FrameListener: Even though we don't use them,
369        they still get called.
370    */
371    void GraphicsManager::postUpdate(const Clock& time)
372    {
373        // Time before rendering
374        uint64_t timeBeforeTick = time.getRealMicroseconds();
375
376        // Ogre's time keeping object
377        Ogre::FrameEvent evt;
378
379        // Translate to Ogre float times before the update
380        float temp = lastFrameStartTime_;
381        lastFrameStartTime_ = (float)timeBeforeTick * 0.000001f;
382        evt.timeSinceLastFrame = lastFrameStartTime_ - temp;
383        evt.timeSinceLastEvent = lastFrameStartTime_ - lastFrameEndTime_;
384
385        // Ogre requires the time too
386        ogreRoot_->_fireFrameStarted(evt);
387
388        // Pump messages in all registered RenderWindows
389        // This calls the WindowEventListener objects.
390        Ogre::WindowEventUtilities::messagePump();
391        // Make sure the window stays active even when not focused
392        // (probably only necessary on windows)
393        this->renderWindow_->setActive(true);
394
395        // Render frame
396        ogreRoot_->_updateAllRenderTargets();
397
398        uint64_t timeAfterTick = time.getRealMicroseconds();
399        // Subtract the time used for rendering from the tick time counter
400        Game::getInstance().subtractTickTime((int32_t)(timeAfterTick - timeBeforeTick));
401
402        // Translate to Ogre float times after the update
403        temp = lastFrameEndTime_;
404        lastFrameEndTime_ = (float)timeBeforeTick * 0.000001f;
405        evt.timeSinceLastFrame = lastFrameEndTime_ - temp;
406        evt.timeSinceLastEvent = lastFrameEndTime_ - lastFrameStartTime_;
407
408        // Ogre also needs the time after the frame finished
409        ogreRoot_->_fireFrameEnded(evt);
410    }
411
412    void GraphicsManager::setCamera(Ogre::Camera* camera)
413    {
414        Ogre::Camera* oldCamera = this->viewport_->getCamera();
415
416        this->viewport_->setCamera(camera);
417        GUIManager::getInstance().setCamera(camera);
418
419        for (ObjectList<ViewportEventListener>::iterator it = ObjectList<ViewportEventListener>::begin(); it != ObjectList<ViewportEventListener>::end(); ++it)
420            it->cameraChanged(this->viewport_, oldCamera);
421    }
422
423    /**
424    @brief
425        Method called by the LogListener interface from Ogre.
426        We use it to capture Ogre log messages and handle it ourselves.
427    @param message
428        The message to be logged
429    @param lml
430        The message level the log is using
431    @param maskDebug
432        If we are printing to the console or not
433    @param logName
434        The name of this log (so you can have several listeners
435        for different logs, and identify them)
436    */
437    void GraphicsManager::messageLogged(const std::string& message,
438        Ogre::LogMessageLevel lml, bool maskDebug, const std::string& logName)
439    {
440        int orxonoxLevel;
441        std::string introduction;
442        // Do not show caught OGRE exceptions in front
443        if (message.find("EXCEPTION") != std::string::npos)
444        {
445            orxonoxLevel = OutputLevel::Debug;
446            introduction = "Ogre, caught exception: ";
447        }
448        else
449        {
450            switch (lml)
451            {
452            case Ogre::LML_TRIVIAL:
453                orxonoxLevel = this->ogreLogLevelTrivial_;
454                break;
455            case Ogre::LML_NORMAL:
456                orxonoxLevel = this->ogreLogLevelNormal_;
457                break;
458            case Ogre::LML_CRITICAL:
459                orxonoxLevel = this->ogreLogLevelCritical_;
460                break;
461            default:
462                orxonoxLevel = 0;
463            }
464            introduction = "Ogre: ";
465        }
466        OutputHandler::getOutStream(orxonoxLevel)
467            << introduction << message << std::endl;
468    }
469
470    size_t GraphicsManager::getRenderWindowHandle()
471    {
472        size_t windowHnd = 0;
473        renderWindow_->getCustomAttribute("WINDOW", &windowHnd);
474        return windowHnd;
475    }
476
477    bool GraphicsManager::isFullScreen() const
478    {
479        return this->renderWindow_->isFullScreen();
480    }
481
482    unsigned int GraphicsManager::getWindowWidth() const
483    {
484        return this->renderWindow_->getWidth();
485    }
486
487    unsigned int GraphicsManager::getWindowHeight() const
488    {
489        return this->renderWindow_->getHeight();
490    }
491
492    bool GraphicsManager::hasVSyncEnabled() const
493    {
494        Ogre::ConfigOptionMap& options = ogreRoot_->getRenderSystem()->getConfigOptions();
495        Ogre::ConfigOptionMap::iterator it = options.find("VSync");
496        if (it != options.end())
497            return (it->second.currentValue == "Yes");
498        else
499            return false;
500    }
501
502    std::string GraphicsManager::getFSAAMode() const
503    {
504        Ogre::ConfigOptionMap& options = ogreRoot_->getRenderSystem()->getConfigOptions();
505        Ogre::ConfigOptionMap::iterator it = options.find("FSAA");
506        if (it != options.end())
507            return it->second.currentValue;
508        else
509            return "";
510    }
511
512    std::string GraphicsManager::setScreenResolution(unsigned int width, unsigned int height, bool fullscreen)
513    {
514        // workaround to detect if the colour depth should be written to the config file
515        bool bWriteColourDepth = false;
516        Ogre::ConfigOptionMap& options = ogreRoot_->getRenderSystem()->getConfigOptions();
517        Ogre::ConfigOptionMap::iterator it = options.find("Video Mode");
518        if (it != options.end())
519            bWriteColourDepth = (it->second.currentValue.find('@') != std::string::npos);
520
521        if (bWriteColourDepth)
522        {
523            this->ogreRoot_->getRenderSystem()->setConfigOption("Video Mode", multi_cast<std::string>(width)
524                                                                    + " x " + multi_cast<std::string>(height)
525                                                                    + " @ " + multi_cast<std::string>(this->getRenderWindow()->getColourDepth()) + "-bit colour");
526        }
527        else
528        {
529            this->ogreRoot_->getRenderSystem()->setConfigOption("Video Mode", multi_cast<std::string>(width)
530                                                                    + " x " + multi_cast<std::string>(height));
531        }
532
533        this->ogreRoot_->getRenderSystem()->setConfigOption("Full Screen", fullscreen ? "Yes" : "No");
534
535        std::string validate = this->ogreRoot_->getRenderSystem()->validateConfigOptions();
536
537        if (validate == "")
538        {
539            GraphicsManager::getInstance().getRenderWindow()->setFullscreen(fullscreen, width, height);
540            this->ogreRoot_->saveConfig();
541            Core::getInstance().updateOgreConfigTimestamp();
542            // Also reload the input devices
543            InputManager::getInstance().reload();
544        }
545
546        return validate;
547    }
548
549    std::string GraphicsManager::setFSAA(const std::string& mode)
550    {
551        this->ogreRoot_->getRenderSystem()->setConfigOption("FSAA", mode);
552
553        std::string validate = this->ogreRoot_->getRenderSystem()->validateConfigOptions();
554
555        if (validate == "")
556        {
557            //this->ogreRoot_->getRenderSystem()->reinitialise(); // can't use this that easily, because it recreates the render window, invalidating renderWindow_
558            this->ogreRoot_->saveConfig();
559            Core::getInstance().updateOgreConfigTimestamp();
560        }
561
562        return validate;
563    }
564
565    std::string GraphicsManager::setVSync(bool vsync)
566    {
567        this->ogreRoot_->getRenderSystem()->setConfigOption("VSync", vsync ? "Yes" : "No");
568
569        std::string validate = this->ogreRoot_->getRenderSystem()->validateConfigOptions();
570
571        if (validate == "")
572        {
573            //this->ogreRoot_->getRenderSystem()->reinitialise(); // can't use this that easily, because it recreates the render window, invalidating renderWindow_
574            this->ogreRoot_->saveConfig();
575            Core::getInstance().updateOgreConfigTimestamp();
576        }
577
578        return validate;
579    }
580
581    void GraphicsManager::printScreen()
582    {
583        assert(this->renderWindow_);
584        this->renderWindow_->writeContentsToTimestampedFile(PathConfig::getLogPathString() + "screenShot_", ".png");
585    }
586}
Note: See TracBrowser for help on using the repository browser.