From d4aea052469604c8e05a3825a614266decf6ab27 Mon Sep 17 00:00:00 2001 From: david Date: Fri, 28 Dec 2001 23:33:32 +0000 Subject: [PATCH] Substantial rewrite of FGNewMat, the material class. Most of the material-specific logic is now removed from the material library and encapsulated in the material class itself, and materials are loaded from $FG_ROOT/materials.xml rather than $FG_ROOT/materials. This also removes a nasty bug in the old material-loading code that caused a floating-point exception. --- src/Main/fg_init.cxx | 2 +- src/Objects/matlib.cxx | 168 ++++++------------------------ src/Objects/matlib.hxx | 5 +- src/Objects/newmat.cxx | 199 ++++++++++++++++++----------------- src/Objects/newmat.hxx | 230 ++++++++++++++++++++++++++--------------- 5 files changed, 292 insertions(+), 312 deletions(-) diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index 15c406282..ec6039b60 100644 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -529,7 +529,7 @@ bool fgInitSubsystems( void ) { //////////////////////////////////////////////////////////////////// SGPath mpath( globals->get_fg_root() ); - mpath.append( "materials" ); + mpath.append( "materials.xml" ); if ( material_lib.load( mpath.str() ) ) { } else { SG_LOG( SG_GENERAL, SG_ALERT, "Error loading material lib!" ); diff --git a/src/Objects/matlib.cxx b/src/Objects/matlib.cxx index 9d110bd69..9c0ec1e59 100644 --- a/src/Objects/matlib.cxx +++ b/src/Objects/matlib.cxx @@ -37,6 +37,7 @@ #include #include +#include #include #include STL_STRING @@ -65,105 +66,37 @@ FGMaterialLib::FGMaterialLib ( void ) { } -static bool local_file_exists( const string& path ) { - sg_gzifstream in( path ); - if ( ! in.is_open() ) { - return false; - } else { - return true; - } -} - - // Load a library of material properties bool FGMaterialLib::load( const string& mpath ) { - string material_name; - sg_gzifstream in( mpath ); - if ( ! in.is_open() ) { - SG_LOG( SG_GENERAL, SG_ALERT, "Cannot open file: " << mpath ); - exit(-1); - } - -#ifndef __MWERKS__ - while ( ! in.eof() ) { -#else - char c = '\0'; - while ( in.get(c) && c != '\0' ) { - in.putback(c); -#endif - // printf("%s", line); - - // strip leading white space and comments - in >> skipcomment; - - // set to zero to prevent its value accidently being '{' - // after a failed >> operation. - char token = 0; - - in >> material_name; - - if ( material_name == "alias" ) { - string src_mat, dst_mat; - in >> dst_mat >> src_mat; - SG_LOG( SG_GENERAL, SG_INFO, " Material alias: " << dst_mat << - " mapped to " << src_mat ); - FGNewMat *m = matlib[src_mat]; - if ( m != NULL ) { - matlib[dst_mat] = m; - m->ref(); - } else { - SG_LOG( SG_GENERAL, SG_ALERT, - "Bad material alias pointing to nonexistant material" ); - } - } else { - in >> token; - - if ( token == '{' ) { - // Read the data into a temporary but stack allocated - // copy of the structure - FGNewMat tmp; - in >> tmp; - - // create a pointer to a heap allocated copy of the structure - FGNewMat *m = new FGNewMat; - *m = tmp; - m->ref(); - - // build the ssgSimpleState - SGPath tex_path( globals->get_fg_root() ); - tex_path.append( "Textures.high" ); - tex_path.append( m->get_texture_name() ); - if ( ! local_file_exists(tex_path.str()) - || general.get_glMaxTexSize() < 512 ) { - tex_path = SGPath( globals->get_fg_root() ); - tex_path.append( "Textures" ); - tex_path.append( m->get_texture_name() ); - } - - SG_LOG( SG_TERRAIN, SG_INFO, " Loading material " - << material_name << " (" << tex_path.c_str() << ")"); - - GLenum shade_model = GL_SMOOTH; - if ( fgGetBool("/sim/rendering/shading") ) { - shade_model = GL_SMOOTH; - } else { - shade_model = GL_FLAT; - } - - m->set_texture_name( tex_path.str() ); - m->build_ssg_state( shade_model, - fgGetBool("/sim/rendering/textures"), - false ); - -#if EXTRA_DEBUG - m->dump_info(); -#endif - - matlib[material_name] = m; - } - } + SGPropertyNode materials; + + cout << "Reading materials from " << mpath << endl; + try { + readProperties(mpath, &materials); + } catch (const sg_exception &ex) { + SG_LOG(SG_INPUT, SG_ALERT, "Error reading materials: " << ex.getMessage()); + throw ex; + } + + int nMaterials = materials.nChildren(); + for (int i = 0; i < nMaterials; i++) { + const SGPropertyNode * node = materials.getChild(i); + if (node->getName() == "material") { + FGNewMat * m = new FGNewMat(node); + + vectornames = node->getChildren("name"); + for (int j = 0; j < names.size(); j++) { + m->ref(); + matlib[names[j]->getStringValue()] = m; + SG_LOG( SG_TERRAIN, SG_INFO, " Loading material " + << names[j]->getStringValue()); + } + } else { + SG_LOG(SG_INPUT, SG_ALERT, + "Skipping bad material entry " << node->getName()); } + } // hard coded light state ssgSimpleState *lights = new ssgSimpleState; @@ -178,9 +111,7 @@ bool FGMaterialLib::load( const string& mpath ) { lights->disable( GL_ALPHA_TEST ); lights->disable( GL_LIGHTING ); - FGNewMat *m = new FGNewMat; - m->set_ssg_state( lights ); - matlib["LIGHTS"] = m; + matlib["LIGHTS"] = new FGNewMat(lights); return true; } @@ -204,27 +135,10 @@ bool FGMaterialLib::add_item ( const string &mat_name, const string &full_path ) string tex_name = full_path.substr( pos + 1 ); string tex_path = full_path.substr( 0, pos ); - FGNewMat *m = new FGNewMat( mat_name, full_path ); - SG_LOG( SG_TERRAIN, SG_INFO, " Loading material " << mat_name << " (" << full_path << ")"); -#if EXTRA_DEBUG - m->dump_info(); -#endif - - GLenum shade_model = GL_SMOOTH; - if ( fgGetBool("/sim/rendering/shading") ) { - shade_model = GL_SMOOTH; - } else { - shade_model = GL_FLAT; - } - - m->build_ssg_state( shade_model, - fgGetBool("/sim/rendering/textures"), - true ); - - material_lib.matlib[mat_name] = m; + material_lib.matlib[mat_name] = new FGNewMat(full_path); return true; } @@ -233,16 +147,11 @@ bool FGMaterialLib::add_item ( const string &mat_name, const string &full_path ) // Load a library of material properties bool FGMaterialLib::add_item ( const string &mat_name, ssgSimpleState *state ) { - FGNewMat *m = new FGNewMat( mat_name ); - m->set_ssg_state( state ); + FGNewMat *m = new FGNewMat(state); SG_LOG( SG_TERRAIN, SG_INFO, " Loading material given a premade " << "ssgSimpleState = " << mat_name ); -#if EXTRA_DEBUG - m->dump_info(); -#endif - material_lib.matlib[mat_name] = m; return true; @@ -304,18 +213,7 @@ void FGMaterialLib::load_next_deferred() { for ( material_map_iterator it = begin(); it != end(); it++ ) { const string &key = it->first; FGNewMat *slot = it->second; - // SG_LOG( SG_GENERAL, SG_INFO, "slot = " << slot ); - if ( ! slot->get_texture_loaded() ) { - SG_LOG( SG_GENERAL, SG_INFO, "Loading deferred texture for " - << key ); -#ifdef PLIB_1_2_X - slot->get_textured()-> - setTexture( (char *)slot->get_texture_name_c_str(), 0, 0 ); -#else - slot->get_textured()-> - setTexture( (char *)slot->get_texture_name_c_str(), 0, 0, 1 ); -#endif - slot->set_texture_loaded( true ); - } + if (slot->load_texture()) + return; } } diff --git a/src/Objects/matlib.hxx b/src/Objects/matlib.hxx index cfdca473c..ee1f65017 100644 --- a/src/Objects/matlib.hxx +++ b/src/Objects/matlib.hxx @@ -88,8 +88,9 @@ public: void set_step (int step); int get_step (); - // Load one pending "deferred" texture. Return true if a texture - // loaded successfully, false if no pending, or error. + /** + * Load the next deferred texture, if there is any. + */ void load_next_deferred(); material_map_iterator begin() { return matlib.begin(); } diff --git a/src/Objects/newmat.cxx b/src/Objects/newmat.cxx index 451b344ef..39da01c39 100644 --- a/src/Objects/newmat.cxx +++ b/src/Objects/newmat.cxx @@ -35,46 +35,87 @@ #include #include +#include
+#include
+ #include "newmat.hxx" -// Constructor -FGNewMat::FGNewMat ( void ) { - wrapu = wrapv = 1; - mipmap = 1; - light_coverage = -1.0; - refcount = 0; +static bool +local_file_exists( const string& path ) { + sg_gzifstream in( path ); + if ( ! in.is_open() ) { + return false; + } else { + return true; + } } // Constructor -FGNewMat::FGNewMat ( const string &name ) + +FGNewMat::FGNewMat () + : texture_path(""), + state(0), + textured(0), + nontextured(0), + alpha(false), + xsize(0), + ysize(0), + wrapu(true), + wrapv(true), + mipmap(true), + texture_loaded(false), + refcount(0) +{ + for (int i = 0; i < 4; i++) + ambient[i] = diffuse[i] = specular[i] = emission[i] = 0.0; +} + +FGNewMat::FGNewMat (const SGPropertyNode * props) { - FGNewMat( name, name ); + FGNewMat(); + read_properties(props); + build_ssg_state(false); } +FGNewMat::FGNewMat (const string &texture_path) +{ + FGNewMat(); + build_ssg_state(true); +} -// Constructor -FGNewMat::FGNewMat ( const string &mat_name, const string &tex_name ) +FGNewMat::FGNewMat (ssgSimpleState * s) +{ + FGNewMat(); + set_ssg_state(s); +} + + +bool +FGNewMat::load_texture () { - material_name = mat_name; - texture_name = tex_name; - xsize = ysize = 0; - wrapu = wrapv = 1; - mipmap = 1; - alpha = 0; - ambient[0] = ambient[1] = ambient[2] = ambient[3] = 1.0; - diffuse[0] = diffuse[1] = diffuse[2] = diffuse[3] = 1.0; - specular[0] = specular[1] = specular[2] = specular[3] = 1.0; - emission[0] = emission[1] = emission[2] = emission[3] = 1.0; - light_coverage = -1.0; - refcount = 0; + if (texture_loaded) { + return false; + } else { + SG_LOG( SG_GENERAL, SG_INFO, "Loading deferred texture " << texture_path ); +#ifdef PLIB_1_2_X + textured->setTexture((char *)texture_path.c_str(), wrapu, wrapv ); +#else + textured->setTexture((char *)texture_path.c_str(), wrapu, wrapv, mipmap ); +#endif + texture_loaded = true; + return true; + } } -void FGNewMat::build_ssg_state( GLenum shade_model, bool texture_default, - bool defer_tex_load ) +void FGNewMat::build_ssg_state( bool defer_tex_load ) { + GLenum shade_model = + (fgGetBool("/sim/rendering/shading") ? GL_SMOOTH : GL_FLAT); + bool texture_default = fgGetBool("/sim/rendering/textures"); + state = new ssgStateSelector(2); state->ref(); @@ -92,12 +133,11 @@ void FGNewMat::build_ssg_state( GLenum shade_model, bool texture_default, textured->disable( GL_BLEND ); textured->disable( GL_ALPHA_TEST ); if ( !defer_tex_load ) { - textured->setTexture( (char *)texture_name.c_str(), wrapu, wrapv ); + textured->setTexture( (char *)texture_path.c_str(), wrapu, wrapv ); texture_loaded = true; } else { texture_loaded = false; } - // cout << "wrap u = " << wrapu << " wrapv = " << wrapv << endl; textured->enable( GL_COLOR_MATERIAL ); textured->setColourMaterial( GL_AMBIENT_AND_DIFFUSE ); textured->setMaterial( GL_EMISSION, 0, 0, 0, 1 ); @@ -112,8 +152,6 @@ void FGNewMat::build_ssg_state( GLenum shade_model, bool texture_default, nontextured->disable( GL_ALPHA_TEST ); nontextured->disable( GL_COLOR_MATERIAL ); - /* cout << "ambient = " << ambient[0] << "," << ambient[1] - << "," << ambient[2] << endl; */ nontextured->setMaterial ( GL_AMBIENT, ambient[0], ambient[1], ambient[2], ambient[3] ) ; @@ -180,75 +218,50 @@ void FGNewMat::set_ssg_state( ssgSimpleState *s ) { } -void FGNewMat::dump_info () { - SG_LOG( SG_TERRAIN, SG_INFO, "{" << endl << " texture = " - << texture_name ); - SG_LOG( SG_TERRAIN, SG_INFO, " xsize = " << xsize ); - SG_LOG( SG_TERRAIN, SG_INFO, " ysize = " << ysize ); - SG_LOG( SG_TERRAIN, SG_INFO, " ambient = " << ambient[0] << " " - << ambient[1] <<" "<< ambient[2] <<" "<< ambient[3] ); - SG_LOG( SG_TERRAIN, SG_INFO, " diffuse = " << diffuse[0] << " " - << diffuse[1] << " " << diffuse[2] << " " << diffuse[3] ); - SG_LOG( SG_TERRAIN, SG_INFO, " specular = " << specular[0] << " " - << specular[1] << " " << specular[2] << " " << specular[3]); - SG_LOG( SG_TERRAIN, SG_INFO, " emission = " << emission[0] << " " - << emission[1] << " " << emission[2] << " " << emission[3]); - SG_LOG( SG_TERRAIN, SG_INFO, " alpha = " << alpha << endl <<"}" ); - -} - - // Destructor FGNewMat::~FGNewMat ( void ) { } -istream& -operator >> ( istream& in, FGNewMat& m ) +void +FGNewMat::read_properties (const SGPropertyNode * props) { - string token; - - for (;;) { - in >> token; - if ( token == "texture" ) { - in >> token >> m.texture_name; - } else if ( token == "xsize" ) { - in >> token >> m.xsize; - } else if ( token == "ysize" ) { - in >> token >> m.ysize; - } else if ( token == "wrapu" ) { - in >> token >> m.wrapu; - } else if ( token == "wrapv" ) { - in >> token >> m.wrapv; - } else if ( token == "mipmap" ) { - in >> token >> m.mipmap; - } else if ( token == "ambient" ) { - in >> token >> m.ambient[0] >> m.ambient[1] - >> m.ambient[2] >> m.ambient[3]; - } else if ( token == "diffuse" ) { - in >> token >> m.diffuse[0] >> m.diffuse[1] - >> m.diffuse[2] >> m.diffuse[3]; - } else if ( token == "specular" ) { - in >> token >> m.specular[0] >> m.specular[1] - >> m.specular[2] >> m.specular[3]; - } else if ( token == "emission" ) { - in >> token >> m.emission[0] >> m.emission[1] - >> m.emission[2] >> m.emission[3]; - } else if ( token == "alpha" ) { - in >> token >> token; - if ( token == "yes" ) { - m.alpha = 1; - } else if ( token == "no" ) { - m.alpha = 0; - } else { - SG_LOG( SG_TERRAIN, SG_INFO, "Bad alpha value " << token ); - } - } else if ( token == "light-coverage" ) { - in >> token >> m.light_coverage; - } else if ( token[0] == '}' ) { - break; - } - } - - return in; + // Get the path to the texture + string tname = props->getStringValue("texture", "unknown.rgb"); + SGPath tpath(globals->get_fg_root()); + tpath.append("Textures.high"); + tpath.append(tname); + if (!local_file_exists(tpath.str())) { + tpath = SGPath(globals->get_fg_root()); + tpath.append("Textures"); + tpath.append(tname); + } + texture_path = tpath.str(); + + xsize = props->getDoubleValue("xsize", 0.0); + ysize = props->getDoubleValue("ysize", 0.0); + wrapu = props->getBoolValue("wrapu", true); + wrapv = props->getBoolValue("wrapv", true); + mipmap = props->getBoolValue("mipmap", true); + light_coverage = props->getDoubleValue("light-coverage"); + + ambient[0] = props->getDoubleValue("ambient/r", 0.0); + ambient[1] = props->getDoubleValue("ambient/g", 0.0); + ambient[2] = props->getDoubleValue("ambient/b", 0.0); + ambient[3] = props->getDoubleValue("ambient/a", 0.0); + + diffuse[0] = props->getDoubleValue("diffuse/r", 0.0); + diffuse[1] = props->getDoubleValue("diffuse/g", 0.0); + diffuse[2] = props->getDoubleValue("diffuse/b", 0.0); + diffuse[3] = props->getDoubleValue("diffuse/a", 0.0); + + specular[0] = props->getDoubleValue("specular/r", 0.0); + specular[1] = props->getDoubleValue("specular/g", 0.0); + specular[2] = props->getDoubleValue("specular/b", 0.0); + specular[3] = props->getDoubleValue("specular/a", 0.0); + + emission[0] = props->getDoubleValue("emissive/r", 0.0); + emission[1] = props->getDoubleValue("emissive/g", 0.0); + emission[2] = props->getDoubleValue("emissive/b", 0.0); + emission[3] = props->getDoubleValue("emissive/a", 0.0); } diff --git a/src/Objects/newmat.hxx b/src/Objects/newmat.hxx index 72a92ecc3..28199619e 100644 --- a/src/Objects/newmat.hxx +++ b/src/Objects/newmat.hxx @@ -1,6 +1,8 @@ -// newmat.hxx -- class to handle material properties +// newmat.hxx -- a material in the scene graph. +// TODO: this class needs to be renamed. // // Written by Curtis Olson, started May 1998. +// Overhauled by David Megginson, December 2001 // // Copyright (C) 1998 - 2000 Curtis L. Olson - curt@flightgear.org // @@ -24,7 +26,6 @@ #ifndef _NEWMAT_HXX #define _NEWMAT_HXX - #ifndef __cplusplus # error This library requires C++ #endif @@ -41,6 +42,7 @@ #include #include +#include #include @@ -49,113 +51,179 @@ SG_USING_STD(string); -// MSVC++ 6.0 kuldge - Need forward declaration of friends. -class FGNewMat; -istream& operator >> ( istream& in, FGNewMat& m ); - -// Material property class +/** + * A material in the scene graph. + * + * A material represents information about a single surface type + * in the 3D scene graph, including texture, colour, lighting, + * tiling, and so on; most of the materials in FlightGear are + * defined in the $FG_ROOT/materials.xml file, and can be changed + * at runtime. + */ class FGNewMat { -private: +public: - // names - string material_name; - string texture_name; + + //////////////////////////////////////////////////////////////////// + // Public Constructors. + //////////////////////////////////////////////////////////////////// - // pointers to ssg states - ssgStateSelector *state; - ssgSimpleState *textured; - ssgSimpleState *nontextured; + /** + * Construct a material from a set of properties. + * + * @param props A property node containing subnodes with the + * state information for the material. This node is usually + * loaded from the $FG_ROOT/materials.xml file. + */ + FGNewMat (const SGPropertyNode * props); - // alpha texture? - int alpha; - // texture size - double xsize, ysize; + /** + * Construct a material from an absolute texture path. + * + * @param texture_path A string containing an absolute path + * to a texture file (usually RGB). + */ + FGNewMat (const string &texture_path); - // wrap texture? - int wrapu, wrapv; - // use mipmapping? - int mipmap; + /** + * Construct a material around an existing SSG state. + * + * This constructor allows the application to create a custom, + * low-level state for the scene graph and wrap a material around + * it. Note: the pointer ownership is transferred to the material. + * + * @param s The SSG state for this material. + */ + FGNewMat (ssgSimpleState * s); - // coverage of night lighting. This number is specifically the - // amount of area coverage we give a single light. The size of a - // triangle is divided by this number and that is the number of - // lights assigned to that triangle. Lower numbers mean more - // dense light ocverage. - double light_coverage; + /** + * Destructor. + */ + virtual ~FGNewMat ( void ); - // material properties - sgVec4 ambient, diffuse, specular, emission; - // true if texture loading deferred, and not yet loaded - bool texture_loaded; + + //////////////////////////////////////////////////////////////////// + // Public methods. + //////////////////////////////////////////////////////////////////// - // ref count so we can properly delete if we have multiple - // pointers to this record - int refcount; + /** + * Force the texture to load if it hasn't already. + * + * @return true if the texture loaded, false if it was loaded + * already. + */ + virtual bool load_texture (); -public: - // Constructor - FGNewMat ( void ); - FGNewMat ( const string& name ); - FGNewMat ( const string &mat_name, const string &tex_name ); + /** + * Get the textured state. + */ + virtual inline ssgSimpleState *get_textured () { return textured; } - // Destructor - ~FGNewMat ( void ); - friend istream& operator >> ( istream& in, FGNewMat& m ); + /** + * Get the xsize of the texture, in meters. + */ + virtual inline double get_xsize() const { return xsize; } - // void load_texture( const string& root ); - void build_ssg_state( GLenum shade_model, bool texture_default, - bool defer_tex_load = false ); - void set_ssg_state( ssgSimpleState *s ); - inline string get_material_name() const { return material_name; } - inline void set_material_name( const string& n ) { material_name = n; } + /** + * Get the ysize of the texture, in meters. + */ + virtual inline double get_ysize() const { return ysize; } - inline string get_texture_name() const { return texture_name; } - inline const char *get_texture_name_c_str() const { - return texture_name.c_str(); - } - inline void set_texture_name( const string& n ) { texture_name = n; } - inline ssgSimpleState *get_textured() { return textured; } + /** + * Get the light coverage. + * + * A smaller number means more generated night lighting. + * + * @return The area (m^2?) covered by each light. + */ + virtual inline double get_light_coverage () const { return light_coverage; } - inline double get_xsize() const { return xsize; } - inline double get_ysize() const { return ysize; } - inline void set_xsize( double x ) { xsize = x; } - inline void set_ysize( double y ) { ysize = y; } - inline float *get_ambient() { return ambient; } - inline float *get_diffuse() { return diffuse; } - inline float *get_specular() { return specular; } - inline float *get_emission() { return emission; } - inline void set_ambient( sgVec4 a ) { sgCopyVec4( ambient, a ); } - inline void set_diffuse( sgVec4 d ) { sgCopyVec4( diffuse, d ); } - inline void set_specular( sgVec4 s ) { sgCopyVec4( specular, s ); } - inline void set_emission( sgVec4 e ) { sgCopyVec4( emission, e ); } + /** + * Get the current state. + */ + virtual inline ssgStateSelector *get_state() const { return state; } - inline bool get_texture_loaded() const { return texture_loaded; } - inline void set_texture_loaded( bool val ) { texture_loaded = val; } - inline double get_light_coverage () const { return light_coverage; } - inline void set_light_coverage (double coverage) { - light_coverage = coverage; - } + /** + * Add a reference to the texture. + */ + virtual inline void ref() { refcount++; } - inline ssgStateSelector *get_state() const { return state; } - inline void ref() { refcount++; } - inline void deRef() { refcount--; } - inline int getRef() const { return refcount; } + /** + * Remove a reference from the texture. + */ + virtual inline void deRef() { refcount--; } - void dump_info(); -}; + /** + * Get the number of references to the texture. + */ + virtual inline int getRef() const { return refcount; } -#endif // _NEWMAT_HXX +private: + + + //////////////////////////////////////////////////////////////////// + // Internal state. + //////////////////////////////////////////////////////////////////// + + // names + string texture_path; + + // pointers to ssg states + ssgStateSelector *state; + ssgSimpleState *textured; + ssgSimpleState *nontextured; + + // alpha texture? + bool alpha; + + // texture size + double xsize, ysize; + + // wrap texture? + bool wrapu, wrapv; + // use mipmapping? + int mipmap; + + // coverage of night lighting. + double light_coverage; + + // material properties + sgVec4 ambient, diffuse, specular, emission; + + // true if texture loading deferred, and not yet loaded + bool texture_loaded; + + // ref count so we can properly delete if we have multiple + // pointers to this record + int refcount; + + + + //////////////////////////////////////////////////////////////////// + // Internal constructors and methods. + //////////////////////////////////////////////////////////////////// + + FGNewMat (); + FGNewMat (const FGNewMat &mat); // unimplemented + + void read_properties (const SGPropertyNode * props); + void build_ssg_state(bool defer_tex_load = false); + void set_ssg_state( ssgSimpleState *s ); + +}; + +#endif // _NEWMAT_HXX -- 2.39.5