//========= Copyright � 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: // // $NoKeywords: $ // //=============================================================================// // nav_file.cpp // Reading and writing nav files // Author: Michael S. Booth (mike@turtlerockstudios.com), January-September 2003 #include "cbase.h" #include "nav_mesh.h" #include "gamerules.h" #include "datacache/imdlcache.h" #ifdef TERROR #include "func_elevator.h" #endif #include "tier1/lzmaDecoder.h" #ifdef CSTRIKE_DLL #include "cs_shareddefs.h" #include "nav_pathfind.h" #include "cs_nav_area.h" #endif // NOTE: This has to be the last file included! #include "tier0/memdbgon.h" //-------------------------------------------------------------------------------------------------------------- /// The current version of the nav file format /// IMPORTANT: If this version changes, the swap function in makegamedata /// must be updated to match. If not, this will break the Xbox 360. // TODO: Was changed from 15, update when latest 360 code is integrated (MSB 5/5/09) const int NavCurrentVersion = 16; //-------------------------------------------------------------------------------------------------------------- // // The 'place directory' is used to save and load places from // nav files in a size-efficient manner that also allows for the // order of the place ID's to change without invalidating the // nav files. // // The place directory is stored in the nav file as a list of // place name strings. Each nav area then contains an index // into that directory, or zero if no place has been assigned to // that area. // PlaceDirectory::PlaceDirectory( void ) { Reset(); } void PlaceDirectory::Reset( void ) { m_directory.RemoveAll(); m_hasUnnamedAreas = false; } /// return true if this place is already in the directory bool PlaceDirectory::IsKnown( Place place ) const { return m_directory.HasElement( place ); } /// return the directory index corresponding to this Place (0 = no entry) PlaceDirectory::IndexType PlaceDirectory::GetIndex( Place place ) const { if (place == UNDEFINED_PLACE) return 0; int i = m_directory.Find( place ); if (i < 0) { AssertMsg( false, "PlaceDirectory::GetIndex failure" ); return 0; } return (IndexType)(i+1); } /// add the place to the directory if not already known void PlaceDirectory::AddPlace( Place place ) { if (place == UNDEFINED_PLACE) { m_hasUnnamedAreas = true; return; } Assert( place < 1000 ); if (IsKnown( place )) return; m_directory.AddToTail( place ); } /// given an index, return the Place Place PlaceDirectory::IndexToPlace( IndexType entry ) const { if (entry == 0) return UNDEFINED_PLACE; int i = entry-1; if (i >= m_directory.Count()) { AssertMsg( false, "PlaceDirectory::IndexToPlace: Invalid entry" ); return UNDEFINED_PLACE; } return m_directory[ i ]; } /// store the directory void PlaceDirectory::Save( CUtlBuffer &fileBuffer ) { // store number of entries in directory IndexType count = (IndexType)m_directory.Count(); fileBuffer.PutUnsignedShort( count ); // store entries for( int i=0; iPlaceToName( m_directory[i] ); // store string length followed by string itself unsigned short len = (unsigned short)(strlen( placeName ) + 1); fileBuffer.PutUnsignedShort( len ); fileBuffer.Put( placeName, len ); } fileBuffer.PutUnsignedChar( m_hasUnnamedAreas ); } /// load the directory void PlaceDirectory::Load( CUtlBuffer &fileBuffer, int version ) { // read number of entries IndexType count = fileBuffer.GetUnsignedShort(); m_directory.RemoveAll(); // read each entry char placeName[256]; unsigned short len; for( int i=0; iNameToPlace( placeName ); if (place == UNDEFINED_PLACE) { Warning( "Warning: NavMesh place %s is undefined?\n", placeName ); } AddPlace( place ); } if ( version > 11 ) { m_hasUnnamedAreas = fileBuffer.GetUnsignedChar() != 0; } } PlaceDirectory placeDirectory; #define FORMAT_BSPFILE "maps\\%s" PLATFORM_EXT ".bsp" #define FORMAT_NAVFILE "maps\\%s" PLATFORM_EXT ".nav" //-------------------------------------------------------------------------------------------------------------- /** * Replace extension with "bsp" */ char *GetBspFilename( const char *navFilename ) { static char bspFilename[256]; Q_snprintf( bspFilename, sizeof( bspFilename ), FORMAT_BSPFILE, STRING( gpGlobals->mapname ) ); int len = strlen( bspFilename ); if (len < 3) return NULL; bspFilename[ len-3 ] = 'b'; bspFilename[ len-2 ] = 's'; bspFilename[ len-1 ] = 'p'; return bspFilename; } unsigned char CNavArea::GetSavedHidingSpotCount( void ) const { unsigned char count = 0; FOR_EACH_VEC( m_hidingSpots, i ) { if ( count == 0xff ) break; if ( m_hidingSpots[ i ]->IsSaved() ) count++; } return count; } //-------------------------------------------------------------------------------------------------------------- /** * Save a navigation area to the opened binary stream */ void CNavArea::Save( CUtlBuffer &fileBuffer, unsigned int version ) const { // save ID fileBuffer.PutUnsignedInt( m_id ); // save attribute flags fileBuffer.PutInt( m_attributeFlags ); // save extent of area fileBuffer.Put( &m_nwCorner, 3*sizeof(float) ); fileBuffer.Put( &m_seCorner, 3*sizeof(float) ); // save heights of implicit corners fileBuffer.PutFloat( m_neZ ); fileBuffer.PutFloat( m_swZ ); // save connections to adjacent areas // in the enum order NORTH, EAST, SOUTH, WEST for( int d=0; dm_id ); } } // // Store hiding spots for this area // unsigned char count = GetSavedHidingSpotCount(); if ( count > 255 ) { count = 255; Warning( "Warning: NavArea #%d: Truncated hiding spot list to 255\n", m_id ); } fileBuffer.PutUnsignedChar( count ); // store HidingSpot objects unsigned int saveCount = 0; FOR_EACH_VEC( m_hidingSpots, hit ) { HidingSpot *spot = m_hidingSpots[ hit ]; spot->Save( fileBuffer, version ); // overflow check if (++saveCount == count) break; } // // Save encounter spots for this area // { // save number of encounter paths for this area unsigned int count = m_spotEncounters.Count(); fileBuffer.PutUnsignedInt( count ); SpotEncounter *e; FOR_EACH_VEC( m_spotEncounters, it ) { e = m_spotEncounters[ it ]; if (e->from.area) fileBuffer.PutUnsignedInt( e->from.area->m_id ); else fileBuffer.PutUnsignedInt( 0 ); unsigned char dir = (unsigned char)e->fromDir; fileBuffer.PutUnsignedChar( dir ); if (e->to.area) fileBuffer.PutUnsignedInt( e->to.area->m_id ); else fileBuffer.PutUnsignedInt( 0 ); dir = (unsigned char)e->toDir; fileBuffer.PutUnsignedChar( dir ); // write list of spots along this path unsigned char spotCount; if (e->spots.Count() > 255) { spotCount = 255; Warning( "Warning: NavArea #%d: Truncated encounter spot list to 255\n", m_id ); } else { spotCount = (unsigned char)e->spots.Count(); } fileBuffer.PutUnsignedChar( spotCount ); saveCount = 0; FOR_EACH_VEC( e->spots, sit ) { SpotOrder *order = &e->spots[ sit ]; // order->spot may be NULL if we've loaded a nav mesh that has been edited but not re-analyzed unsigned int id = (order->spot) ? order->spot->GetID() : 0; fileBuffer.PutUnsignedInt( id ); unsigned char t = (unsigned char)(255 * order->t); fileBuffer.PutUnsignedChar( t ); // overflow check if (++saveCount == spotCount) break; } } } // store place dictionary entry PlaceDirectory::IndexType entry = placeDirectory.GetIndex( GetPlace() ); fileBuffer.Put( &entry, sizeof(entry) ); // write out ladder info int i; for ( i=0; iGetID(); fileBuffer.PutUnsignedInt( id ); } } // save earliest occupy times for( i=0; iGetID() : 0; fileBuffer.PutUnsignedInt( id ); fileBuffer.PutUnsignedChar( m_potentiallyVisibleAreas[ vit ].attributes ); } // store area we inherit visibility from unsigned int id = ( m_inheritVisibilityFrom.area ) ? m_inheritVisibilityFrom.area->GetID() : 0; fileBuffer.PutUnsignedInt( id ); } //-------------------------------------------------------------------------------------------------------------- /** * Load a navigation area from the file */ NavErrorType CNavArea::Load( CUtlBuffer &fileBuffer, unsigned int version, unsigned int subVersion ) { // load ID m_id = fileBuffer.GetUnsignedInt(); // update nextID to avoid collisions if (m_id >= m_nextID) m_nextID = m_id+1; // load attribute flags if ( version <= 8 ) { m_attributeFlags = fileBuffer.GetUnsignedChar(); } else if ( version < 13 ) { m_attributeFlags = fileBuffer.GetUnsignedShort(); } else { m_attributeFlags = fileBuffer.GetInt(); } // load extent of area fileBuffer.Get( &m_nwCorner, 3*sizeof(float) ); fileBuffer.Get( &m_seCorner, 3*sizeof(float) ); if ( ( m_seCorner.x - m_nwCorner.x ) > 0.0f && ( m_seCorner.y - m_nwCorner.y ) > 0.0f ) { m_invDxCorners = 1.0f / ( m_seCorner.x - m_nwCorner.x ); m_invDyCorners = 1.0f / ( m_seCorner.y - m_nwCorner.y ); } else { m_invDxCorners = m_invDyCorners = 0; DevWarning( "Degenerate Navigation Area #%d at setpos %g %g %g\n", m_id, m_nwCorner.x, m_nwCorner.y, m_nwCorner.z ); } // load heights of implicit corners m_neZ = fileBuffer.GetFloat(); m_swZ = fileBuffer.GetFloat(); CheckWaterLevel(); // load connections (IDs) to adjacent areas // in the enum order NORTH, EAST, SOUTH, WEST for( int d=0; dCreateHidingSpot(); spot->SetPosition( pos ); spot->SetFlags( HidingSpot::IN_COVER ); m_hidingSpots.AddToTail( spot ); } } else { // load HidingSpot objects for this area for( int h=0; hCreateHidingSpot(); spot->Load( fileBuffer, version ); m_hidingSpots.AddToTail( spot ); } } if ( version < 15 ) { // // Eat the approach areas // int nToEat = fileBuffer.GetUnsignedChar(); // load approach area info (IDs) for( int a=0; afrom.id = fileBuffer.GetUnsignedInt(); unsigned char dir = fileBuffer.GetUnsignedChar(); encounter->fromDir = static_cast( dir ); encounter->to.id = fileBuffer.GetUnsignedInt(); dir = fileBuffer.GetUnsignedChar(); encounter->toDir = static_cast( dir ); // read list of spots along this path unsigned char spotCount = fileBuffer.GetUnsignedChar(); SpotOrder order; for( int s=0; sspots.AddToTail( order ); } m_spotEncounters.AddToTail( encounter ); } if (version < 5) return NAV_OK; // // Load Place data // PlaceDirectory::IndexType entry = fileBuffer.GetUnsignedShort(); // convert entry to actual Place SetPlace( placeDirectory.IndexToPlace( entry ) ); if ( version < 7 ) return NAV_OK; // load ladder data for ( int dir=0; dirGetLadders().Find( connect.ladder ) == TheNavMesh->GetLadders().InvalidIndex() ) { connect.ladder = TheNavMesh->GetLadderByID( id ); } if (id && connect.ladder == NULL) { Msg( "CNavArea::PostLoad: Corrupt navigation ladder data. Cannot connect Navigation Areas.\n" ); error = NAV_CORRUPT_DATA; } } } // connect areas together for( int d=0; did; connect->area = TheNavMesh->GetNavAreaByID( id ); if (id && connect->area == NULL) { Msg( "CNavArea::PostLoad: Corrupt navigation data. Cannot connect Navigation Areas.\n" ); error = NAV_CORRUPT_DATA; } connect->length = ( connect->area->GetCenter() - GetCenter() ).Length(); } } // resolve spot encounter IDs SpotEncounter *e; FOR_EACH_VEC( m_spotEncounters, it ) { e = m_spotEncounters[ it ]; e->from.area = TheNavMesh->GetNavAreaByID( e->from.id ); if (e->from.area == NULL) { Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing \"from\" Navigation Area for Encounter Spot.\n" ); error = NAV_CORRUPT_DATA; } e->to.area = TheNavMesh->GetNavAreaByID( e->to.id ); if (e->to.area == NULL) { Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing \"to\" Navigation Area for Encounter Spot.\n" ); error = NAV_CORRUPT_DATA; } if (e->from.area && e->to.area) { // compute path float halfWidth; ComputePortal( e->to.area, e->toDir, &e->path.to, &halfWidth ); ComputePortal( e->from.area, e->fromDir, &e->path.from, &halfWidth ); const float eyeHeight = HalfHumanHeight; e->path.from.z = e->from.area->GetZ( e->path.from ) + eyeHeight; e->path.to.z = e->to.area->GetZ( e->path.to ) + eyeHeight; } // resolve HidingSpot IDs FOR_EACH_VEC( e->spots, sit ) { SpotOrder *order = &e->spots[ sit ]; order->spot = GetHidingSpotByID( order->id ); if (order->spot == NULL) { Msg( "CNavArea::PostLoad: Corrupt navigation data. Missing Hiding Spot\n" ); error = NAV_CORRUPT_DATA; } } } // convert visible ID's to pointers to actual areas for ( int it=0; itGetNavAreaByID( info.id ); if ( info.area == NULL ) { Warning( "Invalid area in visible set for area #%d\n", GetID() ); } } m_inheritVisibilityFrom.area = TheNavMesh->GetNavAreaByID( m_inheritVisibilityFrom.id ); Assert( m_inheritVisibilityFrom.area != this ); // remove any invalid areas from the list AreaBindInfo bad; bad.area = NULL; while( m_potentiallyVisibleAreas.FindAndRemove( bad ) ); return error; } //-------------------------------------------------------------------------------------------------------------- /** * Compute travel distance along shortest path from startPos to goalPos. * Return -1 if can't reach endPos from goalPos. */ template< typename CostFunctor > float NavAreaTravelDistance( const Vector &startPos, const Vector &goalPos, CostFunctor &costFunc ) { CNavArea *startArea = TheNavMesh->GetNearestNavArea( startPos ); if (startArea == NULL) { return -1.0f; } // compute path between areas using given cost heuristic CNavArea *goalArea = NULL; if (NavAreaBuildPath( startArea, NULL, &goalPos, costFunc, &goalArea ) == false) { return -1.0f; } // compute distance along path if (goalArea->GetParent() == NULL) { // both points are in the same area - return euclidean distance return (goalPos - startPos).Length(); } else { CNavArea *area; float distance; // goalPos is assumed to be inside goalArea (or very close to it) - skip to next area area = goalArea->GetParent(); distance = (goalPos - area->GetCenter()).Length(); for( ; area->GetParent(); area = area->GetParent() ) { distance += (area->GetCenter() - area->GetParent()->GetCenter()).Length(); } // add in distance to startPos distance += (startPos - area->GetCenter()).Length(); return distance; } } //-------------------------------------------------------------------------------------------------------------- /** * Determine the earliest time this hiding spot can be reached by either team */ void CNavArea::ComputeEarliestOccupyTimes( void ) { #ifdef CSTRIKE_DLL /// @todo Derive cstrike-specific navigation classes for( int i=0; iGetAbsOrigin(), GetCenter(), cost ); if (travelDistance < 0.0f) continue; float travelTime = travelDistance / playerSpeed; if (travelTime < m_earliestOccupyTime[ team ]) { m_earliestOccupyTime[ team ] = travelTime; } } // determine the shortest time it will take a CT to reach this area team = TEAM_CT % MAX_NAV_TEAMS; for( spot = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); spot; spot = gEntList.FindEntityByClassname( spot, "info_player_counterterrorist" ) ) { float travelDistance = NavAreaTravelDistance( spot->GetAbsOrigin(), GetCenter(), cost ); if (travelDistance < 0.0f) continue; float travelTime = travelDistance / playerSpeed; if (travelTime < m_earliestOccupyTime[ team ]) { m_earliestOccupyTime[ team ] = travelTime; } } #else for( int i=0; iGetNavArea( tSpawn->GetAbsOrigin() ); if (tArea == NULL) continue; for( ctSpawn = gEntList.FindEntityByClassname( NULL, "info_player_counterterrorist" ); ctSpawn; ctSpawn = gEntList.FindEntityByClassname( ctSpawn, "info_player_counterterrorist" ) ) { CNavArea *ctArea = TheNavMesh->GetNavArea( ctSpawn->GetAbsOrigin() ); if (ctArea == NULL) continue; if (tArea == ctArea) { m_isBattlefront = true; return; } // build path between these two spawn points - assume if path fails, it at least got close // (ie: imagine spawn points that you jump down from - can't path to) CNavArea *goalArea = NULL; NavAreaBuildPath( tArea, ctArea, NULL, cost, &goalArea ); if (goalArea == NULL) continue; /** * @todo Need to enumerate ALL paths between all pairs of spawn points to find all battlefront areas */ // find the area with the earliest overlapping occupy times CNavArea *battlefront = NULL; float earliestTime = 999999.9f; const float epsilon = 1.0f; CNavArea *area; for( area = goalArea; area; area = area->GetParent() ) { if (fabs(area->GetEarliestOccupyTime( TEAM_TERRORIST ) - area->GetEarliestOccupyTime( TEAM_CT )) < epsilon) { } } } } #endif #endif } //-------------------------------------------------------------------------------------------------------------- /** * Return the filename for this map's "nav map" file */ const char *CNavMesh::GetFilename( void ) const { // filename is local to game dir for Steam, so we need to prepend game dir for regular file save char gamePath[256]; engine->GetGameDir( gamePath, 256 ); // persistant return value static char filename[256]; Q_snprintf( filename, sizeof( filename ), "%s\\" FORMAT_NAVFILE, gamePath, STRING( gpGlobals->mapname ) ); return filename; } //-------------------------------------------------------------------------------------------------------------- /* ============ COM_FixSlashes Changes all '/' characters into '\' characters, in place. ============ */ inline void COM_FixSlashes( char *pname ) { #ifdef _WIN32 while ( *pname ) { if ( *pname == '/' ) *pname = '\\'; pname++; } #else while ( *pname ) { if ( *pname == '\\' ) *pname = '/'; pname++; } #endif } static void WarnIfMeshNeedsAnalysis( int version ) { // Quick check to warn about needing to analyze: nav_strip, nav_delete, etc set // every CNavArea's m_approachCount to 0, and delete their m_spotEncounterList. // So, if no area has either, odds are good we need an analyze. if ( version >= 14 ) { if ( !TheNavMesh->IsAnalyzed() ) { Warning( "The nav mesh needs a full nav_analyze\n" ); return; } } #ifdef CSTRIKE_DLL else { bool hasApproachAreas = false; bool hasSpotEncounters = false; FOR_EACH_VEC( TheNavAreas, it ) { CCSNavArea *area = dynamic_cast< CCSNavArea * >( TheNavAreas[ it ] ); if ( area ) { if ( area->GetApproachInfoCount() ) { hasApproachAreas = true; } if ( area->GetSpotEncounterCount() ) { hasSpotEncounters = true; } } } if ( !hasApproachAreas || !hasSpotEncounters ) { Warning( "The nav mesh needs a full nav_analyze\n" ); } } #endif } /** * Store Navigation Mesh to a file */ bool CNavMesh::Save( void ) const { WarnIfMeshNeedsAnalysis( NavCurrentVersion ); const char *filename = GetFilename(); if (filename == NULL) return false; // // Store the NAV file // COM_FixSlashes( const_cast(filename) ); // get size of source bsp file for later (before we open the nav file for writing, in // case of failure) char *bspFilename = GetBspFilename( filename ); if (bspFilename == NULL) { return false; } #if defined( PORTAL2 ) // Nav mesh unused in Portal2, don't want to allocate the 1MB fileBuffer. return false; #endif CUtlBuffer fileBuffer( 4096, 1024*1024 ); // store "magic number" to help identify this kind of file unsigned int magic = NAV_MAGIC_NUMBER; fileBuffer.PutUnsignedInt( magic ); // store version number of file // 1 = hiding spots as plain vector array // 2 = hiding spots as HidingSpot objects // 3 = Encounter spots use HidingSpot ID's instead of storing vector again // 4 = Includes size of source bsp file to verify nav data correlation // ---- Beta Release at V4 ----- // 5 = Added Place info // ---- Conversion to Src ------ // 6 = Added Ladder info // 7 = Areas store ladder ID's so ladders can have one-way connections // 8 = Added earliest occupy times (2 floats) to each area // 9 = Promoted CNavArea's attribute flags to a short // 10 - Added sub-version number to allow derived classes to have custom area data // 11 - Added light intensity to each area // 12 - Storing presence of unnamed areas in the PlaceDirectory // 13 - Widened NavArea attribute bits from unsigned short to int // 14 - Added a bool for if the nav needs analysis // 15 - removed approach areas // 16 - Added visibility data to the base mesh fileBuffer.PutUnsignedInt( NavCurrentVersion ); // The sub-version number is maintained and owned by classes derived from CNavMesh and CNavArea // and allows them to track their custom data just as we do at this top level fileBuffer.PutUnsignedInt( GetSubVersionNumber() ); // store the size of source bsp file in the nav file // so we can test if the bsp changed since the nav file was made unsigned int bspSize = filesystem->Size( bspFilename ); DevMsg( "Size of bsp file '%s' is %u bytes.\n", bspFilename, bspSize ); fileBuffer.PutUnsignedInt( bspSize ); // Store the analysis state fileBuffer.PutUnsignedChar( m_isAnalyzed ); // // Build a directory of the Places in this map // placeDirectory.Reset(); FOR_EACH_VEC( TheNavAreas, nit ) { CNavArea *area = TheNavAreas[ nit ]; Place place = area->GetPlace(); placeDirectory.AddPlace( place ); } placeDirectory.Save( fileBuffer ); SaveCustomDataPreArea( fileBuffer ); // // Store navigation areas // { // store number of areas unsigned int count = TheNavAreas.Count(); fileBuffer.PutUnsignedInt( count ); // store each area FOR_EACH_VEC( TheNavAreas, it ) { CNavArea *area = TheNavAreas[ it ]; area->Save( fileBuffer, NavCurrentVersion ); } } // // Store ladders // { // store number of ladders unsigned int count = m_ladders.Count(); fileBuffer.PutUnsignedInt( count ); // store each ladder for ( int i=0; iSave( fileBuffer, NavCurrentVersion ); } } // // Store derived class mesh info // SaveCustomData( fileBuffer ); if ( !filesystem->WriteFile( filename, "MOD", fileBuffer ) ) { Warning( "Unable to save %d bytes to %s\n", fileBuffer.Size(), filename ); return false; } unsigned int navSize = filesystem->Size( filename ); DevMsg( "Size of nav file '%s' is %u bytes.\n", filename, navSize ); return true; } //-------------------------------------------------------------------------------------------------------------- static NavErrorType CheckNavFile( const char *bspFilename ) { if ( !bspFilename ) return NAV_CANT_ACCESS_FILE; char baseName[256]; Q_StripExtension(bspFilename,baseName,sizeof(baseName)); char bspPathname[256]; Q_snprintf(bspPathname,sizeof(bspPathname), FORMAT_BSPFILE, baseName); char filename[256]; Q_snprintf(filename,sizeof(filename), FORMAT_NAVFILE, baseName); bool navIsInBsp = false; FileHandle_t file = filesystem->Open( filename, "rb", "MOD" ); // this ignores .nav files embedded in the .bsp ... if ( !file ) { navIsInBsp = true; file = filesystem->Open( filename, "rb", "GAME" ); // ... and this looks for one if it's the only one around. } if (!file) { return NAV_CANT_ACCESS_FILE; } // check magic number int result; unsigned int magic; result = filesystem->Read( &magic, sizeof(unsigned int), file ); if (!result || magic != NAV_MAGIC_NUMBER) { filesystem->Close( file ); return NAV_INVALID_FILE; } // read file version number unsigned int version; result = filesystem->Read( &version, sizeof(unsigned int), file ); if (!result || version > NavCurrentVersion || version < 4) { filesystem->Close( file ); return NAV_BAD_FILE_VERSION; } // get size of source bsp file and verify that the bsp hasn't changed unsigned int saveBspSize; filesystem->Read( &saveBspSize, sizeof(unsigned int), file ); // verify size unsigned int bspSize = filesystem->Size( bspPathname ); if (bspSize != saveBspSize && !navIsInBsp) { return NAV_FILE_OUT_OF_DATE; } return NAV_OK; } //-------------------------------------------------------------------------------------------------------------- void CommandNavCheckFileConsistency( void ) { if ( !UTIL_IsCommandIssuedByServerAdmin() ) return; FileFindHandle_t findHandle; const char *bspFilename = filesystem->FindFirstEx( "maps/*.bsp", "MOD", &findHandle ); while ( bspFilename ) { switch ( CheckNavFile( bspFilename ) ) { case NAV_CANT_ACCESS_FILE: Warning( "Missing nav file for %s\n", bspFilename ); break; case NAV_INVALID_FILE: Warning( "Invalid nav file for %s\n", bspFilename ); break; case NAV_BAD_FILE_VERSION: Warning( "Old nav file for %s\n", bspFilename ); break; case NAV_FILE_OUT_OF_DATE: Warning( "The nav file for %s is built from an old version of the map\n", bspFilename ); break; case NAV_OK: Msg( "The nav file for %s is up-to-date\n", bspFilename ); break; } bspFilename = filesystem->FindNext( findHandle ); } filesystem->FindClose( findHandle ); } static ConCommand nav_check_file_consistency( "nav_check_file_consistency", CommandNavCheckFileConsistency, "Scans the maps directory and reports any missing/out-of-date navigation files.", FCVAR_GAMEDLL | FCVAR_CHEAT ); //-------------------------------------------------------------------------------------------------------------- /** * Reads the used place names from the nav file (can be used to selectively precache before the nav is loaded) */ const CUtlVector< Place > *CNavMesh::GetPlacesFromNavFile( bool *hasUnnamedPlaces ) { placeDirectory.Reset(); // nav filename is derived from map filename char filename[256]; Q_snprintf( filename, sizeof( filename ), FORMAT_NAVFILE, STRING( gpGlobals->mapname ) ); #if defined( PORTAL2 ) // Nav mesh unused in Portal2, don't want to allocate the 1MB fileBuffer. return NULL; #endif CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::READ_ONLY ); if ( !filesystem->ReadFile( filename, "GAME", fileBuffer ) ) // this ignores .nav files embedded in the .bsp ... { if ( !filesystem->ReadFile( filename, "BSP", fileBuffer ) ) // ... and this looks for one if it's the only one around. { return NULL; } } if ( IsGameConsole() ) { // 360 has compressed NAVs CLZMA lzma; if ( lzma.IsCompressed( (unsigned char *)fileBuffer.Base() ) ) { int originalSize = lzma.GetActualSize( (unsigned char *)fileBuffer.Base() ); unsigned char *pOriginalData = new unsigned char[originalSize]; lzma.Uncompress( (unsigned char *)fileBuffer.Base(), pOriginalData ); fileBuffer.AssumeMemory( pOriginalData, originalSize, originalSize, CUtlBuffer::READ_ONLY ); } } // check magic number unsigned int magic = fileBuffer.GetUnsignedInt(); if ( !fileBuffer.IsValid() || magic != NAV_MAGIC_NUMBER ) { return NULL; // Corrupt nav file? } // read file version number unsigned int version = fileBuffer.GetUnsignedInt(); if ( !fileBuffer.IsValid() || version > NavCurrentVersion ) { return NULL; // Unknown nav file version } if ( version < 5 ) { return NULL; // Too old to have place names } unsigned int subVersion = 0; if ( version >= 10 ) { subVersion = fileBuffer.GetUnsignedInt(); if ( !fileBuffer.IsValid() ) { return NULL; // No sub-version } } fileBuffer.GetUnsignedInt(); // skip BSP file size if ( version >= 14 ) { fileBuffer.GetUnsignedChar(); // skip m_isAnalyzed } placeDirectory.Load( fileBuffer, version ); LoadCustomDataPreArea( fileBuffer, subVersion ); if ( hasUnnamedPlaces ) { *hasUnnamedPlaces = placeDirectory.HasUnnamedPlaces(); } return placeDirectory.GetPlaces(); } //-------------------------------------------------------------------------------------------------------------- /** * Load AI navigation data from a file */ NavErrorType CNavMesh::Load( void ) { MDLCACHE_CRITICAL_SECTION(); // free previous navigation mesh data Reset(); placeDirectory.Reset(); CNavVectorNoEditAllocator::Reset(); GameRules()->OnNavMeshLoad(); CNavArea::m_nextID = 1; // nav filename is derived from map filename char filename[256]; Q_snprintf( filename, sizeof( filename ), FORMAT_NAVFILE, STRING( gpGlobals->mapname ) ); #if defined( PORTAL2 ) // Nav mesh unused in Portal2, don't want to allocate the 1MB fileBuffer. return NAV_CANT_ACCESS_FILE; #endif bool navIsInBsp = false; CUtlBuffer fileBuffer( 4096, 1024*1024, CUtlBuffer::READ_ONLY ); if ( IsGameConsole() ) { if ( !filesystem->ReadFile( filename, "GAME", fileBuffer ) ) // this ignores .nav files embedded in the .bsp ... { navIsInBsp = true; if ( !filesystem->ReadFile( filename, "BSP", fileBuffer ) ) // ... and this looks for one if it's the only one around. { return NAV_CANT_ACCESS_FILE; } } // 360 has compressed NAVs CLZMA lzma; if ( lzma.IsCompressed( (unsigned char *)fileBuffer.Base() ) ) { int originalSize = lzma.GetActualSize( (unsigned char *)fileBuffer.Base() ); unsigned char *pOriginalData = new unsigned char[originalSize]; lzma.Uncompress( (unsigned char *)fileBuffer.Base(), pOriginalData ); fileBuffer.AssumeMemory( pOriginalData, originalSize, originalSize, CUtlBuffer::READ_ONLY ); } } else { if ( !filesystem->ReadFile( filename, "MOD", fileBuffer ) ) // this ignores .nav files embedded in the .bsp ... { navIsInBsp = true; if ( !filesystem->ReadFile( filename, "BSP", fileBuffer ) ) // ... and this looks for one if it's the only one around. { return NAV_CANT_ACCESS_FILE; } } } // check magic number unsigned int magic = fileBuffer.GetUnsignedInt(); if ( !fileBuffer.IsValid() || magic != NAV_MAGIC_NUMBER ) { Msg( "Invalid navigation file '%s'.\n", filename ); return NAV_INVALID_FILE; } // read file version number unsigned int version = fileBuffer.GetUnsignedInt(); if ( !fileBuffer.IsValid() || version > NavCurrentVersion ) { Msg( "Unknown navigation file version.\n" ); return NAV_BAD_FILE_VERSION; } unsigned int subVersion = 0; if ( version >= 10 ) { subVersion = fileBuffer.GetUnsignedInt(); if ( !fileBuffer.IsValid() ) { Msg( "Error reading sub-version number.\n" ); return NAV_INVALID_FILE; } } if ( version >= 4 ) { // get size of source bsp file and verify that the bsp hasn't changed unsigned int saveBspSize = fileBuffer.GetUnsignedInt(); // verify size char *bspFilename = GetBspFilename( filename ); if ( bspFilename == NULL ) { return NAV_INVALID_FILE; } unsigned int bspSize = filesystem->Size( bspFilename ); if ( bspSize != saveBspSize && !navIsInBsp ) { if ( !IsGameConsole() ) { if ( engine->IsDedicatedServer() ) { // Warning doesn't print to the dedicated server console, so we'll use Msg instead DevMsg( "The Navigation Mesh was built using a different version of this map.\n" ); } else { DevWarning( "The Navigation Mesh was built using a different version of this map.\n" ); } } m_isOutOfDate = true; } } if ( version >= 14 ) { m_isAnalyzed = fileBuffer.GetUnsignedChar() != 0; } else { m_isAnalyzed = false; } // load Place directory if ( version >= 5 ) { placeDirectory.Load( fileBuffer, version ); } LoadCustomDataPreArea( fileBuffer, subVersion ); // get number of areas unsigned int count = fileBuffer.GetUnsignedInt(); unsigned int i; if ( count == 0 ) { return NAV_INVALID_FILE; } Extent extent; extent.lo.x = 9999999999.9f; extent.lo.y = 9999999999.9f; extent.hi.x = -9999999999.9f; extent.hi.y = -9999999999.9f; // load the areas and compute total extent TheNavMesh->PreLoadAreas( count ); Extent areaExtent; for( i=0; iCreateArea(); area->Load( fileBuffer, version, subVersion ); TheNavAreas.AddToTail( area ); area->GetExtent( &areaExtent ); if (areaExtent.lo.x < extent.lo.x) extent.lo.x = areaExtent.lo.x; if (areaExtent.lo.y < extent.lo.y) extent.lo.y = areaExtent.lo.y; if (areaExtent.hi.x > extent.hi.x) extent.hi.x = areaExtent.hi.x; if (areaExtent.hi.y > extent.hi.y) extent.hi.y = areaExtent.hi.y; } // add the areas to the grid AllocateGrid( extent.lo.x, extent.hi.x, extent.lo.y, extent.hi.y ); FOR_EACH_VEC( TheNavAreas, it ) { AddNavArea( TheNavAreas[ it ] ); } // // Set up all the ladders // if (version >= 6) { count = fileBuffer.GetUnsignedInt(); m_ladders.EnsureCapacity( count ); // load the ladders for( i=0; iLoad( fileBuffer, version ); m_ladders.AddToTail( ladder ); } } else { BuildLadders(); } // mark stairways (TODO: this can be removed once all maps are re-saved with this attribute in them) MarkStairAreas(); // // Load derived class mesh info // LoadCustomData( fileBuffer, subVersion ); // // Bind pointers, etc // NavErrorType loadResult = PostLoad( version ); WarnIfMeshNeedsAnalysis( version ); return loadResult; } struct OneWayLink_t { CNavArea *destArea; CNavArea *area; int backD; static int Compare(const OneWayLink_t *lhs, const OneWayLink_t *rhs ) { int result = ( lhs->destArea - rhs->destArea ); if ( result != 0 ) { return result; } return ( lhs->backD - rhs->backD ); } }; //-------------------------------------------------------------------------------------------------------------- /** * Invoked after all areas have been loaded - for pointer binding, etc */ NavErrorType CNavMesh::PostLoad( unsigned int version ) { // allow areas to connect to each other, etc FOR_EACH_VEC( TheNavAreas, pit ) { CNavArea *area = TheNavAreas[ pit ]; area->PostLoad(); } // allow hiding spots to compute information FOR_EACH_VEC( TheHidingSpots, hit ) { HidingSpot *spot = TheHidingSpots[ hit ]; spot->PostLoad(); } if ( version < 8 ) { // Old nav meshes need to compute earliest occupy times FOR_EACH_VEC( TheNavAreas, nit ) { CNavArea *area = TheNavAreas[ nit ]; area->ComputeEarliestOccupyTimes(); } } ComputeBattlefrontAreas(); // // Allow each nav area to know what other areas have one-way connections to it. Need to gather // then sort due to allocation restrictions on the 360 // OneWayLink_t oneWayLink; CUtlVectorFixedGrowable oneWayLinks; FOR_EACH_VEC( TheNavAreas, oit ) { oneWayLink.area = TheNavAreas[ oit ]; for( int d=0; dGetAdjacentAreas( (NavDirType)d ); FOR_EACH_VEC( (*connectList), it ) { NavConnect connect = (*connectList)[ it ]; oneWayLink.destArea = connect.area; // if the area we connect to has no connection back to us, allow that area to remember us as an incoming connection oneWayLink.backD = OppositeDirection( (NavDirType)d ); const NavConnectVector *backConnectList = oneWayLink.destArea->GetAdjacentAreas( (NavDirType)oneWayLink.backD ); bool isOneWay = true; FOR_EACH_VEC( (*backConnectList), bit ) { NavConnect backConnect = (*backConnectList)[ bit ]; if (backConnect.area->GetID() == oneWayLink.area->GetID()) { isOneWay = false; break; } } if (isOneWay) { oneWayLinks.AddToTail( oneWayLink ); } } } } oneWayLinks.Sort( &OneWayLink_t::Compare ); for ( int i = 0; i < oneWayLinks.Count(); i++ ) { // add this one-way connection oneWayLinks[i].destArea->AddIncomingConnection( oneWayLinks[i].area, (NavDirType)oneWayLinks[i].backD ); } ValidateNavAreaConnections(); // TERROR: loading into a map directly creates entities before the mesh is loaded. Tell the preexisting // entities now that the mesh is loaded so they can update areas. for ( int i=0; iOnNavMeshLoaded(); } // the Navigation Mesh has been successfully loaded m_isLoaded = true; return NAV_OK; }