Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: code/branches/resource2/src/core/Core.cc @ 5690

Last change on this file since 5690 was 5670, checked in by rgrieder, 15 years ago

Added support for non exclusive mouse mode on Windows. This means you get the normal mouse cursor when displaying a GUI menu in windowed mode.
The feature is activated via InputState (InputState::setIsExclusiveMouse(bool)). Whenever that state is on top of the mouse state stack, the input manager reloads if necessary.

  • Property svn:eol-style set to native
File size: 24.5 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 *      Fabian 'x3n' Landau
24 *      Reto Grieder
25 *   Co-authors:
26 *      ...
27 *
28 */
29
30/**
31@file
32@brief
33    Implementation of the Core singleton with its global variables (avoids boost include)
34*/
35
36#include "Core.h"
37
38#include <cassert>
39#include <fstream>
40#include <cstdlib>
41#include <cstdio>
42#include <boost/filesystem.hpp>
43
44#ifdef ORXONOX_PLATFORM_WINDOWS
45#  ifndef WIN32_LEAN_AND_MEAN
46#    define WIN32_LEAN_AND_MEAN
47#  endif
48#  include <windows.h>
49#  undef min
50#  undef max
51#elif defined(ORXONOX_PLATFORM_APPLE)
52#  include <sys/param.h>
53#  include <mach-o/dyld.h>
54#else /* Linux */
55#  include <sys/types.h>
56#  include <unistd.h>
57#endif
58
59#include "SpecialConfig.h"
60#include "util/Debug.h"
61#include "util/Exception.h"
62#include "util/SignalHandler.h"
63#include "Clock.h"
64#include "CommandExecutor.h"
65#include "CommandLine.h"
66#include "ConfigFileManager.h"
67#include "ConfigValueIncludes.h"
68#include "CoreIncludes.h"
69#include "Factory.h"
70#include "GameMode.h"
71#include "GraphicsManager.h"
72#include "GUIManager.h"
73#include "Identifier.h"
74#include "Language.h"
75#include "LuaState.h"
76#include "Shell.h"
77#include "TclBind.h"
78#include "TclThreadManager.h"
79#include "input/InputManager.h"
80
81namespace orxonox
82{
83    //! Static pointer to the singleton
84    Core* Core::singletonPtr_s  = 0;
85
86    SetCommandLineArgument(externalDataPath, "").information("Path to the external data files");
87    SetCommandLineOnlyArgument(writingPathSuffix, "").information("Additional subfolder for config and log files");
88    SetCommandLineArgument(settingsFile, "orxonox.ini").information("THE configuration file");
89#ifdef ORXONOX_PLATFORM_WINDOWS
90    SetCommandLineArgument(limitToCPU, 0).information("Limits the program to one cpu/core (1, 2, 3, etc.). 0 turns it off (default)");
91#endif
92
93    /**
94    @brief
95        Helper class for the Core singleton: we cannot derive
96        Core from OrxonoxClass because we need to handle the Identifier
97        destruction in the Core destructor.
98    */
99    class CoreConfiguration : public OrxonoxClass
100    {
101    public:
102        CoreConfiguration()
103        {
104        }
105
106        void initialise()
107        {
108            RegisterRootObject(CoreConfiguration);
109            this->setConfigValues();
110
111            // External data directory only exists for dev runs
112            if (Core::isDevelopmentRun())
113            {
114                // Possible data path override by the command line
115                if (!CommandLine::getArgument("externalDataPath")->hasDefaultValue())
116                    tsetExternalDataPath(CommandLine::getValue("externalDataPath"));
117            }
118        }
119
120        /**
121            @brief Function to collect the SetConfigValue-macro calls.
122        */
123        void setConfigValues()
124        {
125#ifdef NDEBUG
126            const unsigned int defaultLevelConsole = 1;
127            const unsigned int defaultLevelLogfile = 3;
128            const unsigned int defaultLevelShell   = 1;
129#else
130            const unsigned int defaultLevelConsole = 3;
131            const unsigned int defaultLevelLogfile = 4;
132            const unsigned int defaultLevelShell   = 3;
133#endif
134            SetConfigValue(softDebugLevelConsole_, defaultLevelConsole)
135                .description("The maximal level of debug output shown in the console")
136                .callback(this, &CoreConfiguration::debugLevelChanged);
137            SetConfigValue(softDebugLevelLogfile_, defaultLevelLogfile)
138                .description("The maximal level of debug output shown in the logfile")
139                .callback(this, &CoreConfiguration::debugLevelChanged);
140            SetConfigValue(softDebugLevelShell_, defaultLevelShell)
141                .description("The maximal level of debug output shown in the ingame shell")
142                .callback(this, &CoreConfiguration::debugLevelChanged);
143
144            SetConfigValue(language_, Language::getInstance().defaultLanguage_)
145                .description("The language of the ingame text")
146                .callback(this, &CoreConfiguration::languageChanged);
147            SetConfigValue(bInitializeRandomNumberGenerator_, true)
148                .description("If true, all random actions are different each time you start the game")
149                .callback(this, &CoreConfiguration::initializeRandomNumberGenerator);
150        }
151
152        /**
153            @brief Callback function if the debug level has changed.
154        */
155        void debugLevelChanged()
156        {
157            // softDebugLevel_ is the maximum of the 3 variables
158            this->softDebugLevel_ = this->softDebugLevelConsole_;
159            if (this->softDebugLevelLogfile_ > this->softDebugLevel_)
160                this->softDebugLevel_ = this->softDebugLevelLogfile_;
161            if (this->softDebugLevelShell_ > this->softDebugLevel_)
162                this->softDebugLevel_ = this->softDebugLevelShell_;
163
164            OutputHandler::setSoftDebugLevel(OutputHandler::LD_All,     this->softDebugLevel_);
165            OutputHandler::setSoftDebugLevel(OutputHandler::LD_Console, this->softDebugLevelConsole_);
166            OutputHandler::setSoftDebugLevel(OutputHandler::LD_Logfile, this->softDebugLevelLogfile_);
167            OutputHandler::setSoftDebugLevel(OutputHandler::LD_Shell,   this->softDebugLevelShell_);
168        }
169
170        /**
171            @brief Callback function if the language has changed.
172        */
173        void languageChanged()
174        {
175            // Read the translation file after the language was configured
176            Language::getInstance().readTranslatedLanguageFile();
177        }
178
179        /**
180            @brief Sets the language in the config-file back to the default.
181        */
182        void resetLanguage()
183        {
184            ResetConfigValue(language_);
185        }
186
187        /**
188        @brief
189            Temporary sets the data path
190        @param path
191            The new data path
192        */
193        void tsetExternalDataPath(const std::string& path)
194        {
195            dataPath_ = boost::filesystem::path(path);
196        }
197
198        void initializeRandomNumberGenerator()
199        {
200            static bool bInitialized = false;
201            if (!bInitialized && this->bInitializeRandomNumberGenerator_)
202            {
203                srand(static_cast<unsigned int>(time(0)));
204                rand();
205                bInitialized = true;
206            }
207        }
208
209        int softDebugLevel_;                            //!< The debug level
210        int softDebugLevelConsole_;                     //!< The debug level for the console
211        int softDebugLevelLogfile_;                     //!< The debug level for the logfile
212        int softDebugLevelShell_;                       //!< The debug level for the ingame shell
213        std::string language_;                          //!< The language
214        bool bInitializeRandomNumberGenerator_;         //!< If true, srand(time(0)) is called
215
216        //! Path to the parent directory of the ones above if program was installed with relativ pahts
217        boost::filesystem::path rootPath_;
218        boost::filesystem::path executablePath_;        //!< Path to the executable
219        boost::filesystem::path dataPath_;              //!< Path to the data file folder
220        boost::filesystem::path externalDataPath_;      //!< Path to the external data file folder
221        boost::filesystem::path configPath_;            //!< Path to the config file folder
222        boost::filesystem::path logPath_;               //!< Path to the log file folder
223    };
224
225
226    Core::Core(const std::string& cmdLine)
227        // Cleanup guard for identifier destruction (incl. XMLPort, configValues, consoleCommands)
228        : identifierDestroyer_(Identifier::destroyAllIdentifiers)
229        // Cleanup guard for external console commands that don't belong to an Identifier
230        , consoleCommandDestroyer_(CommandExecutor::destroyExternalCommands)
231        , configuration_(new CoreConfiguration()) // Don't yet create config values!
232        , bDevRun_(false)
233        , bGraphicsLoaded_(false)
234    {
235        // Parse command line arguments first
236        CommandLine::parseCommandLine(cmdLine);
237
238        // Determine and set the location of the executable
239        setExecutablePath();
240
241        // Determine whether we have an installed or a binary dir run
242        // The latter occurs when simply running from the build directory
243        checkDevBuild();
244
245        // Make sure the directories we write in exist or else make them
246        createDirectories();
247
248        // create a signal handler (only active for linux)
249        // This call is placed as soon as possible, but after the directories are set
250        this->signalHandler_.reset(new SignalHandler());
251        this->signalHandler_->doCatch(configuration_->executablePath_.string(), Core::getLogPathString() + "orxonox_crash.log");
252
253        // Set the correct log path. Before this call, /tmp (Unix) or %TEMP% was used
254        OutputHandler::getOutStream().setLogPath(Core::getLogPathString());
255
256        // Parse additional options file now that we know its path
257        CommandLine::parseFile();
258
259#ifdef ORXONOX_PLATFORM_WINDOWS
260        // limit the main thread to the first core so that QueryPerformanceCounter doesn't jump
261        // do this after ogre has initialised. Somehow Ogre changes the settings again (not through
262        // the timer though).
263        int limitToCPU = CommandLine::getValue("limitToCPU");
264        if (limitToCPU > 0)
265            setThreadAffinity(static_cast<unsigned int>(limitToCPU));
266#endif
267
268        // Manage ini files and set the default settings file (usually orxonox.ini)
269        this->configFileManager_.reset(new ConfigFileManager());
270        this->configFileManager_->setFilename(ConfigFileType::Settings,
271            CommandLine::getValue("settingsFile").getString());
272
273        // Required as well for the config values
274        this->languageInstance_.reset(new Language());
275
276        // creates the class hierarchy for all classes with factories
277        Factory::createClassHierarchy();
278
279        // Do this soon after the ConfigFileManager has been created to open up the
280        // possibility to configure everything below here
281        this->configuration_->initialise();
282
283        // Load OGRE excluding the renderer and the render window
284        this->graphicsManager_.reset(new GraphicsManager(false));
285
286        // initialise Tcl
287        this->tclBind_.reset(new TclBind(Core::getDataPathString()));
288        this->tclThreadManager_.reset(new TclThreadManager(tclBind_->getTclInterpreter()));
289
290        // create a shell
291        this->shell_.reset(new Shell());
292    }
293
294    /**
295    @brief
296        All destruction code is handled by scoped_ptrs and ScopeGuards.
297    */
298    Core::~Core()
299    {
300    }
301
302    void Core::loadGraphics()
303    {
304        // Any exception should trigger this, even in upgradeToGraphics (see its remarks)
305        Loki::ScopeGuard unloader = Loki::MakeObjGuard(*this, &Core::unloadGraphics);
306
307        // Upgrade OGRE to receive a render window
308        graphicsManager_->upgradeToGraphics();
309
310        // Calls the InputManager which sets up the input devices.
311        inputManager_.reset(new InputManager());
312
313        // load the CEGUI interface
314        guiManager_.reset(new GUIManager(graphicsManager_->getRenderWindow(),
315            inputManager_->getMousePosition(), graphicsManager_->isFullScreen()));
316
317        unloader.Dismiss();
318
319        bGraphicsLoaded_ = true;
320    }
321
322    void Core::unloadGraphics()
323    {
324        this->guiManager_.reset();;
325        this->inputManager_.reset();;
326        this->graphicsManager_.reset();
327
328        // Load Ogre::Root again, but without the render system
329        try
330            { this->graphicsManager_.reset(new GraphicsManager(false)); }
331        catch (...)
332        {
333            COUT(0) << "An exception occurred during 'new GraphicsManager' while "
334                    << "another exception was being handled. This will lead to undefined behaviour!" << std::endl
335                    << "Terminating the program." << std::endl;
336            abort();
337        }
338
339        bGraphicsLoaded_ = false;
340    }
341
342    /**
343        @brief Returns the softDebugLevel for the given device (returns a default-value if the class is right about to be created).
344        @param device The device
345        @return The softDebugLevel
346    */
347    /*static*/ int Core::getSoftDebugLevel(OutputHandler::OutputDevice device)
348    {
349        switch (device)
350        {
351        case OutputHandler::LD_All:
352            return Core::getInstance().configuration_->softDebugLevel_;
353        case OutputHandler::LD_Console:
354            return Core::getInstance().configuration_->softDebugLevelConsole_;
355        case OutputHandler::LD_Logfile:
356            return Core::getInstance().configuration_->softDebugLevelLogfile_;
357        case OutputHandler::LD_Shell:
358            return Core::getInstance().configuration_->softDebugLevelShell_;
359        default:
360            assert(0);
361            return 2;
362        }
363    }
364
365     /**
366        @brief Sets the softDebugLevel for the given device. Please use this only temporary and restore the value afterwards, as it overrides the configured value.
367        @param device The device
368        @param level The level
369    */
370    /*static*/ void Core::setSoftDebugLevel(OutputHandler::OutputDevice device, int level)
371    {
372        if (device == OutputHandler::LD_All)
373            Core::getInstance().configuration_->softDebugLevel_ = level;
374        else if (device == OutputHandler::LD_Console)
375            Core::getInstance().configuration_->softDebugLevelConsole_ = level;
376        else if (device == OutputHandler::LD_Logfile)
377            Core::getInstance().configuration_->softDebugLevelLogfile_ = level;
378        else if (device == OutputHandler::LD_Shell)
379            Core::getInstance().configuration_->softDebugLevelShell_ = level;
380
381        OutputHandler::setSoftDebugLevel(device, level);
382    }
383
384    /**
385        @brief Returns the configured language.
386    */
387    /*static*/ const std::string& Core::getLanguage()
388    {
389        return Core::getInstance().configuration_->language_;
390    }
391
392    /**
393        @brief Sets the language in the config-file back to the default.
394    */
395    /*static*/ void Core::resetLanguage()
396    {
397        Core::getInstance().configuration_->resetLanguage();
398    }
399
400    /*static*/ void Core::tsetExternalDataPath(const std::string& path)
401    {
402        getInstance().configuration_->tsetExternalDataPath(path);
403    }
404
405    /*static*/ const boost::filesystem::path& Core::getDataPath()
406    {
407        return getInstance().configuration_->dataPath_;
408    }
409    /*static*/ std::string Core::getDataPathString()
410    {
411        return getInstance().configuration_->dataPath_.string() + '/';
412    }
413
414    /*static*/ const boost::filesystem::path& Core::getExternalDataPath()
415    {
416        return getInstance().configuration_->externalDataPath_;
417    }
418    /*static*/ std::string Core::getExternalDataPathString()
419    {
420        return getInstance().configuration_->externalDataPath_.string() + '/';
421    }
422
423    /*static*/ const boost::filesystem::path& Core::getConfigPath()
424    {
425        return getInstance().configuration_->configPath_;
426    }
427    /*static*/ std::string Core::getConfigPathString()
428    {
429        return getInstance().configuration_->configPath_.string() + '/';
430    }
431
432    /*static*/ const boost::filesystem::path& Core::getLogPath()
433    {
434        return getInstance().configuration_->logPath_;
435    }
436    /*static*/ std::string Core::getLogPathString()
437    {
438        return getInstance().configuration_->logPath_.string() + '/';
439    }
440
441    /*static*/ const boost::filesystem::path& Core::getRootPath()
442    {
443        return getInstance().configuration_->rootPath_;
444    }
445    /*static*/ std::string Core::getRootPathString()
446    {
447        return getInstance().configuration_->rootPath_.string() + '/';
448    }
449
450    /**
451    @note
452        The code of this function has been copied and adjusted from OGRE, an open source graphics engine.
453            (Object-oriented Graphics Rendering Engine)
454        For the latest info, see http://www.ogre3d.org/
455
456        Copyright (c) 2000-2008 Torus Knot Software Ltd
457
458        OGRE is licensed under the LGPL. For more info, see OGRE license.
459    */
460    void Core::setThreadAffinity(int limitToCPU)
461    {
462#ifdef ORXONOX_PLATFORM_WINDOWS
463
464        if (limitToCPU <= 0)
465            return;
466
467        unsigned int coreNr = limitToCPU - 1;
468        // Get the current process core mask
469        DWORD procMask;
470        DWORD sysMask;
471#  if _MSC_VER >= 1400 && defined (_M_X64)
472        GetProcessAffinityMask(GetCurrentProcess(), (PDWORD_PTR)&procMask, (PDWORD_PTR)&sysMask);
473#  else
474        GetProcessAffinityMask(GetCurrentProcess(), &procMask, &sysMask);
475#  endif
476
477        // If procMask is 0, consider there is only one core available
478        // (using 0 as procMask will cause an infinite loop below)
479        if (procMask == 0)
480            procMask = 1;
481
482        // if the core specified with coreNr is not available, take the lowest one
483        if (!(procMask & (1 << coreNr)))
484            coreNr = 0;
485
486        // Find the lowest core that this process uses and coreNr suggests
487        DWORD threadMask = 1;
488        while ((threadMask & procMask) == 0 || (threadMask < (1u << coreNr)))
489            threadMask <<= 1;
490
491        // Set affinity to the first core
492        SetThreadAffinityMask(GetCurrentThread(), threadMask);
493#endif
494    }
495
496    /**
497    @brief
498        Compares the executable path with the working directory
499    */
500    void Core::setExecutablePath()
501    {
502#ifdef ORXONOX_PLATFORM_WINDOWS
503        // get executable module
504        TCHAR buffer[1024];
505        if (GetModuleFileName(NULL, buffer, 1024) == 0)
506            ThrowException(General, "Could not retrieve executable path.");
507
508#elif defined(ORXONOX_PLATFORM_APPLE)
509        char buffer[1024];
510        unsigned long path_len = 1023;
511        if (_NSGetExecutablePath(buffer, &path_len))
512            ThrowException(General, "Could not retrieve executable path.");
513
514#else /* Linux */
515        /* written by Nicolai Haehnle <prefect_@gmx.net> */
516
517        /* Get our PID and build the name of the link in /proc */
518        char linkname[64]; /* /proc/<pid>/exe */
519        if (snprintf(linkname, sizeof(linkname), "/proc/%i/exe", getpid()) < 0)
520        {
521            /* This should only happen on large word systems. I'm not sure
522               what the proper response is here.
523               Since it really is an assert-like condition, aborting the
524               program seems to be in order. */
525            assert(false);
526        }
527
528        /* Now read the symbolic link */
529        char buffer[1024];
530        int ret;
531        ret = readlink(linkname, buffer, 1024);
532        /* In case of an error, leave the handling up to the caller */
533        if (ret == -1)
534            ThrowException(General, "Could not retrieve executable path.");
535
536        /* Ensure proper NUL termination */
537        buffer[ret] = 0;
538#endif
539
540        configuration_->executablePath_ = boost::filesystem::path(buffer);
541#ifndef ORXONOX_PLATFORM_APPLE
542        configuration_->executablePath_ = configuration_->executablePath_.branch_path(); // remove executable name
543#endif
544    }
545
546    /**
547    @brief
548        Checks for "orxonox_dev_build.keep_me" in the executable diretory.
549        If found it means that this is not an installed run, hence we
550        don't write the logs and config files to ~/.orxonox
551    @throws
552        GeneralException
553    */
554    void Core::checkDevBuild()
555    {
556        if (boost::filesystem::exists(configuration_->executablePath_ / "orxonox_dev_build.keep_me"))
557        {
558            COUT(1) << "Running from the build tree." << std::endl;
559            Core::bDevRun_ = true;
560            configuration_->dataPath_   = specialConfig::dataDevDirectory;
561            configuration_->externalDataPath_ = specialConfig::externalDataDevDirectory;
562            configuration_->configPath_ = specialConfig::configDevDirectory;
563            configuration_->logPath_    = specialConfig::logDevDirectory;
564        }
565        else
566        {
567#ifdef INSTALL_COPYABLE // --> relative paths
568            // Also set the root path
569            boost::filesystem::path relativeExecutablePath(specialConfig::defaultRuntimePath);
570            configuration_->rootPath_ = configuration_->executablePath_;
571            while (!boost::filesystem::equivalent(configuration_->rootPath_ / relativeExecutablePath, configuration_->executablePath_)
572                   && !configuration_->rootPath_.empty())
573                configuration_->rootPath_ = configuration_->rootPath_.branch_path();
574            if (configuration_->rootPath_.empty())
575                ThrowException(General, "Could not derive a root directory. Might the binary installation directory contain '..' when taken relative to the installation prefix path?");
576
577            // Using paths relative to the install prefix, complete them
578            configuration_->dataPath_   = configuration_->rootPath_ / specialConfig::defaultDataPath;
579            configuration_->configPath_ = configuration_->rootPath_ / specialConfig::defaultConfigPath;
580            configuration_->logPath_    = configuration_->rootPath_ / specialConfig::defaultLogPath;
581#else
582            // There is no root path, so don't set it at all
583
584            configuration_->dataPath_  = specialConfig::dataInstallDirectory;
585
586            // Get user directory
587#  ifdef ORXONOX_PLATFORM_UNIX /* Apple? */
588            char* userDataPathPtr(getenv("HOME"));
589#  else
590            char* userDataPathPtr(getenv("APPDATA"));
591#  endif
592            if (userDataPathPtr == NULL)
593                ThrowException(General, "Could not retrieve user data path.");
594            boost::filesystem::path userDataPath(userDataPathPtr);
595            userDataPath /= ".orxonox";
596
597            configuration_->configPath_ = userDataPath / specialConfig::defaultConfigPath;
598            configuration_->logPath_    = userDataPath / specialConfig::defaultLogPath;
599#endif
600        }
601
602        // Option to put all the config and log files in a separate folder
603        if (!CommandLine::getArgument("writingPathSuffix")->hasDefaultValue())
604        {
605            std::string directory(CommandLine::getValue("writingPathSuffix").getString());
606            configuration_->configPath_ = configuration_->configPath_ / directory;
607            configuration_->logPath_    = configuration_->logPath_    / directory;
608        }
609    }
610
611    /*
612    @brief
613        Checks for the log and the config directory and creates them
614        if necessary. Otherwise me might have problems opening those files.
615    @throws
616        orxonox::GeneralException if the directory to be created is a file.
617    */
618    void Core::createDirectories()
619    {
620        std::vector<std::pair<boost::filesystem::path, std::string> > directories;
621        directories.push_back(std::make_pair(boost::filesystem::path(configuration_->configPath_), "config"));
622        directories.push_back(std::make_pair(boost::filesystem::path(configuration_->logPath_), "log"));
623
624        for (std::vector<std::pair<boost::filesystem::path, std::string> >::iterator it = directories.begin();
625            it != directories.end(); ++it)
626        {
627            if (boost::filesystem::exists(it->first) && !boost::filesystem::is_directory(it->first))
628            {
629                ThrowException(General, std::string("The ") + it->second + " directory has been preoccupied by a file! \
630                                         Please remove " + it->first.string());
631            }
632            if (boost::filesystem::create_directories(it->first)) // function may not return true at all (bug?)
633            {
634                COUT(4) << "Created " << it->second << " directory" << std::endl;
635            }
636        }
637    }
638
639    void Core::preUpdate(const Clock& time)
640    {
641        if (this->bGraphicsLoaded_)
642        {
643            // process input events
644            this->inputManager_->update(time);
645            // process gui events
646            this->guiManager_->update(time);
647        }
648        // process thread commands
649        this->tclThreadManager_->update(time);
650    }
651
652    void Core::postUpdate(const Clock& time)
653    {
654        if (this->bGraphicsLoaded_)
655        {
656            // Render (doesn't throw)
657            this->graphicsManager_->update(time);
658        }
659    }
660}
Note: See TracBrowser for help on using the repository browser.