diff --git a/CMakeLists.txt b/CMakeLists.txt index 9623c60e..f6a3419f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,6 @@ add_custom_target( copy_runtime_files ALL # Dependencies and compiler settings # ###################################### include( "cmake/paths.cmake" ) -#include( "cmake/mysql.cmake" ) include( "cmake/compiler.cmake" ) include( "cmake/cotire.cmake" ) diff --git a/cmake/paths.cmake b/cmake/paths.cmake index 93e6008b..ab4ae84d 100644 --- a/cmake/paths.cmake +++ b/cmake/paths.cmake @@ -12,3 +12,4 @@ endif() # Create log folder file( MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin/log ) +file( MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin/navi ) diff --git a/config/global.ini.default b/config/global.ini.default index 87c3b4cc..db3ec4a2 100644 --- a/config/global.ini.default +++ b/config/global.ini.default @@ -12,6 +12,8 @@ ServerSecret = default DataPath = C:\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\sqpack WorldID = 67 DefaultGMRank = 255 +LogLevel = 1 +LogFilter = 0 [Network] ; Values definining how Users and other servers will access - these have to be set to your public IP when running a public server diff --git a/config/world.ini.default b/config/world.ini.default index 0fd35e32..ec461c19 100644 --- a/config/world.ini.default +++ b/config/world.ini.default @@ -15,6 +15,9 @@ DisconnectTimeout = 20 ; Sent on login - each line must be shorter than 307 characters, split lines with ';' MotD = Welcome to Sapphire!;This is a very good server;You can change these messages by editing General.MotD in config/config.ini +[Navigation] +MeshPath = navi + [Housing] ; Set the default estate name. {0} will be replaced with the plot number DefaultEstateName = Estate ${0} \ No newline at end of file diff --git a/src/api/main.cpp b/src/api/main.cpp index 5fd967a0..92d422b1 100644 --- a/src/api/main.cpp +++ b/src/api/main.cpp @@ -713,6 +713,8 @@ int main( int argc, char* argv[] ) if( !loadSettings( argc, argv ) ) throw std::exception(); + Logger::setLogLevel( m_config.global.general.logLevel ); + server.resource[ "^/ZoneName/([0-9]+)$" ][ "GET" ] = &getZoneName; server.resource[ "^/sapphire-api/lobby/createAccount" ][ "POST" ] = &createAccount; server.resource[ "^/sapphire-api/lobby/login" ][ "POST" ] = &login; diff --git a/src/common/Common.h b/src/common/Common.h index 6dfc5e70..1735d54b 100644 --- a/src/common/Common.h +++ b/src/common/Common.h @@ -615,6 +615,7 @@ namespace Sapphire::Common InvincibilityNone, InvincibilityRefill, InvincibilityStayAlive, + InvincibilityIgnoreDamage, }; enum PlayerStateFlag : uint8_t diff --git a/src/common/Config/ConfigDef.h b/src/common/Config/ConfigDef.h index a6a111b8..3aca9cff 100644 --- a/src/common/Config/ConfigDef.h +++ b/src/common/Config/ConfigDef.h @@ -16,6 +16,8 @@ namespace Sapphire::Common::Config uint16_t worldID; uint8_t defaultGMRank; + uint8_t logLevel; + uint32_t logFilter; } general; struct Network @@ -56,6 +58,11 @@ namespace Sapphire::Common::Config bool hotSwap; } scripts; + struct Navigation + { + std::string meshPath; + } navigation; + std::string motd; }; diff --git a/src/common/Config/ConfigMgr.cpp b/src/common/Config/ConfigMgr.cpp index a1ad914b..a4fa1bdc 100644 --- a/src/common/Config/ConfigMgr.cpp +++ b/src/common/Config/ConfigMgr.cpp @@ -57,6 +57,8 @@ bool Sapphire::ConfigMgr::loadGlobalConfig( Common::Config::GlobalConfig& config config.general.serverSecret = getValue< std::string >( "General", "ServerSecret", "default" ); config.general.worldID = getValue< uint16_t >( "General", "WorldID", 67 ); config.general.defaultGMRank = getValue< uint8_t >( "General", "DefaultGMRank", 255 ); + config.general.logLevel = getValue< uint8_t >( "General", "LogLevel", 1 ); + config.general.logFilter = getValue< uint32_t >( "General", "LogFilter", 0 ); // network config.network.zoneHost = getValue< std::string >( "Network", "ZoneHost", "127.0.0.1" ); diff --git a/src/common/Logging/Logger.cpp b/src/common/Logging/Logger.cpp index dbf83f4f..a8c17bff 100644 --- a/src/common/Logging/Logger.cpp +++ b/src/common/Logging/Logger.cpp @@ -53,6 +53,11 @@ namespace Sapphire spdlog::flush_on( spdlog::level::critical ); } + void Logger::setLogLevel( uint8_t logLevel ) + { + spdlog::set_level( static_cast< spdlog::level::level_enum >( logLevel ) ); + } + void Logger::error( const std::string& text ) { spdlog::get( "logger" )->error( text ); diff --git a/src/common/Logging/Logger.h b/src/common/Logging/Logger.h index 5151eb1d..af0c9d0d 100644 --- a/src/common/Logging/Logger.h +++ b/src/common/Logging/Logger.h @@ -19,6 +19,7 @@ namespace Sapphire public: static void init( const std::string& logPath ); + static void setLogLevel( uint8_t logLevel ); // todo: this is a minor increase in build time because of fmtlib, but much less than including spdlog directly diff --git a/src/common/Network/PacketDef/Ipcs.h b/src/common/Network/PacketDef/Ipcs.h index 08edad4e..afded198 100644 --- a/src/common/Network/PacketDef/Ipcs.h +++ b/src/common/Network/PacketDef/Ipcs.h @@ -229,6 +229,17 @@ namespace Sapphire::Network::Packets IPCTYPE_UNK_320 = 0x0253, // updated 4.5 IPCTYPE_UNK_322 = 0x0255, // updated 4.5 + /// Doman Mahjong ////////////////////////////////////// + MahjongOpenGui = 0x02BC, // only available in mahjong instance + MahjongNextRound = 0x02BD, // initial hands(baipai), # of riichi(wat), winds, honba, score and stuff + MahjongPlayerAction = 0x02BE, // tsumo(as in drawing a tile) called chi/pon/kan/riichi + MahjongEndRoundTsumo = 0x02BF, // called tsumo + MahjongEndRoundRon = 0x2C0, // called ron or double ron (waiting for action must be flagged from discard packet to call) + MahjongTileDiscard = 0x02C1, // giri (discarding a tile.) chi(1)/pon(2)/kan(4)/ron(8) flags etc.. + MahjongPlayersInfo = 0x02C2, // actor id, name, rating and stuff.. + // 2C3 and 2C4 are currently unknown + MahjongEndRoundDraw = 0x02C5, // self explanatory + MahjongEndGame = 0x02C6, // finished oorasu(all-last) round; shows a result screen. }; /** diff --git a/src/common/Network/PacketDef/Zone/ServerZoneDef.h b/src/common/Network/PacketDef/Zone/ServerZoneDef.h index ef70853c..65de5804 100644 --- a/src/common/Network/PacketDef/Zone/ServerZoneDef.h +++ b/src/common/Network/PacketDef/Zone/ServerZoneDef.h @@ -666,7 +666,7 @@ struct FFXIVIpcActorMove : { /* 0000 */ uint8_t rotation; /* 0001 */ uint8_t unknown_1; - /* 0002 */ uint8_t unknown_2; + /* 0002 */ uint8_t animationType; /* 0003 */ uint8_t unknown_3; /* 0004 */ uint16_t unknown_4; /* 0006 */ uint16_t posX; diff --git a/src/lobby/ServerLobby.cpp b/src/lobby/ServerLobby.cpp index d1530dcf..47d8a868 100644 --- a/src/lobby/ServerLobby.cpp +++ b/src/lobby/ServerLobby.cpp @@ -63,6 +63,8 @@ namespace Sapphire return; } + Logger::setLogLevel( m_config.global.general.logLevel ); + auto pFw = std::make_shared< Framework >(); Network::HivePtr hive( new Network::Hive() ); Network::addServerToHive< Network::GameConnection >( m_ip, m_port, hive, pFw ); diff --git a/src/tools/pcb_reader/CMakeLists.txt b/src/tools/pcb_reader/CMakeLists.txt index 6156b25c..d42aa873 100644 --- a/src/tools/pcb_reader/CMakeLists.txt +++ b/src/tools/pcb_reader/CMakeLists.txt @@ -3,7 +3,10 @@ cmake_policy(SET CMP0015 NEW) project(Tool_pcb_reader2) file(GLOB SERVER_PUBLIC_INCLUDE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/*") -file(GLOB SERVER_SOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}*.c*") +file(GLOB SERVER_SOURCE_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + *.c* + nav/*.c* + nav/ext/*.c*) add_executable(pcb_reader2 ${SERVER_PUBLIC_INCLUDE_FILES} ${SERVER_SOURCE_FILES}) diff --git a/src/tools/pcb_reader/exportmgr.h b/src/tools/pcb_reader/exportmgr.h index 656e15f7..c448b5d2 100644 --- a/src/tools/pcb_reader/exportmgr.h +++ b/src/tools/pcb_reader/exportmgr.h @@ -20,14 +20,14 @@ public: void exportZone(const ExportedZone& zone, ExportFileType exportFileTypes) { - if( exportFileTypes & ExportFileType::WavefrontObj ) + m_threadpool.queue( [zone, exportFileTypes]() { - m_threadpool.queue( [zone](){ ObjExporter::exportZone( zone ); } ); - } - if( exportFileTypes & ExportFileType::Navmesh ) - { - m_threadpool.queue( [zone](){ NavmeshExporter::exportZone( zone ); } ); - } + if( exportFileTypes & ExportFileType::WavefrontObj ) + ObjExporter::exportZone( zone ); + + if( exportFileTypes & ExportFileType::Navmesh ) + NavmeshExporter::exportZone( zone ); + } ); } void exportGroup( const std::string& zoneName, const ExportedGroup& group, ExportFileType exportFileTypes ) @@ -36,10 +36,10 @@ public: { m_threadpool.queue( [zoneName, group](){ ObjExporter::exportGroup( zoneName, group ); } ); } - if( exportFileTypes & ExportFileType::Navmesh ) - { - m_threadpool.queue( [zoneName, group](){ NavmeshExporter::exportGroup( zoneName, group ); } ); - } +// if( exportFileTypes & ExportFileType::Navmesh ) +// { +// m_threadpool.queue( [zoneName, group](){ NavmeshExporter::exportGroup( zoneName, group ); } ); +// } } void waitForTasks() diff --git a/src/tools/pcb_reader/main.cpp b/src/tools/pcb_reader/main.cpp index 60042257..7dadc758 100644 --- a/src/tools/pcb_reader/main.cpp +++ b/src/tools/pcb_reader/main.cpp @@ -32,7 +32,7 @@ // garbage to ignore models bool noObj = false; -std::string gamePath( "C:\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\sqpack" ); +std::string gamePath( "/mnt/c/Program Files (x86)/Steam/steamapps/common/FINAL FANTASY XIV Online/game/sqpack" ); std::unordered_map< uint16_t, std::string > zoneNameMap; std::map< std::string, std::string > exportedTeriMap; @@ -190,7 +190,7 @@ int main( int argc, char* argv[] ) } catch( std::exception& e ) { - printf( "Unable to initialise EXD! Usage: pcb_reader \"path/to/FINAL FANTASY XIV - A REALM REBORN/game/sqpack\" [--no-obj, --dump-all, --navmesh]" ); + printf( "Unable to initialise EXD!\n Usage: pcb_reader \"path/to/FINAL FANTASY XIV - A REALM REBORN/game/sqpack\" [--no-obj, --dump-all, --navmesh]\n" ); return -1; } ExportMgr exportMgr; @@ -380,7 +380,7 @@ int main( int argc, char* argv[] ) buildModelEntry( pPcbFile, exportedTerrainGroup, fileName, zoneName ); } exportedZone.groups.emplace( exportedTerrainGroup.name, exportedTerrainGroup ); - + for( const auto& lgb : lgbList ) { for( const auto& group : lgb.groups ) @@ -497,13 +497,13 @@ int main( int argc, char* argv[] ) exportMgr.exportZone( exportedZone, ExportFileType::Navmesh ); - printf( "Exported %s in %u seconds \n", + printf( "Exported %s in %lu seconds \n", zoneName.c_str(), - std::chrono::duration_cast< std::chrono::seconds >( std::chrono::high_resolution_clock::now() - entryStartTime ) ); + std::chrono::duration_cast< std::chrono::seconds >( std::chrono::high_resolution_clock::now() - entryStartTime ).count() ); } catch( std::exception& e ) { - printf( ( std::string( e.what() ) + "\n" ).c_str() ); + printf( "%s", ( std::string( e.what() ) + "\n" ).c_str() ); printf( "Unable to extract collision data.\n" ); printf( "Usage: pcb_reader2 territory \"path/to/game/sqpack/ffxiv\"\n" ); } @@ -511,14 +511,11 @@ int main( int argc, char* argv[] ) exportMgr.waitForTasks(); std::cout << "\n\n\n"; - printf( "Finished all tasks in %u seconds\n", + printf( "Finished all tasks in %lu seconds\n", std::chrono::duration_cast< std::chrono::seconds >( std::chrono::high_resolution_clock::now() - startTime ).count() ); - getchar(); - - if( eData ) - delete eData; - if( data1 ) - delete data1; + delete eData; + delete data1; + return 0; } diff --git a/src/tools/pcb_reader/nav/TiledNavmeshGenerator.cpp b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.cpp new file mode 100644 index 00000000..3fa19668 --- /dev/null +++ b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.cpp @@ -0,0 +1,578 @@ +#include "TiledNavmeshGenerator.h" + +#include +#include + +#include + +namespace fs = std::experimental::filesystem; + + +inline unsigned int nextPow2( uint32_t v ) +{ + v--; + v |= v >> 1; + v |= v >> 2; + v |= v >> 4; + v |= v >> 8; + v |= v >> 16; + v++; + return v; +} + +inline unsigned int ilog2( uint32_t v ) +{ + uint32_t r; + uint32_t shift; + r = (v > 0xffff) << 4; v >>= r; + shift = (v > 0xff) << 3; v >>= shift; r |= shift; + shift = (v > 0xf) << 2; v >>= shift; r |= shift; + shift = (v > 0x3) << 1; v >>= shift; r |= shift; + r |= (v >> 1); + return r; +} + +bool TiledNavmeshGenerator::init( const std::string& path ) +{ + if( !fs::exists( path ) ) + throw std::runtime_error( "what" ); + + // ignore logging/bullshit/etc + m_ctx = new rcContext( false ); + + printf( "[Navmesh] loading obj: %s\n", path.substr( path.find( "pcb_export" ) - 1 ).c_str() ); + + m_mesh = new rcMeshLoaderObj; + assert( m_mesh ); + + if( !m_mesh->load( path ) ) + { + printf( "[Navmesh] Failed to allocate rcMeshLoaderObj\n" ); + return false; + } + + rcCalcBounds( m_mesh->getVerts(), m_mesh->getVertCount(), m_meshBMin, m_meshBMax ); + + m_chunkyMesh = new rcChunkyTriMesh; + assert( m_chunkyMesh ); + + if( !rcCreateChunkyTriMesh( m_mesh->getVerts(), m_mesh->getTris(), m_mesh->getTriCount(), 256, m_chunkyMesh ) ) + { + printf( "[Navmesh] buildTiledNavigation: Failed to build chunky mesh.\n" ); + return false; + } + + // todo: load some bullshit settings from exd + + int gw = 0, gh = 0; + rcCalcGridSize( m_meshBMin, m_meshBMax, m_cellSize, &gw, &gh ); + + auto ts = static_cast< uint32_t >( m_tileSize ); + const uint32_t tw = ( gw + ts - 1 ) / ts; + const uint32_t th = ( gh + ts - 1 ) / ts; + + printf( "[Navmesh] - Tiles %d x %d\n", tw, th ); + + int tileBits = rcMin( ( int ) ilog2( nextPow2( tw * th ) ), 14 ); + if( tileBits > 14 ) + tileBits = 14; + int polyBits = 22 - tileBits; + m_maxTiles = 1 << tileBits; + m_maxPolysPerTile = 1 << polyBits; + + printf( "[Navmesh] - %.1fK verts, %.1fK tris\n", m_mesh->getVertCount() / 1000.0f, m_mesh->getTriCount() / 1000.0f ); + + return true; +} + +TiledNavmeshGenerator::~TiledNavmeshGenerator() +{ + delete m_mesh; + delete m_chunkyMesh; + + delete m_ctx; + + dtFreeNavMesh( m_navMesh ); + dtFreeNavMeshQuery( m_navQuery ); +} + +void TiledNavmeshGenerator::saveNavmesh( const std::string& name ) +{ + assert( m_navMesh ); + + // fuck this gay earth + auto mesh = const_cast< const dtNavMesh* >( m_navMesh ); + + auto dir = fs::current_path().string() + "/pcb_export/" + name + "/"; + auto fileName = dir + name + ".nav"; + + fs::create_directories( dir ); + + FILE* fp = fopen( fileName.c_str(), "wb" ); + if( !fp ) + return; + + // Store header. + NavMeshSetHeader header; + header.magic = NAVMESHSET_MAGIC; + header.version = NAVMESHSET_VERSION; + header.numTiles = 0; + for( int i = 0; i < mesh->getMaxTiles(); ++i ) + { + auto tile = mesh->getTile( i ); + if( !tile || !tile->header || !tile->dataSize ) + continue; + + header.numTiles++; + } + + memcpy( &header.params, mesh->getParams(), sizeof( dtNavMeshParams ) ); + fwrite( &header, sizeof( NavMeshSetHeader ), 1, fp ); + + // Store tiles. + for( int i = 0; i < mesh->getMaxTiles(); ++i ) + { + auto tile = mesh->getTile( i ); + if( !tile || !tile->header || !tile->dataSize ) + continue; + + NavMeshTileHeader tileHeader; + tileHeader.tileRef = mesh->getTileRef( tile ); + tileHeader.dataSize = tile->dataSize; + fwrite( &tileHeader, sizeof( tileHeader ), 1, fp ); + + fwrite( tile->data, tile->dataSize, 1, fp ); + } + + fclose( fp ); + + auto pos = fileName.find( "pcb_export" ); + fileName = fileName.substr( pos - 1 ); + + printf( "[Navmesh] Saved navmesh to '%s'\n", fileName.c_str() ); +} + +bool TiledNavmeshGenerator::buildNavmesh() +{ + assert( m_mesh ); + + m_navMesh = dtAllocNavMesh(); + if( !m_navMesh ) + { + printf( "[Navmesh] buildTiledNavigation: Could not allocate navmesh.\n" ); + return false; + } + + dtNavMeshParams params{}; + rcVcopy( params.orig, m_meshBMin ); + params.tileWidth = m_tileSize * m_cellSize; + params.tileHeight = m_tileSize * m_cellSize; + params.maxTiles = m_maxTiles; + params.maxPolys = m_maxPolysPerTile; + + dtStatus status; + + status = m_navMesh->init( ¶ms ); + if( dtStatusFailed( status ) ) + { + printf( "[Navmesh] buildTiledNavigation: Could not init navmesh.\n" ); + return false; + } + + m_navQuery = dtAllocNavMeshQuery(); + assert( m_navQuery ); + + status = m_navQuery->init( m_navMesh, 2048 ); + if( dtStatusFailed( status ) ) + { + printf( "[Navmesh] buildTiledNavigation: Could not init Detour navmesh query\n" ); + return false; + } + + // todo: duplicated from above, we can probably cache all this and only do it once + int gw = 0, gh = 0; + rcCalcGridSize( m_meshBMin, m_meshBMax, m_cellSize, &gw, &gh ); + auto ts = static_cast< uint32_t >( m_tileSize ); + const int tw = ( gw + ts - 1 ) / ts; + const int th = ( gh + ts - 1 ) / ts; + const float tcs = m_tileSize * m_cellSize; + + for( int y = 0; y < th; y++ ) + { + for( int x = 0; x < tw; x++ ) + { + m_lastBuiltTileBmin[ 0 ] = m_meshBMin[ 0 ] + x * tcs; + m_lastBuiltTileBmin[ 1 ] = m_meshBMin[ 1 ]; + m_lastBuiltTileBmin[ 2 ] = m_meshBMin[ 2 ] + y * tcs; + + m_lastBuiltTileBmax[ 0 ] = m_meshBMin[ 0 ] + ( x + 1 ) * tcs; + m_lastBuiltTileBmax[ 1 ] = m_meshBMax[ 1 ]; + m_lastBuiltTileBmax[ 2 ] = m_meshBMin[ 2 ] + ( y + 1 ) * tcs; + + int dataSize = 0; + + unsigned char* data = buildTileMesh( x, y, m_lastBuiltTileBmin, m_lastBuiltTileBmax, dataSize ); + if( data ) + { + // Remove any previous data (navmesh owns and deletes the data). + m_navMesh->removeTile( m_navMesh->getTileRefAt( x, y, 0 ), nullptr, nullptr ); + + // Let the navmesh own the data. + status = m_navMesh->addTile( data, dataSize, DT_TILE_FREE_DATA, 0, nullptr ); + + if( dtStatusFailed( status ) ) + { + dtFree( data ); + } + } + } + } + + return true; +} + + +unsigned char* TiledNavmeshGenerator::buildTileMesh( const int tx, const int ty, const float* bmin, const float* bmax, + int& dataSize ) +{ + const float* verts = m_mesh->getVerts(); + const int nverts = m_mesh->getVertCount(); + const int ntris = m_mesh->getTriCount(); + + // Init build configuration from GUI + memset( &m_cfg, 0, sizeof( m_cfg ) ); + m_cfg.cs = m_cellSize; + m_cfg.ch = m_cellHeight; + m_cfg.walkableSlopeAngle = m_agentMaxSlope; + m_cfg.walkableHeight = static_cast< int >( ceilf( m_agentHeight / m_cfg.ch ) ); + m_cfg.walkableClimb = static_cast< int >( floorf( m_agentMaxClimb / m_cfg.ch ) ); + m_cfg.walkableRadius = static_cast< int >( ceilf( m_agentRadius / m_cfg.cs ) ); + m_cfg.maxEdgeLen = static_cast< int >( m_edgeMaxLen / m_cellSize ); + m_cfg.maxSimplificationError = m_edgeMaxError; + m_cfg.minRegionArea = static_cast< int >( rcSqr( m_regionMinSize ) ); // Note: area = size*size + m_cfg.mergeRegionArea = static_cast< int >( rcSqr( m_regionMergeSize ) ); // Note: area = size*size + m_cfg.maxVertsPerPoly = static_cast< int >( m_vertsPerPoly ); + m_cfg.tileSize = static_cast< int >( m_tileSize ); + m_cfg.borderSize = m_cfg.walkableRadius + 3; // Reserve enough padding. + m_cfg.width = m_cfg.tileSize + m_cfg.borderSize * 2; + m_cfg.height = m_cfg.tileSize + m_cfg.borderSize * 2; + m_cfg.detailSampleDist = m_detailSampleDist < 0.9f ? 0 : m_cellSize * m_detailSampleDist; + m_cfg.detailSampleMaxError = m_cellHeight * m_detailSampleMaxError; + + // Expand the heighfield bounding box by border size to find the extents of geometry we need to build this tile. + // + // This is done in order to make sure that the navmesh tiles connect correctly at the borders, + // and the obstacles close to the border work correctly with the dilation process. + // No polygons (or contours) will be created on the border area. + // + // IMPORTANT! + // + // :''''''''': + // : +-----+ : + // : | | : + // : | |<--- tile to build + // : | | : + // : +-----+ :<-- geometry needed + // :.........: + // + // You should use this bounding box to query your input geometry. + // + // For example if you build a navmesh for terrain, and want the navmesh tiles to match the terrain tile size + // you will need to pass in data from neighbour terrain tiles too! In a simple case, just pass in all the 8 neighbours, + // or use the bounding box below to only pass in a sliver of each of the 8 neighbours. + rcVcopy( m_cfg.bmin, bmin ); + rcVcopy( m_cfg.bmax, bmax ); + m_cfg.bmin[ 0 ] -= m_cfg.borderSize * m_cfg.cs; + m_cfg.bmin[ 2 ] -= m_cfg.borderSize * m_cfg.cs; + m_cfg.bmax[ 0 ] += m_cfg.borderSize * m_cfg.cs; + m_cfg.bmax[ 2 ] += m_cfg.borderSize * m_cfg.cs; + + m_solid = rcAllocHeightfield(); + if( !m_solid ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'solid'.\n" ); + return nullptr; + } + + if( !rcCreateHeightfield( m_ctx, *m_solid, m_cfg.width, m_cfg.height, m_cfg.bmin, m_cfg.bmax, m_cfg.cs, m_cfg.ch ) ) + { + printf( "[Navmesh] buildNavigation: Could not create solid heightfield.\n" ); + return nullptr; + } + + // Allocate array that can hold triangle flags. + // If you have multiple meshes you need to process, allocate + // and array which can hold the max number of triangles you need to process. + m_triareas = new unsigned char[ m_chunkyMesh->maxTrisPerChunk ]; + if( !m_triareas ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'm_triareas' (%d).\n", m_chunkyMesh->maxTrisPerChunk ); + return nullptr; + } + + float tbmin[ 2 ]; + float tbmax[ 2 ]; + tbmin[ 0 ] = m_cfg.bmin[ 0 ]; + tbmin[ 1 ] = m_cfg.bmin[ 2 ]; + tbmax[ 0 ] = m_cfg.bmax[ 0 ]; + tbmax[ 1 ] = m_cfg.bmax[ 2 ]; + + int cid[512];// TODO: Make grow when returning too many items. + const int ncid = rcGetChunksOverlappingRect( m_chunkyMesh, tbmin, tbmax, cid, 512 ); + + if( !ncid ) + return nullptr; + + m_tileTriCount = 0; + + for( int i = 0; i < ncid; ++i ) + { + const rcChunkyTriMeshNode& node = m_chunkyMesh->nodes[ cid[ i ] ]; + const int* ctris = &m_chunkyMesh->tris[ node.i * 3 ]; + const int nctris = node.n; + + m_tileTriCount += nctris; + + memset( m_triareas, 0, nctris * sizeof( unsigned char ) ); + rcMarkWalkableTriangles( m_ctx, m_cfg.walkableSlopeAngle, verts, nverts, ctris, nctris, m_triareas ); + if( !rcRasterizeTriangles( m_ctx, verts, nverts, ctris, m_triareas, nctris, *m_solid, m_cfg.walkableClimb ) ) + return nullptr; + } + + delete[] m_triareas; + m_triareas = nullptr; + + // Once all geometry is rasterized, we do initial pass of filtering to + // remove unwanted overhangs caused by the conservative rasterization + // as well as filter spans where the character cannot possibly stand. + rcFilterLowHangingWalkableObstacles( m_ctx, m_cfg.walkableClimb, *m_solid ); + rcFilterLedgeSpans( m_ctx, m_cfg.walkableHeight, m_cfg.walkableClimb, *m_solid ); + rcFilterWalkableLowHeightSpans( m_ctx, m_cfg.walkableHeight, *m_solid ); + + // Compact the heightfield so that it is faster to handle from now on. + // This will result more cache coherent data as well as the neighbours + // between walkable cells will be calculated. + m_chf = rcAllocCompactHeightfield(); + if( !m_chf ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'chf'." ); + return nullptr; + } + if( !rcBuildCompactHeightfield( m_ctx, m_cfg.walkableHeight, m_cfg.walkableClimb, *m_solid, *m_chf ) ) + { + printf( "[Navmesh] buildNavigation: Could not build compact data." ); + return nullptr; + } + + rcFreeHeightField( m_solid ); + m_solid = nullptr; + + // Erode the walkable area by agent radius. + if( !rcErodeWalkableArea( m_ctx, m_cfg.walkableRadius, *m_chf ) ) + { + printf( "[Navmesh] buildNavigation: Could not erode." ); + return nullptr; + } + + // (Optional) Mark areas. +// const ConvexVolume* vols = m_mesh->getConvexVolumes(); +// for (int i = 0; i < m_geom->getConvexVolumeCount(); ++i) +// rcMarkConvexPolyArea(m_ctx, vols[i].verts, vols[i].nverts, vols[i].hmin, vols[i].hmax, (unsigned char)vols[i].area, *m_chf); + + // Partition the heightfield so that we can use simple algorithm later to triangulate the walkable areas. + // There are 3 martitioning methods, each with some pros and cons: + // 1) Watershed partitioning + // - the classic Recast partitioning + // - creates the nicest tessellation + // - usually slowest + // - partitions the heightfield into nice regions without holes or overlaps + // - the are some corner cases where this method creates produces holes and overlaps + // - holes may appear when a small obstacles is close to large open area (triangulation can handle this) + // - overlaps may occur if you have narrow spiral corridors (i.e stairs), this make triangulation to fail + // * generally the best choice if you precompute the nacmesh, use this if you have large open areas + // 2) Monotone partioning + // - fastest + // - partitions the heightfield into regions without holes and overlaps (guaranteed) + // - creates long thin polygons, which sometimes causes paths with detours + // * use this if you want fast navmesh generation + // 3) Layer partitoining + // - quite fast + // - partitions the heighfield into non-overlapping regions + // - relies on the triangulation code to cope with holes (thus slower than monotone partitioning) + // - produces better triangles than monotone partitioning + // - does not have the corner cases of watershed partitioning + // - can be slow and create a bit ugly tessellation (still better than monotone) + // if you have large open areas with small obstacles (not a problem if you use tiles) + // * good choice to use for tiled navmesh with medium and small sized tiles + + if( m_partitionType == SAMPLE_PARTITION_WATERSHED ) + { + // Prepare for region partitioning, by calculating distance field along the walkable surface. + if( !rcBuildDistanceField( m_ctx, *m_chf ) ) + { + printf( "[Navmesh] buildNavigation: Could not build distance field." ); + return nullptr; + } + + // Partition the walkable surface into simple regions without holes. + if( !rcBuildRegions( m_ctx, *m_chf, m_cfg.borderSize, m_cfg.minRegionArea, m_cfg.mergeRegionArea ) ) + { + printf( "[Navmesh] buildNavigation: Could not build watershed regions." ); + return nullptr; + } + } + else if( m_partitionType == SAMPLE_PARTITION_MONOTONE ) + { + // Partition the walkable surface into simple regions without holes. + // Monotone partitioning does not need distancefield. + if( !rcBuildRegionsMonotone( m_ctx, *m_chf, m_cfg.borderSize, m_cfg.minRegionArea, m_cfg.mergeRegionArea ) ) + { + printf( "[Navmesh] buildNavigation: Could not build monotone regions." ); + return nullptr; + } + } + else // SAMPLE_PARTITION_LAYERS + { + // Partition the walkable surface into simple regions without holes. + if( !rcBuildLayerRegions( m_ctx, *m_chf, m_cfg.borderSize, m_cfg.minRegionArea ) ) + { + printf( "[Navmesh] buildNavigation: Could not build layer regions." ); + return nullptr; + } + } + + // Create contours. + m_cset = rcAllocContourSet(); + if( !m_cset ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'cset'." ); + return nullptr; + } + if( !rcBuildContours( m_ctx, *m_chf, m_cfg.maxSimplificationError, m_cfg.maxEdgeLen, *m_cset ) ) + { + printf( "[Navmesh] buildNavigation: Could not create contours." ); + return nullptr; + } + + if( m_cset->nconts == 0 ) + { + return nullptr; + } + + // Build polygon navmesh from the contours. + m_pmesh = rcAllocPolyMesh(); + if( !m_pmesh ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'pmesh'." ); + return nullptr; + } + if( !rcBuildPolyMesh( m_ctx, *m_cset, m_cfg.maxVertsPerPoly, *m_pmesh ) ) + { + printf( "[Navmesh] buildNavigation: Could not triangulate contours." ); + return nullptr; + } + + // Build detail mesh. + m_dmesh = rcAllocPolyMeshDetail(); + if( !m_dmesh ) + { + printf( "[Navmesh] buildNavigation: Out of memory 'dmesh'." ); + return nullptr; + } + + if( !rcBuildPolyMeshDetail( m_ctx, *m_pmesh, *m_chf, + m_cfg.detailSampleDist, m_cfg.detailSampleMaxError, + *m_dmesh ) ) + { + printf( "[Navmesh] buildNavigation: Could build polymesh detail." ); + return nullptr; + } + + rcFreeCompactHeightfield( m_chf ); + rcFreeContourSet( m_cset ); + m_chf = nullptr; + m_cset = nullptr; + + unsigned char* navData = 0; + int navDataSize = 0; + if( m_cfg.maxVertsPerPoly <= DT_VERTS_PER_POLYGON ) + { + if( m_pmesh->nverts >= 0xffff ) + { + // The vertex indices are ushorts, and cannot point to more than 0xffff vertices. + printf( "[Navmesh] Too many vertices per tile %d (max: %d).", m_pmesh->nverts, 0xffff ); + return nullptr; + } + + // Update poly flags from areas. + for( int i = 0; i < m_pmesh->npolys; ++i ) + { + if( m_pmesh->areas[ i ] == RC_WALKABLE_AREA ) + m_pmesh->areas[ i ] = SAMPLE_POLYAREA_GROUND; + + if( m_pmesh->areas[ i ] == SAMPLE_POLYAREA_GROUND || + m_pmesh->areas[ i ] == SAMPLE_POLYAREA_GRASS || + m_pmesh->areas[ i ] == SAMPLE_POLYAREA_ROAD ) + { + m_pmesh->flags[ i ] = SAMPLE_POLYFLAGS_WALK; + } + else if( m_pmesh->areas[ i ] == SAMPLE_POLYAREA_WATER ) + { + m_pmesh->flags[ i ] = SAMPLE_POLYFLAGS_SWIM; + } + else if( m_pmesh->areas[ i ] == SAMPLE_POLYAREA_DOOR ) + { + m_pmesh->flags[ i ] = SAMPLE_POLYFLAGS_WALK | SAMPLE_POLYFLAGS_DOOR; + } + } + + dtNavMeshCreateParams params; + memset( ¶ms, 0, sizeof( params ) ); + params.verts = m_pmesh->verts; + params.vertCount = m_pmesh->nverts; + params.polys = m_pmesh->polys; + params.polyAreas = m_pmesh->areas; + params.polyFlags = m_pmesh->flags; + params.polyCount = m_pmesh->npolys; + params.nvp = m_pmesh->nvp; + params.detailMeshes = m_dmesh->meshes; + params.detailVerts = m_dmesh->verts; + params.detailVertsCount = m_dmesh->nverts; + params.detailTris = m_dmesh->tris; + params.detailTriCount = m_dmesh->ntris; + + params.offMeshConVerts = nullptr; + params.offMeshConRad = nullptr; + params.offMeshConDir = nullptr; + params.offMeshConAreas = nullptr; + params.offMeshConFlags = nullptr; + params.offMeshConUserID = nullptr; + params.offMeshConCount = 0; + + params.walkableHeight = m_agentHeight; + params.walkableRadius = m_agentRadius; + params.walkableClimb = m_agentMaxClimb; + params.tileX = tx; + params.tileY = ty; + params.tileLayer = 0; + rcVcopy( params.bmin, m_pmesh->bmin ); + rcVcopy( params.bmax, m_pmesh->bmax ); + params.cs = m_cfg.cs; + params.ch = m_cfg.ch; + params.buildBvTree = true; + + if( !dtCreateNavMeshData( ¶ms, &navData, &navDataSize ) ) + { + printf( "[Navmesh] Could not build Detour navmesh." ); + return nullptr; + } + } + + rcFreePolyMesh( m_pmesh ); + rcFreePolyMeshDetail( m_dmesh ); + m_pmesh = nullptr; + m_dmesh = nullptr; + + dataSize = navDataSize; + return navData; +} diff --git a/src/tools/pcb_reader/nav/TiledNavmeshGenerator.h b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.h new file mode 100644 index 00000000..64785c23 --- /dev/null +++ b/src/tools/pcb_reader/nav/TiledNavmeshGenerator.h @@ -0,0 +1,124 @@ +#ifndef SAPPHIRE_TILEDNAVMESHGENERATOR_H +#define SAPPHIRE_TILEDNAVMESHGENERATOR_H + +#include +#include +#include + +#include "ext/MeshLoaderObj.h" +#include "ext/ChunkyTriMesh.h" + +#include "recastnavigation/Detour/Include/DetourNavMesh.h" +#include "recastnavigation/Detour/Include/DetourNavMeshQuery.h" +#include "recastnavigation/Recast/Include/Recast.h" + +class TiledNavmeshGenerator +{ +public: + enum SamplePartitionType + { + SAMPLE_PARTITION_WATERSHED, + SAMPLE_PARTITION_MONOTONE, + SAMPLE_PARTITION_LAYERS, + }; + + enum SamplePolyAreas + { + SAMPLE_POLYAREA_GROUND, + SAMPLE_POLYAREA_WATER, + SAMPLE_POLYAREA_ROAD, + SAMPLE_POLYAREA_DOOR, + SAMPLE_POLYAREA_GRASS, + SAMPLE_POLYAREA_JUMP, + }; + enum SamplePolyFlags + { + SAMPLE_POLYFLAGS_WALK = 0x01, // Ability to walk (ground, grass, road) + SAMPLE_POLYFLAGS_SWIM = 0x02, // Ability to swim (water). + SAMPLE_POLYFLAGS_DOOR = 0x04, // Ability to move through doors. + SAMPLE_POLYFLAGS_JUMP = 0x08, // Ability to jump. + SAMPLE_POLYFLAGS_DISABLED = 0x10, // Disabled polygon + SAMPLE_POLYFLAGS_ALL = 0xffff // All abilities. + }; + + static const int NAVMESHSET_MAGIC = 'M'<<24 | 'S'<<16 | 'E'<<8 | 'T'; //'MSET'; + static const int NAVMESHSET_VERSION = 1; + + struct NavMeshSetHeader + { + int magic; + int version; + int numTiles; + dtNavMeshParams params; + }; + + struct NavMeshTileHeader + { + dtTileRef tileRef; + int dataSize; + }; + + + TiledNavmeshGenerator() = default; + ~TiledNavmeshGenerator(); + + bool init( const std::string& path ); + unsigned char* buildTileMesh( const int tx, const int ty, const float* bmin, const float* bmax, int& dataSize ); + bool buildNavmesh(); + void saveNavmesh( const std::string& name ); + +private: + rcConfig m_cfg; + + rcMeshLoaderObj* m_mesh; + rcChunkyTriMesh* m_chunkyMesh; + + rcContext* m_ctx; + dtNavMesh* m_navMesh; + dtNavMeshQuery* m_navQuery; + rcHeightfield* m_solid; + rcContourSet* m_cset; + rcPolyMesh* m_pmesh; + rcPolyMeshDetail* m_dmesh; + + rcCompactHeightfield* m_chf; + + unsigned char* m_triareas; + + int m_maxTiles = 0; + int m_maxPolysPerTile = 0; + + int m_tileTriCount = 0; + + int m_partitionType = SamplePartitionType::SAMPLE_PARTITION_WATERSHED; + + float m_meshBMin[ 3 ]; + float m_meshBMax[ 3 ]; + + float m_lastBuiltTileBmin[ 3 ]; + float m_lastBuiltTileBmax[ 3 ]; + + // options + float m_tileSize = 160.f; + float m_cellSize = 0.2f; + float m_cellHeight = 0.2f; + + float m_agentMaxSlope = 56.f; + float m_agentHeight = 2.f; + float m_agentMaxClimb = 0.6f; + float m_agentRadius = 0.5f; + + float m_regionMinSize = 8.f; + float m_regionMergeSize = 20.f; + + float m_edgeMaxLen = 12.f; + float m_edgeMaxError = 1.4f; + float m_vertsPerPoly = 6.f; + + float m_detailSampleDist = 6.f; + float m_detailSampleMaxError = 1.f; + +}; + + +#endif //SAPPHIRE_TILEDNAVMESHGENERATOR_H diff --git a/src/tools/pcb_reader/nav/ext/ChunkyTriMesh.cpp b/src/tools/pcb_reader/nav/ext/ChunkyTriMesh.cpp new file mode 100644 index 00000000..7b5ef0d6 --- /dev/null +++ b/src/tools/pcb_reader/nav/ext/ChunkyTriMesh.cpp @@ -0,0 +1,331 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#include "ChunkyTriMesh.h" +#include +#include +#include + +struct BoundsItem +{ + float bmin[ 2 ]; + float bmax[ 2 ]; + int i; +}; + +static int compareItemX( const void* va, const void* vb ) +{ + const BoundsItem* a = ( const BoundsItem* ) va; + const BoundsItem* b = ( const BoundsItem* ) vb; + if( a->bmin[ 0 ] < b->bmin[ 0 ] ) + return -1; + if( a->bmin[ 0 ] > b->bmin[ 0 ] ) + return 1; + return 0; +} + +static int compareItemY( const void* va, const void* vb ) +{ + const BoundsItem* a = ( const BoundsItem* ) va; + const BoundsItem* b = ( const BoundsItem* ) vb; + if( a->bmin[ 1 ] < b->bmin[ 1 ] ) + return -1; + if( a->bmin[ 1 ] > b->bmin[ 1 ] ) + return 1; + return 0; +} + +static void calcExtends( const BoundsItem* items, const int /*nitems*/, + const int imin, const int imax, + float* bmin, float* bmax ) +{ + bmin[ 0 ] = items[ imin ].bmin[ 0 ]; + bmin[ 1 ] = items[ imin ].bmin[ 1 ]; + + bmax[ 0 ] = items[ imin ].bmax[ 0 ]; + bmax[ 1 ] = items[ imin ].bmax[ 1 ]; + + for( int i = imin + 1; i < imax; ++i ) + { + const BoundsItem& it = items[ i ]; + if( it.bmin[ 0 ] < bmin[ 0 ] ) + bmin[ 0 ] = it.bmin[ 0 ]; + if( it.bmin[ 1 ] < bmin[ 1 ] ) + bmin[ 1 ] = it.bmin[ 1 ]; + + if( it.bmax[ 0 ] > bmax[ 0 ] ) + bmax[ 0 ] = it.bmax[ 0 ]; + if( it.bmax[ 1 ] > bmax[ 1 ] ) + bmax[ 1 ] = it.bmax[ 1 ]; + } +} + +inline int longestAxis( float x, float y ) +{ + return y > x ? 1 : 0; +} + +static void subdivide( BoundsItem* items, int nitems, int imin, int imax, int trisPerChunk, + int& curNode, rcChunkyTriMeshNode* nodes, const int maxNodes, + int& curTri, int* outTris, const int* inTris ) +{ + int inum = imax - imin; + int icur = curNode; + + if( curNode > maxNodes ) + return; + + rcChunkyTriMeshNode& node = nodes[ curNode++ ]; + + if( inum <= trisPerChunk ) + { + // Leaf + calcExtends( items, nitems, imin, imax, node.bmin, node.bmax ); + + // Copy triangles. + node.i = curTri; + node.n = inum; + + for( int i = imin; i < imax; ++i ) + { + const int* src = &inTris[ items[ i ].i * 3 ]; + int* dst = &outTris[ curTri * 3 ]; + curTri++; + dst[ 0 ] = src[ 0 ]; + dst[ 1 ] = src[ 1 ]; + dst[ 2 ] = src[ 2 ]; + } + } + else + { + // Split + calcExtends( items, nitems, imin, imax, node.bmin, node.bmax ); + + int axis = longestAxis( node.bmax[ 0 ] - node.bmin[ 0 ], + node.bmax[ 1 ] - node.bmin[ 1 ] ); + + if( axis == 0 ) + { + // Sort along x-axis + qsort( items + imin, static_cast(inum), sizeof( BoundsItem ), compareItemX ); + } + else if( axis == 1 ) + { + // Sort along y-axis + qsort( items + imin, static_cast(inum), sizeof( BoundsItem ), compareItemY ); + } + + int isplit = imin + inum / 2; + + // Left + subdivide( items, nitems, imin, isplit, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris ); + // Right + subdivide( items, nitems, isplit, imax, trisPerChunk, curNode, nodes, maxNodes, curTri, outTris, inTris ); + + int iescape = curNode - icur; + // Negative index means escape. + node.i = -iescape; + } +} + +bool rcCreateChunkyTriMesh( const float* verts, const int* tris, int ntris, + int trisPerChunk, rcChunkyTriMesh* cm ) +{ + int nchunks = ( ntris + trisPerChunk - 1 ) / trisPerChunk; + + cm->nodes = new rcChunkyTriMeshNode[nchunks * 4]; + if( !cm->nodes ) + return false; + + cm->tris = new int[ntris * 3]; + if( !cm->tris ) + return false; + + cm->ntris = ntris; + + // Build tree + BoundsItem* items = new BoundsItem[ntris]; + if( !items ) + return false; + + for( int i = 0; i < ntris; i++ ) + { + const int* t = &tris[ i * 3 ]; + BoundsItem& it = items[ i ]; + it.i = i; + // Calc triangle XZ bounds. + it.bmin[ 0 ] = it.bmax[ 0 ] = verts[ t[ 0 ] * 3 + 0 ]; + it.bmin[ 1 ] = it.bmax[ 1 ] = verts[ t[ 0 ] * 3 + 2 ]; + for( int j = 1; j < 3; ++j ) + { + const float* v = &verts[ t[ j ] * 3 ]; + if( v[ 0 ] < it.bmin[ 0 ] ) + it.bmin[ 0 ] = v[ 0 ]; + if( v[ 2 ] < it.bmin[ 1 ] ) + it.bmin[ 1 ] = v[ 2 ]; + + if( v[ 0 ] > it.bmax[ 0 ] ) + it.bmax[ 0 ] = v[ 0 ]; + if( v[ 2 ] > it.bmax[ 1 ] ) + it.bmax[ 1 ] = v[ 2 ]; + } + } + + int curTri = 0; + int curNode = 0; + subdivide( items, ntris, 0, ntris, trisPerChunk, curNode, cm->nodes, nchunks * 4, curTri, cm->tris, tris ); + + delete[] items; + + cm->nnodes = curNode; + + // Calc max tris per node. + cm->maxTrisPerChunk = 0; + for( int i = 0; i < cm->nnodes; ++i ) + { + rcChunkyTriMeshNode& node = cm->nodes[ i ]; + const bool isLeaf = node.i >= 0; + if( !isLeaf ) + continue; + if( node.n > cm->maxTrisPerChunk ) + cm->maxTrisPerChunk = node.n; + } + + return true; +} + + +inline bool checkOverlapRect( const float amin[2], const float amax[2], + const float bmin[2], const float bmax[2] ) +{ + bool overlap = true; + overlap = ( amin[ 0 ] > bmax[ 0 ] || amax[ 0 ] < bmin[ 0 ] ) ? false : overlap; + overlap = ( amin[ 1 ] > bmax[ 1 ] || amax[ 1 ] < bmin[ 1 ] ) ? false : overlap; + return overlap; +} + +int rcGetChunksOverlappingRect( const rcChunkyTriMesh* cm, + float bmin[2], float bmax[2], + int* ids, const int maxIds ) +{ + // Traverse tree + int i = 0; + int n = 0; + while( i < cm->nnodes ) + { + const rcChunkyTriMeshNode* node = &cm->nodes[ i ]; + const bool overlap = checkOverlapRect( bmin, bmax, node->bmin, node->bmax ); + const bool isLeafNode = node->i >= 0; + + if( isLeafNode && overlap ) + { + if( n < maxIds ) + { + ids[ n ] = i; + n++; + } + } + + if( overlap || isLeafNode ) + i++; + else + { + const int escapeIndex = -node->i; + i += escapeIndex; + } + } + + return n; +} + + +static bool checkOverlapSegment( const float p[2], const float q[2], + const float bmin[2], const float bmax[2] ) +{ + static const float EPSILON = 1e-6f; + + float tmin = 0; + float tmax = 1; + float d[2]; + d[ 0 ] = q[ 0 ] - p[ 0 ]; + d[ 1 ] = q[ 1 ] - p[ 1 ]; + + for( int i = 0; i < 2; i++ ) + { + if( fabsf( d[ i ] ) < EPSILON ) + { + // Ray is parallel to slab. No hit if origin not within slab + if( p[ i ] < bmin[ i ] || p[ i ] > bmax[ i ] ) + return false; + } + else + { + // Compute intersection t value of ray with near and far plane of slab + float ood = 1.0f / d[ i ]; + float t1 = ( bmin[ i ] - p[ i ] ) * ood; + float t2 = ( bmax[ i ] - p[ i ] ) * ood; + if( t1 > t2 ) + { + float tmp = t1; + t1 = t2; + t2 = tmp; + } + if( t1 > tmin ) + tmin = t1; + if( t2 < tmax ) + tmax = t2; + if( tmin > tmax ) + return false; + } + } + return true; +} + +int rcGetChunksOverlappingSegment( const rcChunkyTriMesh* cm, + float p[2], float q[2], + int* ids, const int maxIds ) +{ + // Traverse tree + int i = 0; + int n = 0; + while( i < cm->nnodes ) + { + const rcChunkyTriMeshNode* node = &cm->nodes[ i ]; + const bool overlap = checkOverlapSegment( p, q, node->bmin, node->bmax ); + const bool isLeafNode = node->i >= 0; + + if( isLeafNode && overlap ) + { + if( n < maxIds ) + { + ids[ n ] = i; + n++; + } + } + + if( overlap || isLeafNode ) + i++; + else + { + const int escapeIndex = -node->i; + i += escapeIndex; + } + } + + return n; +} diff --git a/src/tools/pcb_reader/nav/ext/ChunkyTriMesh.h b/src/tools/pcb_reader/nav/ext/ChunkyTriMesh.h new file mode 100644 index 00000000..24eb5890 --- /dev/null +++ b/src/tools/pcb_reader/nav/ext/ChunkyTriMesh.h @@ -0,0 +1,68 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef CHUNKYTRIMESH_H +#define CHUNKYTRIMESH_H + +struct rcChunkyTriMeshNode +{ + float bmin[ 2 ]; + float bmax[ 2 ]; + int i; + int n; +}; + +struct rcChunkyTriMesh +{ + inline rcChunkyTriMesh() : + nodes( 0 ), nnodes( 0 ), tris( 0 ), ntris( 0 ), maxTrisPerChunk( 0 ) + { + }; + + inline ~rcChunkyTriMesh() + { + delete[] nodes; + delete[] tris; + } + + rcChunkyTriMeshNode* nodes; + int nnodes; + int* tris; + int ntris; + int maxTrisPerChunk; + +private: + // Explicitly disabled copy constructor and copy assignment operator. + rcChunkyTriMesh( const rcChunkyTriMesh& ); + + rcChunkyTriMesh& operator=( const rcChunkyTriMesh& ); +}; + +/// Creates partitioned triangle mesh (AABB tree), +/// where each node contains at max trisPerChunk triangles. +bool rcCreateChunkyTriMesh( const float* verts, const int* tris, int ntris, + int trisPerChunk, rcChunkyTriMesh* cm ); + +/// Returns the chunk indices which overlap the input rectable. +int rcGetChunksOverlappingRect( const rcChunkyTriMesh* cm, float bmin[2], float bmax[2], int* ids, const int maxIds ); + +/// Returns the chunk indices which overlap the input segment. +int rcGetChunksOverlappingSegment( const rcChunkyTriMesh* cm, float p[2], float q[2], int* ids, const int maxIds ); + + +#endif // CHUNKYTRIMESH_H diff --git a/src/tools/pcb_reader/nav/ext/MeshLoaderObj.cpp b/src/tools/pcb_reader/nav/ext/MeshLoaderObj.cpp new file mode 100644 index 00000000..08c9c7f1 --- /dev/null +++ b/src/tools/pcb_reader/nav/ext/MeshLoaderObj.cpp @@ -0,0 +1,252 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#include "MeshLoaderObj.h" +#include +#include +#include + +#define _USE_MATH_DEFINES + +#include + +rcMeshLoaderObj::rcMeshLoaderObj() : + m_scale( 1.0f ), + m_verts( 0 ), + m_tris( 0 ), + m_normals( 0 ), + m_vertCount( 0 ), + m_triCount( 0 ) +{ +} + +rcMeshLoaderObj::~rcMeshLoaderObj() +{ + delete[] m_verts; + delete[] m_normals; + delete[] m_tris; +} + +void rcMeshLoaderObj::addVertex( float x, float y, float z, int& cap ) +{ + if( m_vertCount + 1 > cap ) + { + cap = !cap ? 8 : cap * 2; + float* nv = new float[cap * 3]; + if( m_vertCount ) + memcpy( nv, m_verts, m_vertCount * 3 * sizeof( float ) ); + delete[] m_verts; + m_verts = nv; + } + float* dst = &m_verts[ m_vertCount * 3 ]; + *dst++ = x * m_scale; + *dst++ = y * m_scale; + *dst++ = z * m_scale; + m_vertCount++; +} + +void rcMeshLoaderObj::addTriangle( int a, int b, int c, int& cap ) +{ + if( m_triCount + 1 > cap ) + { + cap = !cap ? 8 : cap * 2; + int* nv = new int[cap * 3]; + if( m_triCount ) + memcpy( nv, m_tris, m_triCount * 3 * sizeof( int ) ); + delete[] m_tris; + m_tris = nv; + } + int* dst = &m_tris[ m_triCount * 3 ]; + *dst++ = a; + *dst++ = b; + *dst++ = c; + m_triCount++; +} + +static char* parseRow( char* buf, char* bufEnd, char* row, int len ) +{ + bool start = true; + bool done = false; + int n = 0; + while( !done && buf < bufEnd ) + { + char c = *buf; + buf++; + // multirow + switch( c ) + { + case '\\': + break; + case '\n': + if( start ) + break; + done = true; + break; + case '\r': + break; + case '\t': + case ' ': + if( start ) + break; + // else falls through + default: + start = false; + row[ n++ ] = c; + if( n >= len - 1 ) + done = true; + break; + } + } + row[ n ] = '\0'; + return buf; +} + +static int parseFace( char* row, int* data, int n, int vcnt ) +{ + int j = 0; + while( *row != '\0' ) + { + // Skip initial white space + while( *row != '\0' && ( *row == ' ' || *row == '\t' ) ) + row++; + char* s = row; + // Find vertex delimiter and terminated the string there for conversion. + while( *row != '\0' && *row != ' ' && *row != '\t' ) + { + if( *row == '/' ) + *row = '\0'; + row++; + } + if( *s == '\0' ) + continue; + int vi = atoi( s ); + data[ j++ ] = vi < 0 ? vi + vcnt : vi - 1; + if( j >= n ) + return j; + } + return j; +} + +bool rcMeshLoaderObj::load( const std::string& filename ) +{ + char* buf = 0; + FILE* fp = fopen( filename.c_str(), "rb" ); + if( !fp ) + return false; + if( fseek( fp, 0, SEEK_END ) != 0 ) + { + fclose( fp ); + return false; + } + long bufSize = ftell( fp ); + if( bufSize < 0 ) + { + fclose( fp ); + return false; + } + if( fseek( fp, 0, SEEK_SET ) != 0 ) + { + fclose( fp ); + return false; + } + buf = new char[bufSize]; + if( !buf ) + { + fclose( fp ); + return false; + } + size_t readLen = fread( buf, bufSize, 1, fp ); + fclose( fp ); + + if( readLen != 1 ) + { + delete[] buf; + return false; + } + + char* src = buf; + char* srcEnd = buf + bufSize; + char row[512]; + int face[32]; + float x, y, z; + int nv; + int vcap = 0; + int tcap = 0; + + while( src < srcEnd ) + { + // Parse one row + row[ 0 ] = '\0'; + src = parseRow( src, srcEnd, row, sizeof( row ) / sizeof( char ) ); + // Skip comments + if( row[ 0 ] == '#' ) + continue; + if( row[ 0 ] == 'v' && row[ 1 ] != 'n' && row[ 1 ] != 't' ) + { + // Vertex pos + sscanf( row + 1, "%f %f %f", &x, &y, &z ); + addVertex( x, y, z, vcap ); + } + if( row[ 0 ] == 'f' ) + { + // Faces + nv = parseFace( row + 1, face, 32, m_vertCount ); + for( int i = 2; i < nv; ++i ) + { + const int a = face[ 0 ]; + const int b = face[ i - 1 ]; + const int c = face[ i ]; + if( a < 0 || a >= m_vertCount || b < 0 || b >= m_vertCount || c < 0 || c >= m_vertCount ) + continue; + addTriangle( a, b, c, tcap ); + } + } + } + + delete[] buf; + + // Calculate normals. + m_normals = new float[m_triCount * 3]; + for( int i = 0; i < m_triCount * 3; i += 3 ) + { + const float* v0 = &m_verts[ m_tris[ i ] * 3 ]; + const float* v1 = &m_verts[ m_tris[ i + 1 ] * 3 ]; + const float* v2 = &m_verts[ m_tris[ i + 2 ] * 3 ]; + float e0[3], e1[3]; + for( int j = 0; j < 3; ++j ) + { + e0[ j ] = v1[ j ] - v0[ j ]; + e1[ j ] = v2[ j ] - v0[ j ]; + } + float* n = &m_normals[ i ]; + n[ 0 ] = e0[ 1 ] * e1[ 2 ] - e0[ 2 ] * e1[ 1 ]; + n[ 1 ] = e0[ 2 ] * e1[ 0 ] - e0[ 0 ] * e1[ 2 ]; + n[ 2 ] = e0[ 0 ] * e1[ 1 ] - e0[ 1 ] * e1[ 0 ]; + float d = sqrtf( n[ 0 ] * n[ 0 ] + n[ 1 ] * n[ 1 ] + n[ 2 ] * n[ 2 ] ); + if( d > 0 ) + { + d = 1.0f / d; + n[ 0 ] *= d; + n[ 1 ] *= d; + n[ 2 ] *= d; + } + } + + m_filename = filename; + return true; +} diff --git a/src/tools/pcb_reader/nav/ext/MeshLoaderObj.h b/src/tools/pcb_reader/nav/ext/MeshLoaderObj.h new file mode 100644 index 00000000..1b3f9c56 --- /dev/null +++ b/src/tools/pcb_reader/nav/ext/MeshLoaderObj.h @@ -0,0 +1,82 @@ +// +// Copyright (c) 2009-2010 Mikko Mononen memon@inside.org +// +// This software is provided 'as-is', without any express or implied +// warranty. In no event will the authors be held liable for any damages +// arising from the use of this software. +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// 1. The origin of this software must not be misrepresented; you must not +// claim that you wrote the original software. If you use this software +// in a product, an acknowledgment in the product documentation would be +// appreciated but is not required. +// 2. Altered source versions must be plainly marked as such, and must not be +// misrepresented as being the original software. +// 3. This notice may not be removed or altered from any source distribution. +// + +#ifndef MESHLOADER_OBJ +#define MESHLOADER_OBJ + +#include + +class rcMeshLoaderObj +{ +public: + rcMeshLoaderObj(); + + ~rcMeshLoaderObj(); + + bool load( const std::string& fileName ); + + const float* getVerts() const + { + return m_verts; + } + + const float* getNormals() const + { + return m_normals; + } + + const int* getTris() const + { + return m_tris; + } + + int getVertCount() const + { + return m_vertCount; + } + + int getTriCount() const + { + return m_triCount; + } + + const std::string& getFileName() const + { + return m_filename; + } + +private: + // Explicitly disabled copy constructor and copy assignment operator. + rcMeshLoaderObj( const rcMeshLoaderObj& ); + + rcMeshLoaderObj& operator=( const rcMeshLoaderObj& ); + + void addVertex( float x, float y, float z, int& cap ); + + void addTriangle( int a, int b, int c, int& cap ); + + std::string m_filename; + float m_scale; + float* m_verts; + int* m_tris; + float* m_normals; + int m_vertCount; + int m_triCount; +}; + +#endif // MESHLOADER_OBJ diff --git a/src/tools/pcb_reader/navmesh_exporter.h b/src/tools/pcb_reader/navmesh_exporter.h index 7042e9db..8c16c0ad 100644 --- a/src/tools/pcb_reader/navmesh_exporter.h +++ b/src/tools/pcb_reader/navmesh_exporter.h @@ -9,17 +9,12 @@ #include #include "exporter.h" -#include "obj_exporter.h" +#include "nav/TiledNavmeshGenerator.h" -//* -#include -#include -#include -#include -#include -#include +#include + +namespace fs = std::experimental::filesystem; -//*/ class NavmeshExporter { public: @@ -27,368 +22,34 @@ public: { static std::string currPath = std::experimental::filesystem::current_path().string(); - auto start = std::chrono::high_resolution_clock::now(); + auto dir = fs::current_path().string() + "/pcb_export/" + zone.name + "/"; + auto fileName = dir + zone.name + ".obj"; + + TiledNavmeshGenerator gen; + + if( !gen.init( fileName ) ) + { + printf( "[Navmesh] failed to init TiledNavmeshGenerator for file '%s'\n", fileName.c_str() ); + return; + } + + if( !gen.buildNavmesh() ) + { + printf( "[Navmesh] Failed to build navmesh for '%s'\n", zone.name.c_str() ); + return; + } + + gen.saveNavmesh( zone.name ); - auto fileName = currPath + "/" + zone.name + "/" + zone.name + ".nav"; - exportZoneCommandline( zone, deleteObj ); - //for( auto& group : zone.groups ) - //buildTileMesh(group.second, 0, 0); auto end = std::chrono::high_resolution_clock::now(); - printf( "[Navmesh] Finished exporting %s in %u ms\n", + printf( "[Navmesh] Finished exporting %s in %lu ms\n", fileName.c_str(), std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); } - static void exportGroup( const std::string& zoneName, const ExportedGroup& group, bool deleteObj = false ) + static void exportGroup( const std::string& zoneName, const ExportedGroup& group ) { - static std::string currPath = std::experimental::filesystem::current_path().string(); - - auto start = std::chrono::high_resolution_clock::now(); - - auto fileName = currPath + "/" + zoneName + "/" + zoneName + "_" + group.name + ".obj"; - exportGroupCommandline( zoneName, group ); - - auto end = std::chrono::high_resolution_clock::now(); - printf( "[Navmesh] Finished exporting %s in %u ms\n", - fileName.c_str(), - std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); - } -private: - static void exportZoneCommandline( const ExportedZone& zone, bool deleteObj = false ) - { - static std::string currPath = "\"\"" + std::experimental::filesystem::current_path().string(); - - auto fileName = ObjExporter::exportZone( zone ); - if( fileName.empty() ) - { - printf( "Unable to export navmesh for %s", zone.name.c_str() ); - return; - } - static std::string recastDemoLaunch = std::string( "RecastDemo.exe --type tileMesh --obj "); - std::string actualStr( recastDemoLaunch + "\"" + fileName + "\"" ); - system( actualStr.c_str() ); - } - - static void exportGroupCommandline( const std::string& zoneName, const ExportedGroup& group, bool deleteObj = false ) - { - static std::string currPath = "\"\"" + std::experimental::filesystem::current_path().string(); - - auto fileName = ObjExporter::exportGroup( zoneName, group ); - if( fileName.empty() ) - { - printf( "Unable to export navmesh for %s", zoneName.c_str() ); - return; - } - static std::string recastDemoLaunch = std::string( "RecastDemo.exe --type tileMesh --obj "); - std::string actualStr( recastDemoLaunch + "\"" + fileName + "\"" ); - system( actualStr.c_str() ); - } - /*/ - static unsigned char* buildTileMesh( const ExportedGroup& group, int tx, int ty ) - { - unsigned char* navData; - rcConfig cfg; - cfg.ch = 0.2f; - cfg.cs = 0.2f; - cfg.walkableHeight = 2.f; - cfg.walkableRadius = 0.5; - cfg.walkableClimb = 0.6; - cfg.walkableSlopeAngle = 58.f; - cfg.minRegionArea = 8.0f; - cfg.mergeRegionArea = 20.f; - cfg.maxEdgeLen = 12.f; - cfg.maxSimplificationError = 1.4f; - cfg.maxVertsPerPoly = 6.f; - cfg.detailSampleDist = 6.f; - cfg.detailSampleMaxError = 1.f; - cfg.tileSize = 160.f; - - cfg.walkableHeight = (int)ceilf( cfg.walkableHeight / cfg.ch ); - cfg.walkableClimb = (int)floorf( cfg.walkableClimb / cfg.ch ); - cfg.walkableRadius = (int)ceilf( cfg.walkableRadius / cfg.cs ); - cfg.maxEdgeLen = (int)( cfg.maxEdgeLen / cfg.cs ); - cfg.minRegionArea = (int)rcSqr( cfg.minRegionArea ); // Note: area = size*size - cfg.mergeRegionArea = (int)rcSqr( cfg.mergeRegionArea ); // Note: area = size*size - cfg.borderSize = cfg.walkableRadius + 3; // Reserve enough padding. - cfg.width = cfg.tileSize + cfg.borderSize*2; - cfg.height = cfg.tileSize + cfg.borderSize*2; - cfg.detailSampleDist = cfg.detailSampleDist < 0.9f ? 0 : cfg.cs * cfg.detailSampleDist; - cfg.detailSampleMaxError = cfg.ch * cfg.detailSampleMaxError; - - rcContext ctx; - auto hf = rcAllocHeightfield(); - auto chf = rcAllocCompactHeightfield(); - auto cs = rcAllocContourSet(); - auto pmesh = rcAllocPolyMesh(); - auto pdetailmesh = rcAllocPolyMeshDetail(); - - std::vector< float > verts; - std::vector< int > indices; - - int i = 0; - int numIndices = 0; - for( const auto& model : group.models ) - { - for( const auto& mesh : model.second.meshes ) - { - auto size = mesh.verts.size(); - if (!size) - continue; - rcCalcBounds( mesh.verts.data(), size / 3, &cfg.bmin[0], &cfg.bmax[0] ); - verts.resize( verts.size() + size ); - memcpy( &verts[i], mesh.verts.data(), size ); - i += size; - - size = mesh.indices.size(); - indices.resize( indices.size() + size ); - for( auto j = 0; j < mesh.indices.size(); j += 3 ) - { - indices[j] = mesh.indices[j] + numIndices; - indices[j + 1] = mesh.indices[j + 1] + numIndices; - indices[j + 2] = mesh.indices[j + 2] + numIndices; - } - numIndices += size; - } - } - - - if( !rcCreateHeightfield( &ctx, *hf, cfg.width, cfg.height, cfg.bmin, cfg.bmax, cfg.cs, cfg.ch ) ) - { - - } - float tbmin[2], tbmax[2]; - tbmin[0] = cfg.bmin[0]; - tbmin[1] = cfg.bmin[2]; - tbmax[0] = cfg.bmax[0]; - tbmax[1] = cfg.bmax[2]; - int cid[512];// TODO: Make grow when returning too many items. - - - auto tileTriCount = 0; - - // Once all geometry is rasterized, we do initial pass of filtering to - // remove unwanted overhangs caused by the conservative rasterization - // as well as filter spans where the character cannot possibly stand. - - rcFilterLowHangingWalkableObstacles(&ctx, cfg.walkableClimb, *hf); - - rcFilterLedgeSpans(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf); - rcFilterWalkableLowHeightSpans(&ctx, cfg.walkableHeight, *hf); - - // Compact the heightfield so that it is faster to handle from now on. - // This will result more cache coherent data as well as the neighbours - // between walkable cells will be calculated. - chf = rcAllocCompactHeightfield(); - if (!chf) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'chf'."); - return 0; - } - if (!rcBuildCompactHeightfield(&ctx, cfg.walkableHeight, cfg.walkableClimb, *hf, *chf)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build compact data."); - return 0; - } - - - { - rcFreeHeightField(hf); - hf = 0; - } - - // Erode the walkable area by agent radius. - if (!rcErodeWalkableArea(&ctx, cfg.walkableRadius, *chf)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could not erode."); - return 0; - } - - - // Partition the heightfield so that we can use simple algorithm later to triangulate the walkable areas. - // There are 3 martitioning methods, each with some pros and cons: - // 1) Watershed partitioning - // - the classic Recast partitioning - // - creates the nicest tessellation - // - usually slowest - // - partitions the heightfield into nice regions without holes or overlaps - // - the are some corner cases where this method creates produces holes and overlaps - // - holes may appear when a small obstacles is close to large open area (triangulation can handle this) - // - overlaps may occur if you have narrow spiral corridors (i.e stairs), this make triangulation to fail - // * generally the best choice if you precompute the nacmesh, use this if you have large open areas - // 2) Monotone partioning - // - fastest - // - partitions the heightfield into regions without holes and overlaps (guaranteed) - // - creates long thin polygons, which sometimes causes paths with detours - // * use this if you want fast navmesh generation - // 3) Layer partitoining - // - quite fast - // - partitions the heighfield into non-overlapping regions - // - relies on the triangulation code to cope with holes (thus slower than monotone partitioning) - // - produces better triangles than monotone partitioning - // - does not have the corner cases of watershed partitioning - // - can be slow and create a bit ugly tessellation (still better than monotone) - // if you have large open areas with small obstacles (not a problem if you use tiles) - // * good choice to use for tiled navmesh with medium and small sized tiles - - //if (m_partitionType == SAMPLE_PARTITION_WATERSHED) - { - // Prepare for region partitioning, by calculating distance field along the walkable surface. - if (!rcBuildDistanceField(&ctx, *chf)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build distance field."); - return 0; - } - - // Partition the walkable surface into simple regions without holes. - if (!rcBuildRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build watershed regions."); - return 0; - } - } - //else if (m_partitionType == SAMPLE_PARTITION_MONOTONE) - //{ - // // Partition the walkable surface into simple regions without holes. - // // Monotone partitioning does not need distancefield. - // if (!rcBuildRegionsMonotone(&ctx, *chf, cfg.borderSize, cfg.minRegionArea, cfg.mergeRegionArea)) - // { - // ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build monotone regions."); - // return 0; - // } - //} - //else // SAMPLE_PARTITION_LAYERS - //{ - // // Partition the walkable surface into simple regions without holes. - // if (!rcBuildLayerRegions(&ctx, *chf, cfg.borderSize, cfg.minRegionArea)) - // { - // ctx.log(RC_LOG_ERROR, "buildNavigation: Could not build layer regions."); - // return 0; - // } - //} - - // Create contours. - cs = rcAllocContourSet(); - if (!cs) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'cset'."); - return 0; - } - if (!rcBuildContours(&ctx, *chf, cfg.maxSimplificationError, cfg.maxEdgeLen, *cs)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could not create contours."); - return 0; - } - - if (cs->nconts == 0) - { - return 0; - } - - // Build polygon navmesh from the contours. - pmesh = rcAllocPolyMesh(); - if (!pmesh) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'pmesh'."); - return 0; - } - if (!rcBuildPolyMesh(&ctx, *cs, cfg.maxVertsPerPoly, *pmesh)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could not triangulate contours."); - return 0; - } - - // Build detail mesh. - pdetailmesh = rcAllocPolyMeshDetail(); - if (!pdetailmesh) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Out of memory 'dmesh'."); - return 0; - } - - if (!rcBuildPolyMeshDetail(&ctx, *pmesh, *chf, - cfg.detailSampleDist, cfg.detailSampleMaxError, - *pdetailmesh)) - { - ctx.log(RC_LOG_ERROR, "buildNavigation: Could build polymesh detail."); - return 0; - } - - { - rcFreeCompactHeightfield(chf); - chf = 0; - rcFreeContourSet(cs); - cs = 0; - } - - int navDataSize = 0; - if (cfg.maxVertsPerPoly <= DT_VERTS_PER_POLYGON) - { - if (pmesh->nverts >= 0xffff) - { - // The vertex indices are ushorts, and cannot point to more than 0xffff vertices. - ctx.log(RC_LOG_ERROR, "Too many vertices per tile %d (max: %d).", pmesh->nverts, 0xffff); - return 0; - } - - // Update poly flags from areas. - for (int i = 0; i < pmesh->npolys; ++i) - { - //pmesh->flags[i] = sampleAreaToFlags(pmesh->areas[i]); - } - - dtNavMeshCreateParams params; - memset(¶ms, 0, sizeof(params)); - params.verts = pmesh->verts; - params.vertCount = pmesh->nverts; - params.polys = pmesh->polys; - params.polyAreas = pmesh->areas; - params.polyFlags = pmesh->flags; - params.polyCount = pmesh->npolys; - params.nvp = pmesh->nvp; - params.detailMeshes = pdetailmesh->meshes; - params.detailVerts = pdetailmesh->verts; - params.detailVertsCount = pdetailmesh->nverts; - params.detailTris = pdetailmesh->tris; - params.detailTriCount = pdetailmesh->ntris; - params.offMeshConVerts = 0; - params.offMeshConRad = 0; - params.offMeshConDir = 0; - params.offMeshConAreas = 0; - params.offMeshConFlags = 0; - params.offMeshConUserID = 0; - params.offMeshConCount = 0; - params.walkableHeight = cfg.walkableHeight; - params.walkableRadius = cfg.walkableRadius; - params.walkableClimb = cfg.walkableClimb; - params.tileX = 0; - params.tileY = 0; - params.tileLayer = 0; - rcVcopy(params.bmin, pmesh->bmin); - rcVcopy(params.bmax, pmesh->bmax); - params.cs = cfg.cs; - params.ch = cfg.ch; - params.buildBvTree = true; - - if (!dtCreateNavMeshData(¶ms, &navData, &navDataSize)) - { - ctx.log(RC_LOG_ERROR, "Could not build Detour navmesh."); - return 0; - } - } - auto tileMemUsage = navDataSize/1024.0f; - - ctx.stopTimer(RC_TIMER_TOTAL); - - // Show performance stats. - //duLogBuildTimes(*&ctx, ctx.getAccumulatedTime(RC_TIMER_TOTAL)); - ctx.log(RC_LOG_PROGRESS, ">> Polymesh: %d vertices %d polygons", pmesh->nverts, pmesh->npolys); - - auto tileBuildTime = ctx.getAccumulatedTime(RC_TIMER_TOTAL)/1000.0f; - - auto dataSize = navDataSize; - return navData; } - //*/ }; #endif // !OBJ_EXPORTER_H diff --git a/src/tools/pcb_reader/obj_exporter.h b/src/tools/pcb_reader/obj_exporter.h index dde22604..b01ad970 100644 --- a/src/tools/pcb_reader/obj_exporter.h +++ b/src/tools/pcb_reader/obj_exporter.h @@ -20,14 +20,14 @@ public: auto start = std::chrono::high_resolution_clock::now(); - auto dir = currPath + "/" + zone.name + "/"; - auto fileName = dir + "/" + zone.name + ".obj"; + auto dir = currPath + "/pcb_export/" + zone.name + "/"; + auto fileName = dir + zone.name + ".obj"; std::error_code e; if( !std::experimental::filesystem::exists( dir, e ) ) { - if( !std::experimental::filesystem::create_directory( dir, e ) ) + if( !std::experimental::filesystem::create_directories( dir, e ) ) { printf( "Unable to create directory '%s'", ( dir ).c_str() ); return ""; @@ -50,9 +50,10 @@ public: } auto end = std::chrono::high_resolution_clock::now(); - printf( "[Obj] Finished exporting %s in %u ms\n", - fileName.c_str(), - std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); + + printf( "[Obj] Finished exporting %s in %lu ms\n", + fileName.substr( fileName.find( "pcb_export" ) - 1 ).c_str(), + std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); return fileName; } @@ -62,13 +63,13 @@ public: auto start = std::chrono::high_resolution_clock::now(); - auto dir = currPath + "/" + zoneName + "/"; - auto fileName = dir + "/" + group.name + ".obj"; + auto dir = currPath + "/pcb_export/" + zoneName + "/groups/"; + auto fileName = dir + group.name + ".obj"; std::error_code e; if( !std::experimental::filesystem::exists( dir, e ) ) { - if( !std::experimental::filesystem::create_directory( dir, e ) ) + if( !std::experimental::filesystem::create_directories( dir, e ) ) { printf( "Unable to create directory '%s'", ( dir ).c_str() ); return ""; @@ -88,9 +89,10 @@ public: } auto end = std::chrono::high_resolution_clock::now(); - printf( "[Obj] Finished exporting %s in %u ms\n", - fileName.c_str(), - std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); + printf( "[Obj] Finished exporting %s in %lu ms\n", + fileName.substr( fileName.find( "pcb_export" ) - 1 ).c_str(), + std::chrono::duration_cast< std::chrono::milliseconds >( end - start ).count() ); + return fileName; } private: diff --git a/src/world/Actor/BNpc.cpp b/src/world/Actor/BNpc.cpp index 86b5f402..53b15876 100644 --- a/src/world/Actor/BNpc.cpp +++ b/src/world/Actor/BNpc.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "Forwards.h" #include "Action/Action.h" @@ -19,6 +20,7 @@ #include "Network/PacketWrappers/UpdateHpMpTpPacket.h" #include "Network/PacketWrappers/NpcSpawnPacket.h" #include "Network/PacketWrappers/MoveActorPacket.h" +#include "Navi/NaviProvider.h" #include "StatusEffect/StatusEffect.h" #include "Action/ActionCollision.h" @@ -31,6 +33,9 @@ #include "BNpcTemplate.h" #include "Manager/TerritoryMgr.h" #include "Common.h" +#include "Framework.h" +#include +#include using namespace Sapphire::Common; using namespace Sapphire::Network::Packets; @@ -67,12 +72,15 @@ Sapphire::Entity::BNpc::BNpc( uint32_t id, BNpcTemplatePtr pTemplate, float posX m_spawnPos = m_pos; + m_timeOfDeath = 0; + m_maxHp = maxHp; m_maxMp = 200; m_hp = maxHp; m_mp = 200; m_state = BNpcState::Idle; + m_status = ActorStatus::Idle; m_baseStats.max_hp = maxHp; m_baseStats.max_mp = 200; @@ -128,12 +136,13 @@ uint32_t Sapphire::Entity::BNpc::getBNpcNameId() const void Sapphire::Entity::BNpc::spawn( PlayerPtr pTarget ) { - pTarget->queuePacket( std::make_shared< NpcSpawnPacket >( *getAsBNpc(), *pTarget ) ); + pTarget->queuePacket( std::make_shared< NpcSpawnPacket >( *this, *pTarget ) ); } void Sapphire::Entity::BNpc::despawn( PlayerPtr pTarget ) { pTarget->freePlayerSpawnId( getId() ); + pTarget->queuePacket( makeActorControl143( m_id, DespawnZoneScreenMsg, 0x04, getId(), 0x01 ) ); } Sapphire::Entity::BNpcState Sapphire::Entity::BNpc::getState() const @@ -146,38 +155,88 @@ void Sapphire::Entity::BNpc::setState( BNpcState state ) m_state = state; } +void Sapphire::Entity::BNpc::step() +{ + if( m_naviLastPath.empty() ) + // No path to track + return; + + auto stepPos = m_naviLastPath[ m_naviPathStep ]; + + if( Util::distance( getPos().x, getPos().y, getPos().z, stepPos.x, stepPos.y, stepPos.z ) <= 4 && + m_naviPathStep < m_naviLastPath.size() - 1 ) + { + // Reached step in path + m_naviPathStep++; + stepPos = m_naviLastPath[ m_naviPathStep ]; + } + + // This is probably not a good way to do it but works fine for now + float angle = Util::calcAngFrom( getPos().x, getPos().z, stepPos.x, stepPos.z ) + PI; + + auto x = ( cosf( angle ) * .5f ); + auto y = stepPos.y; + auto z = ( sinf( angle ) * .5f ); + + face( stepPos ); + setPos( { getPos().x + x, y, getPos().z + z } ); + + sendPositionUpdate(); +} + bool Sapphire::Entity::BNpc::moveTo( const FFXIVARR_POSITION3& pos ) { if( Util::distance( getPos().x, getPos().y, getPos().z, pos.x, pos.y, pos.z ) <= 4 ) - // reached destination + { + // Reached destination + m_naviLastPath.clear(); return true; + } - float rot = Util::calcAngFrom( getPos().x, getPos().z, pos.x, pos.z ); - float newRot = PI - rot + ( PI / 2 ); + // Check if we have to recalculate + if( Util::getTimeMs() - m_naviLastUpdate > 500 ) + { + auto pNaviMgr = m_pFw->get< World::Manager::NaviMgr >(); + auto pNaviProvider = pNaviMgr->getNaviProvider( m_pCurrentZone->getBgPath() ); - face( pos ); - float angle = Util::calcAngFrom( getPos().x, getPos().z, pos.x, pos.z ) + PI; + if( !pNaviProvider ) + { + Logger::error( "No NaviProvider for zone#{0} - {1}", m_pCurrentZone->getGuId(), m_pCurrentZone->getInternalName() ); + return false; + } - auto x = ( cosf( angle ) * 1.1f ); - auto y = ( getPos().y + pos.y ) * 0.5f; // fake value while there is no collision - auto z = ( sinf( angle ) * 1.1f ); + auto path = pNaviProvider->findFollowPath( m_pos, pos ); - Common::FFXIVARR_POSITION3 newPos{ getPos().x + x, y, getPos().z + z }; - setPos( newPos ); + if( !path.empty() ) + { + m_naviLastPath = path; + m_naviTarget = pos; + m_naviPathStep = 0; + m_naviLastUpdate = Util::getTimeMs(); + } + else + { + Logger::debug( "No path found from x{0} y{1} z{2} to x{3} y{4} z{5} in {6}", + getPos().x, getPos().y, getPos().z, pos.x, pos.y, pos.z, m_pCurrentZone->getInternalName() ); + } + } - Common::FFXIVARR_POSITION3 tmpPos{ getPos().x + x, y, getPos().z + z }; - setPos( tmpPos ); - setRot( newRot ); - - sendPositionUpdate(); + step(); return false; } void Sapphire::Entity::BNpc::sendPositionUpdate() { - auto movePacket = std::make_shared< MoveActorPacket >( *getAsChara(), 0x3A, 0, 0, 0x5A ); + uint8_t unk1 = 0x3a; + uint8_t animationType = 2; + + if( m_state == BNpcState::Combat ) + animationType = 0; + + + auto movePacket = std::make_shared< MoveActorPacket >( *getAsChara(), unk1, animationType, 0, 0x5A ); sendToInRangeSet( movePacket ); } @@ -278,7 +337,7 @@ void Sapphire::Entity::BNpc::aggro( Sapphire::Entity::CharaPtr pChara ) if( pChara->isPlayer() ) { PlayerPtr tmpPlayer = pChara->getAsPlayer(); - tmpPlayer->queuePacket( makeActorControl142( getId(), ActorControlType::ToggleWeapon, 0, 1, 1 ) ); + tmpPlayer->queuePacket( makeActorControl142( getId(), ActorControlType::ToggleWeapon, 1, 1, 1 ) ); tmpPlayer->onMobAggro( getAsBNpc() ); } } @@ -291,6 +350,7 @@ void Sapphire::Entity::BNpc::deaggro( Sapphire::Entity::CharaPtr pChara ) if( pChara->isPlayer() ) { PlayerPtr tmpPlayer = pChara->getAsPlayer(); + tmpPlayer->queuePacket( makeActorControl142( getId(), ActorControlType::ToggleWeapon, 0, 1, 1 ) ); tmpPlayer->onMobDeaggro( getAsBNpc() ); } } @@ -299,22 +359,82 @@ void Sapphire::Entity::BNpc::update( int64_t currTime ) { const uint8_t minActorDistance = 4; const uint8_t aggroRange = 8; - const uint8_t maxDistanceToOrigin = 30; - - if( m_status == ActorStatus::Dead ) - return; + const uint8_t maxDistanceToOrigin = 40; + const uint32_t roamTick = 20; switch( m_state ) { + case BNpcState::Dead: + case BNpcState::JustDied: + return; + case BNpcState::Retreat: { + setInvincibilityType( InvincibilityType::InvincibilityIgnoreDamage ); + + // slowly restore hp every tick + + if( std::difftime( currTime, m_lastTickTime ) > 3000 ) + { + m_lastTickTime = currTime; + + if( m_hp < getMaxHp() ) + { + auto addHp = static_cast< uint32_t >( getMaxHp() * 0.1f + 1 ); + + if( m_hp + addHp < getMaxHp() ) + m_hp += addHp; + else + m_hp = getMaxHp(); + } + + sendStatusUpdate(); + } + if( moveTo( m_spawnPos ) ) + { + setInvincibilityType( InvincibilityType::InvincibilityNone ); + + // retail doesn't seem to roam straight after retreating + // todo: perhaps requires more investigation? + m_lastRoamTargetReached = Util::getTimeSeconds(); + + setHp( getMaxHp() ); + m_state = BNpcState::Idle; + } + } + break; + + case BNpcState::Roaming: + { + if( moveTo( m_roamPos ) ) + { + m_lastRoamTargetReached = Util::getTimeSeconds(); + m_state = BNpcState::Idle; + } + + // checkaggro } break; case BNpcState::Idle: { + if( Util::getTimeSeconds() - m_lastRoamTargetReached > roamTick ) + { + auto pNaviMgr = m_pFw->get< World::Manager::NaviMgr >(); + auto pNaviProvider = pNaviMgr->getNaviProvider( m_pCurrentZone->getBgPath() ); + + if( !pNaviProvider ) + { + m_lastRoamTargetReached = Util::getTimeSeconds(); + break; + } + + m_roamPos = pNaviProvider->findRandomPositionInCircle( m_spawnPos, 5 ); + m_state = BNpcState::Roaming; + } + // passive mobs should ignore players unless aggro'd if( m_aggressionMode == 1 ) return; @@ -330,8 +450,6 @@ void Sapphire::Entity::BNpc::update( int64_t currTime ) if( distance < aggroRange && pClosestChara->isPlayer() ) aggro( pClosestChara ); - //if( distance < aggroRange && getbehavior() == 2 ) - // aggro( pClosestActor ); } } @@ -389,3 +507,31 @@ void Sapphire::Entity::BNpc::update( int64_t currTime ) } } } + +void Sapphire::Entity::BNpc::onActionHostile( Sapphire::Entity::CharaPtr pSource ) +{ + if( !hateListGetHighest() ) + aggro( pSource ); + + //if( !getClaimer() ) + // setOwner( pSource->getAsPlayer() ); +} + +void Sapphire::Entity::BNpc::onDeath() +{ + setTargetId( INVALID_GAME_OBJECT_ID ); + m_currentStance = Stance::Passive; + m_state = BNpcState::Dead; + m_timeOfDeath = Util::getTimeSeconds(); + hateListClear(); +} + +uint32_t Sapphire::Entity::BNpc::getTimeOfDeath() const +{ + return m_timeOfDeath; +} + +void Sapphire::Entity::BNpc::setTimeOfDeath( uint32_t timeOfDeath ) +{ + m_timeOfDeath = timeOfDeath; +} diff --git a/src/world/Actor/BNpc.h b/src/world/Actor/BNpc.h index 04a39164..cce71450 100644 --- a/src/world/Actor/BNpc.h +++ b/src/world/Actor/BNpc.h @@ -24,6 +24,7 @@ namespace Sapphire::Entity Idle, Combat, Retreat, + Roaming, JustDied, Dead, }; @@ -62,6 +63,9 @@ namespace Sapphire::Entity // return true if it reached the position bool moveTo( const Common::FFXIVARR_POSITION3& pos ); + // processes movement + void step(); + void sendPositionUpdate(); BNpcState getState() const; @@ -79,6 +83,13 @@ namespace Sapphire::Entity void update( int64_t currTime ) override; + void onActionHostile( CharaPtr pSource ) override; + + void onDeath() override; + + uint32_t getTimeOfDeath() const; + void setTimeOfDeath( uint32_t timeOfDeath ); + private: uint32_t m_bNpcBaseId; uint32_t m_bNpcNameId; @@ -92,11 +103,20 @@ namespace Sapphire::Entity uint32_t m_displayFlags; uint8_t m_level; + uint32_t m_timeOfDeath; + uint32_t m_lastRoamTargetReached; + Common::FFXIVARR_POSITION3 m_spawnPos; + Common::FFXIVARR_POSITION3 m_roamPos; BNpcState m_state; std::set< std::shared_ptr< HateListEntry > > m_hateList; + uint64_t m_naviLastUpdate; + std::vector< Common::FFXIVARR_POSITION3 > m_naviLastPath; + uint8_t m_naviPathStep; + Common::FFXIVARR_POSITION3 m_naviTarget; + }; } diff --git a/src/world/Actor/Chara.cpp b/src/world/Actor/Chara.cpp index 3a42f836..6e2272cc 100644 --- a/src/world/Actor/Chara.cpp +++ b/src/world/Actor/Chara.cpp @@ -151,35 +151,35 @@ uint32_t Sapphire::Entity::Chara::getMaxMp() const void Sapphire::Entity::Chara::resetHp() { m_hp = getMaxHp(); - sendStatusUpdate( true ); + sendStatusUpdate(); } /*! \return reset mp to current max mp */ void Sapphire::Entity::Chara::resetMp() { m_mp = getMaxMp(); - sendStatusUpdate( true ); + sendStatusUpdate(); } /*! \param hp amount to set ( caps to maxHp ) */ void Sapphire::Entity::Chara::setHp( uint32_t hp ) { m_hp = hp < getMaxHp() ? hp : getMaxHp(); - sendStatusUpdate( true ); + sendStatusUpdate(); } /*! \param mp amount to set ( caps to maxMp ) */ void Sapphire::Entity::Chara::setMp( uint32_t mp ) { m_mp = mp < getMaxMp() ? mp : getMaxMp(); - sendStatusUpdate( true ); + sendStatusUpdate(); } /*! \param gp amount to set*/ void Sapphire::Entity::Chara::setGp( uint32_t gp ) { m_gp = gp; - sendStatusUpdate( true ); + sendStatusUpdate(); } /*! \param type invincibility type to set */ @@ -244,7 +244,7 @@ bool Sapphire::Entity::Chara::face( const Common::FFXIVARR_POSITION3& p ) setRot( newRot ); - return oldRot != newRot ? true : false; + return oldRot != newRot; } /*! @@ -325,12 +325,14 @@ void Sapphire::Entity::Chara::takeDamage( uint32_t damage ) case InvincibilityStayAlive: setHp( 0 ); break; + case InvincibilityIgnoreDamage: + break; } } else m_hp -= damage; - sendStatusUpdate( false ); + sendStatusUpdate(); } /*! @@ -349,7 +351,7 @@ void Sapphire::Entity::Chara::heal( uint32_t amount ) else m_hp += amount; - sendStatusUpdate( false ); + sendStatusUpdate(); } /*! @@ -359,7 +361,7 @@ so players can have their own version and we can abolish the param. \param true if the update should also be sent to the actor ( player ) himself */ -void Sapphire::Entity::Chara::sendStatusUpdate( bool toSelf ) +void Sapphire::Entity::Chara::sendStatusUpdate() { FFXIVPacketBasePtr packet = std::make_shared< UpdateHpMpTpPacket >( *this ); sendToInRangeSet( packet ); @@ -391,9 +393,10 @@ void Sapphire::Entity::Chara::autoAttack( CharaPtr pTarget ) uint64_t tick = Util::getTimeMs(); + // todo: this needs to use the auto attack delay for the equipped weapon if( ( tick - m_lastAttack ) > 2500 ) { - pTarget->onActionHostile( *this ); + pTarget->onActionHostile( getAsChara() ); m_lastAttack = tick; srand( static_cast< uint32_t >( tick ) ); @@ -461,7 +464,7 @@ void Sapphire::Entity::Chara::handleScriptSkill( uint32_t type, uint16_t actionI sendToInRangeSet( effectPacket, true ); if( target.isAlive() ) - target.onActionHostile( *this ); + target.onActionHostile( getAsChara() ); target.takeDamage( static_cast< uint32_t >( param1 ) ); @@ -481,7 +484,7 @@ void Sapphire::Entity::Chara::handleScriptSkill( uint32_t type, uint16_t actionI if( pHitActor->getAsChara()->isAlive() ) - pHitActor->getAsChara()->onActionHostile( *this ); + pHitActor->getAsChara()->onActionHostile( getAsChara() ); pHitActor->getAsChara()->takeDamage( static_cast< uint32_t >( param1 ) ); diff --git a/src/world/Actor/Chara.h b/src/world/Actor/Chara.h index 6d8d0748..1adf919b 100644 --- a/src/world/Actor/Chara.h +++ b/src/world/Actor/Chara.h @@ -219,7 +219,7 @@ namespace Sapphire::Entity virtual void onDamageTaken( Chara& pSource ) {}; - virtual void onActionHostile( Chara& source ) {}; + virtual void onActionHostile( CharaPtr pSource ) {}; virtual void onActionFriendly( Chara& pSource ) {}; @@ -229,7 +229,7 @@ namespace Sapphire::Entity virtual uint8_t getLevel() const; - virtual void sendStatusUpdate( bool toSelf = true ); + virtual void sendStatusUpdate(); virtual void takeDamage( uint32_t damage ); diff --git a/src/world/Actor/Player.cpp b/src/world/Actor/Player.cpp index aef8cbe2..db5e56fb 100644 --- a/src/world/Actor/Player.cpp +++ b/src/world/Actor/Player.cpp @@ -739,7 +739,7 @@ void Sapphire::Entity::Player::gainLevel() } -void Sapphire::Entity::Player::sendStatusUpdate( bool toSelf ) +void Sapphire::Entity::Player::sendStatusUpdate() { sendToInRangeSet( std::make_shared< UpdateHpMpTpPacket >( *this ), true ); } @@ -810,7 +810,7 @@ void Sapphire::Entity::Player::setClassJob( Common::ClassJob classJob ) sendToInRangeSet( makeActorControl142( getId(), ClassJobChange, 0x04 ), true ); - sendStatusUpdate( true ); + sendStatusUpdate(); } void Sapphire::Entity::Player::setLevel( uint8_t level ) @@ -1515,7 +1515,7 @@ void Sapphire::Entity::Player::autoAttack( CharaPtr pTarget ) auto mainWeap = getItemAt( Common::GearSet0, Common::GearSetSlot::MainHand ); - pTarget->onActionHostile( *this ); + pTarget->onActionHostile( getAsChara() ); //uint64_t tick = Util::getTimeMs(); //srand(static_cast< uint32_t >(tick)); diff --git a/src/world/Actor/Player.h b/src/world/Actor/Player.h index 3c4382da..02e1c282 100644 --- a/src/world/Actor/Player.h +++ b/src/world/Actor/Player.h @@ -707,7 +707,7 @@ namespace Sapphire::Entity void sendStateFlags(); /*! send status update */ - void sendStatusUpdate( bool toSelf = true ) override; + void sendStatusUpdate() override; /*! send the entire inventory sequence */ void sendInventory(); diff --git a/src/world/CMakeLists.txt b/src/world/CMakeLists.txt index 7d25aae6..7954ae70 100644 --- a/src/world/CMakeLists.txt +++ b/src/world/CMakeLists.txt @@ -19,7 +19,8 @@ file( GLOB SERVER_SOURCE_FILES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} Script/*.c* StatusEffect/*.c* Territory/*.c* - Territory/Housing/*.c*) + Territory/Housing/*.c* + Navi/*.c*) add_executable( world ${SERVER_SOURCE_FILES} ) @@ -30,10 +31,12 @@ set_target_properties( world target_link_libraries( world PUBLIC - common ) + common + Detour) target_include_directories( world PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}" ) + "${CMAKE_CURRENT_SOURCE_DIR}" + Detour ) if( UNIX ) diff --git a/src/world/ForwardsZone.h b/src/world/ForwardsZone.h index 4437ac84..46140156 100644 --- a/src/world/ForwardsZone.h +++ b/src/world/ForwardsZone.h @@ -35,6 +35,11 @@ namespace World TYPE_FORWARD( Session ); } +namespace World::Navi +{ +TYPE_FORWARD( NaviProvider ); +} + namespace World::Territory::Housing { TYPE_FORWARD( HousingInteriorTerritory ); diff --git a/src/world/Manager/DebugCommandMgr.cpp b/src/world/Manager/DebugCommandMgr.cpp index 957b1320..e79e3d2c 100644 --- a/src/world/Manager/DebugCommandMgr.cpp +++ b/src/world/Manager/DebugCommandMgr.cpp @@ -367,6 +367,18 @@ void Sapphire::World::Manager::DebugCommandMgr::set( char* data, Entity::Player& } } } + else if( subCommand == "mobaggro" ) + { + auto inRange = player.getInRangeActors(); + + for( auto actor : inRange ) + { + if( actor->getId() == player.getTargetId() && actor->getAsChara()->isAlive() ) + { + actor->getAsBNpc()->onActionHostile( player.getAsChara() ); + } + } + } else { player.sendUrgent( "{0} is not a valid SET command.", subCommand ); diff --git a/src/world/Manager/NaviMgr.cpp b/src/world/Manager/NaviMgr.cpp new file mode 100644 index 00000000..c3490920 --- /dev/null +++ b/src/world/Manager/NaviMgr.cpp @@ -0,0 +1,47 @@ +#include "NaviMgr.h" +#include "Navi/NaviProvider.h" +#include + +Sapphire::World::Manager::NaviMgr::NaviMgr( FrameworkPtr pFw ) : + BaseManager( pFw ), + m_pFw( pFw ) +{ +} + +bool Sapphire::World::Manager::NaviMgr::setupTerritory( const std::string& bgPath ) +{ + std::string bg = getBgName( bgPath ); + + // check if a provider exists already + if( m_naviProviderTerritoryMap.find( bg ) != m_naviProviderTerritoryMap.end() ) + return true; + + auto provider = Navi::make_NaviProvider( bg, m_pFw ); + + if( provider->init() ) + { + m_naviProviderTerritoryMap.insert( std::make_pair( bg, provider ) ); + return true; + } + + return false; +} + +Sapphire::World::Navi::NaviProviderPtr Sapphire::World::Manager::NaviMgr::getNaviProvider( const std::string& bgPath ) +{ + std::string bg = getBgName( bgPath ); + + if( m_naviProviderTerritoryMap.find( bg ) != m_naviProviderTerritoryMap.end() ) + return m_naviProviderTerritoryMap[ bg ]; + + return nullptr; +} + +std::string Sapphire::World::Manager::NaviMgr::getBgName( const std::string& bgPath ) +{ + auto findPos = bgPath.find_last_of( "/" ); + if( findPos != std::string::npos ) + return bgPath.substr( findPos + 1 ); + + return ""; +} diff --git a/src/world/Manager/NaviMgr.h b/src/world/Manager/NaviMgr.h new file mode 100644 index 00000000..6aeac713 --- /dev/null +++ b/src/world/Manager/NaviMgr.h @@ -0,0 +1,32 @@ +#ifndef SAPPHIRE_NAVIMGR_H +#define SAPPHIRE_NAVIMGR_H + +#include "ForwardsZone.h" +#include "BaseManager.h" + +#include + +namespace Sapphire::World::Manager +{ + class NaviMgr : public BaseManager + { + + public: + + NaviMgr( FrameworkPtr pFw ); + virtual ~NaviMgr() = default; + + bool setupTerritory( const std::string& bgPath ); + Navi::NaviProviderPtr getNaviProvider( const std::string& bgPath ); + + private: + FrameworkPtr m_pFw; + + std::string getBgName( const std::string& bgPath ); + + std::unordered_map< std::string, Navi::NaviProviderPtr > m_naviProviderTerritoryMap; + }; + +} + +#endif // SAPPHIRE_NAVIMGR_H diff --git a/src/world/Manager/TerritoryMgr.cpp b/src/world/Manager/TerritoryMgr.cpp index 81c1aa96..2c2edb83 100644 --- a/src/world/Manager/TerritoryMgr.cpp +++ b/src/world/Manager/TerritoryMgr.cpp @@ -16,6 +16,7 @@ #include "Territory/Land.h" #include "Territory/House.h" #include "Territory/Housing/HousingInteriorTerritory.h" +#include "NaviMgr.h" Sapphire::World::Manager::TerritoryMgr::TerritoryMgr( Sapphire::FrameworkPtr pFw ) : BaseManager( pFw ), @@ -163,12 +164,17 @@ bool Sapphire::World::Manager::TerritoryMgr::createDefaultTerritories() uint32_t guid = getNextInstanceId(); - Logger::info( "{0}\t{1}\t{2}\t{3:<10}\t{4}\t{5}", + auto pNaviMgr = framework()->get< Manager::NaviMgr >(); + std::string bgPath = territoryInfo->bg; + bool hasNaviMesh = pNaviMgr->setupTerritory( bgPath ); + + Logger::info( "{0}\t{1}\t{2}\t{3:<10}\t{4}\t{5}\t{6}", territoryTypeId, guid, territoryInfo->territoryIntendedUse, territoryInfo->name, ( isPrivateTerritory( territoryTypeId ) ? "PRIVATE" : "PUBLIC" ), + hasNaviMesh ? "NAVI" : "", pPlaceName->name ); auto pZone = make_Zone( territoryTypeId, guid, territoryInfo->name, pPlaceName->name, framework() ); @@ -194,7 +200,7 @@ bool Sapphire::World::Manager::TerritoryMgr::createHousingTerritories() auto territoryTypeId = territory.first; auto territoryInfo = territory.second; uint32_t wardNum; - uint32_t wardMaxNum = 1; + uint32_t wardMaxNum = 18; if( territoryInfo->name.empty() ) continue; @@ -208,7 +214,7 @@ bool Sapphire::World::Manager::TerritoryMgr::createHousingTerritories() { uint32_t guid = getNextInstanceId(); - Logger::info( "{0}\t{1}\t{2}\t{3:<10}\tHOUSING\t{4}#{5}", + Logger::info( "{0}\t{1}\t{2}\t{3:<10}\tHOUSING\t\t{4}#{5}", territoryTypeId, guid, territoryInfo->territoryIntendedUse, @@ -219,7 +225,6 @@ bool Sapphire::World::Manager::TerritoryMgr::createHousingTerritories() auto pHousingZone = make_HousingZone( wardNum, territoryTypeId, guid, territoryInfo->name, pPlaceName->name, framework() ); pHousingZone->init(); - wardMaxNum = 18; InstanceIdToZonePtrMap instanceMap; instanceMap[ guid ] = pHousingZone; diff --git a/src/world/Navi/NaviProvider.cpp b/src/world/Navi/NaviProvider.cpp new file mode 100644 index 00000000..f6ce1537 --- /dev/null +++ b/src/world/Navi/NaviProvider.cpp @@ -0,0 +1,523 @@ +#include +#include +#include +#include +#include + +#include + +#include "NaviProvider.h" + +#include +#include +#include +#include +#include + +Sapphire::World::Navi::NaviProvider::NaviProvider( const std::string& internalName, FrameworkPtr pFw ) : + m_naviMesh( nullptr ), + m_naviMeshQuery( nullptr ), + m_internalName( internalName ), + m_pFw( pFw ) +{ + // Set defaults + m_polyFindRange[ 0 ] = 10; + m_polyFindRange[ 1 ] = 20; + m_polyFindRange[ 2 ] = 10; +} + +bool Sapphire::World::Navi::NaviProvider::init() +{ + auto& cfg = m_pFw->get< Sapphire::World::ServerMgr >()->getConfig(); + + auto meshesFolder = std::experimental::filesystem::path( cfg.navigation.meshPath ); + auto meshFolder = meshesFolder / std::experimental::filesystem::path( m_internalName ); + + if( std::experimental::filesystem::exists( meshFolder ) ) + { + auto baseMesh = meshFolder / std::experimental::filesystem::path( m_internalName + ".nav" ); + + if( !loadMesh( baseMesh.string() ) ) + return false; + + initQuery(); + + return true; + } + + return false; +} + +bool Sapphire::World::Navi::NaviProvider::hasNaviMesh() const +{ + return m_naviMesh != nullptr; +} + +void Sapphire::World::Navi::NaviProvider::initQuery() +{ + if( m_naviMeshQuery ) + dtFreeNavMeshQuery( m_naviMeshQuery ); + + m_naviMeshQuery = dtAllocNavMeshQuery(); + m_naviMeshQuery->init( m_naviMesh, 2048 ); +} + +int32_t Sapphire::World::Navi::NaviProvider::fixupCorridor( dtPolyRef* path, const int32_t npath, const int32_t maxPath, + const dtPolyRef* visited, const int32_t nvisited ) +{ + int32_t furthestPath = -1; + int32_t furthestVisited = -1; + + // Find furthest common polygon. + for( int32_t i = npath - 1; i >= 0; --i ) + { + bool found = false; + for( int32_t j = nvisited - 1; j >= 0; --j ) + { + if( path[ i ] == visited[ j ] ) + { + furthestPath = i; + furthestVisited = j; + found = true; + } + } + if( found ) + break; + } + + // If no intersection found just return current path. + if( furthestPath == -1 || furthestVisited == -1 ) + return npath; + + // Concatenate paths. + + // Adjust beginning of the buffer to include the visited. + const int32_t req = nvisited - furthestVisited; + const int32_t orig = rcMin( furthestPath + 1, npath ); + int32_t size = rcMax( 0, npath - orig ); + if( req + size > maxPath ) + size = maxPath - req; + if( size ) + memmove( path + req, path + orig, size * sizeof( dtPolyRef ) ); + + // Store visited + for( int32_t i = 0; i < req; ++i ) + path[i] = visited[( nvisited - 1 ) - i]; + + return req + size; +} + +int32_t Sapphire::World::Navi::NaviProvider::fixupShortcuts( dtPolyRef* path, int32_t npath, dtNavMeshQuery* navQuery ) +{ + if( npath < 3 ) + return npath; + + // Get connected polygons + const int32_t maxNeis = 16; + dtPolyRef neis[ maxNeis ]; + int32_t nneis = 0; + + const dtMeshTile* tile = 0; + const dtPoly* poly = 0; + if( dtStatusFailed( navQuery->getAttachedNavMesh()->getTileAndPolyByRef( path[ 0 ], &tile, &poly ) ) ) + return npath; + + for( uint32_t k = poly->firstLink; k != DT_NULL_LINK; k = tile->links[ k ].next ) + { + const dtLink* link = &tile->links[ k ]; + if( link->ref != 0 ) + { + if( nneis < maxNeis ) + neis[ nneis++ ] = link->ref; + } + } + + // If any of the neighbour polygons is within the next few polygons + // in the path, short cut to that polygon directly. + const int32_t maxLookAhead = 6; + int32_t cut = 0; + for( int32_t i = dtMin( maxLookAhead, npath ) - 1; i > 1 && cut == 0; i-- ) + { + for( int32_t j = 0; j < nneis; j++ ) + { + if( path[ i ] == neis[ j ] ) + { + cut = i; + break; + } + } + } + if( cut > 1 ) + { + int32_t offset = cut - 1; + npath -= offset; + for( int32_t i = 1; i < npath; i++ ) + path[ i ] = path[ i + offset ]; + } + + return npath; +} + +bool Sapphire::World::Navi::NaviProvider::inRange( const float* v1, const float* v2, const float r, const float h ) +{ + const float dx = v2[ 0 ] - v1[ 0 ]; + const float dy = v2[ 1 ] - v1[ 1 ]; + const float dz = v2[ 2 ] - v1[ 2 ]; + return ( dx * dx + dz * dz ) < r * r && fabsf( dy ) < h; +} + +bool Sapphire::World::Navi::NaviProvider::getSteerTarget( dtNavMeshQuery* navQuery, const float* startPos, const float* endPos, + const float minTargetDist, const dtPolyRef* path, const int32_t pathSize, + float* steerPos, unsigned char& steerPosFlag, dtPolyRef& steerPosRef, + float* outPoints, int32_t* outPointCount ) +{ + // Find steer target. + const int32_t MAX_STEER_POINTS = 3; + float steerPath[ MAX_STEER_POINTS * 3 ]; + uint8_t steerPathFlags[ MAX_STEER_POINTS ]; + dtPolyRef steerPathPolys[ MAX_STEER_POINTS ]; + int32_t nsteerPath = 0; + navQuery->findStraightPath( startPos, endPos, path, pathSize, + steerPath, steerPathFlags, steerPathPolys, &nsteerPath, MAX_STEER_POINTS ); + if( !nsteerPath ) + return false; + + if( outPoints && outPointCount ) + { + *outPointCount = nsteerPath; + for( int32_t i = 0; i < nsteerPath; ++i ) + dtVcopy( &outPoints[ i * 3 ], &steerPath[ i * 3 ] ); + } + + // Find vertex far enough to steer to. + int32_t ns = 0; + while( ns < nsteerPath ) + { + // Stop at Off-Mesh link or when point is further than slop away. + if( ( steerPathFlags[ ns ] & DT_STRAIGHTPATH_OFFMESH_CONNECTION ) || + !inRange( &steerPath[ ns * 3 ], startPos, minTargetDist, 1000.0f ) ) + break; + ns++; + } + // Failed to find good point to steer to. + if( ns >= nsteerPath ) + return false; + + dtVcopy( steerPos, &steerPath[ ns * 3 ] ); + steerPos[ 1 ] = startPos[ 1 ]; + steerPosFlag = steerPathFlags[ ns ]; + steerPosRef = steerPathPolys[ ns ]; + + return true; +} + +static float frand() +{ + return ( float ) rand() / (float)RAND_MAX; +} + + +Sapphire::Common::FFXIVARR_POSITION3 + Sapphire::World::Navi::NaviProvider::findRandomPositionInCircle( const Sapphire::Common::FFXIVARR_POSITION3& startPos, + float maxRadius ) +{ + dtStatus status; + + float spos[ 3 ] = { startPos.x, startPos.y, startPos.z }; + + float polyPickExt[ 3 ]; + polyPickExt[ 0 ] = 30; + polyPickExt[ 1 ] = 60; + polyPickExt[ 2 ] = 30; + + float randomPt[ 3 ]; + float snearest[ 3 ]; + + dtQueryFilter filter; + filter.setIncludeFlags( 0xffff ); + filter.setExcludeFlags( 0 ); + + dtPolyRef startRef; + dtPolyRef randomRef; + + status = m_naviMeshQuery->findNearestPoly( spos, polyPickExt, &filter, &startRef, snearest ); + + if( dtStatusFailed( status ) ) + { + return {}; + } + + if( !m_naviMesh->isValidPolyRef( startRef ) ) + { + return {}; + } + + auto pRNGMgr = m_pFw->get< World::Manager::RNGMgr >(); + auto rng = pRNGMgr->getRandGenerator< float >( 0.f, 1.f ); + status = m_naviMeshQuery->findRandomPointAroundCircle( startRef, spos, maxRadius, &filter, frand, + &randomRef, randomPt); + + if( dtStatusFailed( status ) ) + { + return {}; + } + + return { randomPt[ 0 ], randomPt[ 1 ], randomPt[ 2 ] }; +} + +std::vector< Sapphire::Common::FFXIVARR_POSITION3 > + Sapphire::World::Navi::NaviProvider::findFollowPath( const Common::FFXIVARR_POSITION3& startPos, + const Common::FFXIVARR_POSITION3& endPos ) +{ + if( !m_naviMesh || !m_naviMeshQuery ) + throw std::runtime_error( "No navimesh loaded" ); + + auto resultCoords = std::vector< Common::FFXIVARR_POSITION3 >(); + + dtPolyRef startRef, endRef = 0; + + float spos[ 3 ] = { startPos.x, startPos.y, startPos.z }; + float epos[ 3 ] = { endPos.x, endPos.y, endPos.z }; + + dtQueryFilter filter; + filter.setIncludeFlags( 0xffff ); + filter.setExcludeFlags( 0 ); + + m_naviMeshQuery->findNearestPoly( spos, m_polyFindRange, &filter, &startRef, 0 ); + m_naviMeshQuery->findNearestPoly( epos, m_polyFindRange, &filter, &endRef, 0 ); + + // Couldn't find any close polys to navigate from + if( !startRef || !endRef ) + return resultCoords; + + dtPolyRef polys[ MAX_POLYS ]; + int32_t numPolys = 0; + + m_naviMeshQuery->findPath( startRef, endRef, spos, epos, &filter, polys, &numPolys, MAX_POLYS ); + + // Check if we got polys back for navigation + if( numPolys ) + { + // Iterate over the path to find smooth path on the detail mesh surface. + memcpy( polys, polys, sizeof( dtPolyRef )*numPolys ); + int32_t npolys = numPolys; + + float iterPos[3], targetPos[3]; + m_naviMeshQuery->closestPointOnPoly( startRef, spos, iterPos, 0 ); + m_naviMeshQuery->closestPointOnPoly( polys[ npolys - 1 ], epos, targetPos, 0 ); + + //Logger::debug( "IterPos: {0} {1} {2}; TargetPos: {3} {4} {5}", + // iterPos[ 0 ], iterPos[ 1 ], iterPos[ 2 ], + // targetPos[ 0 ], targetPos[ 1 ], targetPos[ 2 ] ); + + const float STEP_SIZE = 1.2f; + const float SLOP = 0.15f; + + int32_t numSmoothPath = 0; + float smoothPath[ MAX_SMOOTH * 3 ]; + + dtVcopy( &smoothPath[ numSmoothPath * 3 ], iterPos ); + numSmoothPath++; + + // Move towards target a small advancement at a time until target reached or + // when ran out of memory to store the path. + while( npolys && numSmoothPath < MAX_SMOOTH ) + { + // Find location to steer towards. + float steerPos[ 3 ]; + uint8_t steerPosFlag; + dtPolyRef steerPosRef; + + if( !getSteerTarget( m_naviMeshQuery, iterPos, targetPos, SLOP, + polys, npolys, steerPos, steerPosFlag, steerPosRef ) ) + break; + + bool endOfPath = ( steerPosFlag & DT_STRAIGHTPATH_END ) ? true : false; + bool offMeshConnection = ( steerPosFlag & DT_STRAIGHTPATH_OFFMESH_CONNECTION ) ? true : false; + + // Find movement delta. + float delta[ 3 ], len; + dtVsub( delta, steerPos, iterPos ); + len = dtMathSqrtf( dtVdot( delta, delta ) ); + // If the steer target is end of path or off-mesh link, do not move past the location. + if( ( endOfPath || offMeshConnection ) && len < STEP_SIZE ) + len = 1; + else + len = STEP_SIZE / len; + float moveTgt[ 3 ]; + dtVmad( moveTgt, iterPos, delta, len ); + + // Move + float result[ 3 ]; + dtPolyRef visited[ 16 ]; + int32_t nvisited = 0; + m_naviMeshQuery->moveAlongSurface( polys[ 0 ], iterPos, moveTgt, &filter, + result, visited, &nvisited, 16 ); + + npolys = fixupCorridor( polys, npolys, MAX_POLYS, visited, nvisited ); + npolys = fixupShortcuts( polys, npolys, m_naviMeshQuery ); + + float h = 0; + m_naviMeshQuery->getPolyHeight( polys[0], result, &h ); + result[ 1 ] = h; + dtVcopy( iterPos, result ); + + // Handle end of path and off-mesh links when close enough. + if( endOfPath && inRange( iterPos, steerPos, SLOP, 1.0f ) ) + { + // Reached end of path. + dtVcopy( iterPos, targetPos ); + if( numSmoothPath < MAX_SMOOTH ) + { + dtVcopy( &smoothPath[ numSmoothPath * 3 ], iterPos ); + numSmoothPath++; + } + break; + } + else if( offMeshConnection && inRange( iterPos, steerPos, SLOP, 1.0f ) ) + { + // Reached off-mesh connection. + float startPos[ 3 ], endPos[ 3 ]; + + // Advance the path up to and over the off-mesh connection. + dtPolyRef prevRef = 0, polyRef = polys[ 0 ]; + int32_t npos = 0; + while( npos < npolys && polyRef != steerPosRef ) + { + prevRef = polyRef; + polyRef = polys[ npos ]; + npos++; + } + for( int32_t i = npos; i < npolys; ++i ) + polys[ i - npos ] = polys[ i ]; + npolys -= npos; + + // Handle the connection. + dtStatus status = m_naviMesh->getOffMeshConnectionPolyEndPoints( prevRef, polyRef, startPos, endPos ); + if( dtStatusSucceed( status ) ) + { + if( numSmoothPath < MAX_SMOOTH ) + { + dtVcopy( &smoothPath[ numSmoothPath * 3 ], startPos ); + numSmoothPath++; + // Hack to make the dotted path not visible during off-mesh connection. + if( numSmoothPath & 1 ) + { + dtVcopy( &smoothPath[ numSmoothPath * 3 ], startPos ); + numSmoothPath++; + } + } + // Move position at the other side of the off-mesh link. + dtVcopy( iterPos, endPos ); + float eh = 0.0f; + m_naviMeshQuery->getPolyHeight( polys[ 0 ], iterPos, &eh ); + iterPos[ 1 ] = eh; + } + } + + // Store results. + if( numSmoothPath < MAX_SMOOTH ) + { + dtVcopy( &smoothPath[ numSmoothPath * 3 ], iterPos ); + numSmoothPath++; + } + } + + for( int32_t i = 0; i < numSmoothPath; i += 3 ) + { + resultCoords.emplace_back( Common::FFXIVARR_POSITION3{ smoothPath[ i ], smoothPath[ i + 1 ], smoothPath[ i + 2 ] } ); + } + } + + return resultCoords; +} + +bool Sapphire::World::Navi::NaviProvider::loadMesh( const std::string& path ) +{ + FILE* fp = fopen( path.c_str(), "rb" ); + if( !fp ) + { + Logger::error( "Couldn't open navimesh file: {0}", path ); + return false; + } + + // Read header. + NavMeshSetHeader header; + + size_t readLen = fread( &header, sizeof( NavMeshSetHeader ), 1, fp ); + if( readLen != 1 ) + { + fclose( fp ); + Logger::error( "Couldn't read NavMeshSetHeader for {0}", path ); + return false; + } + + if( header.magic != NAVMESHSET_MAGIC ) + { + fclose( fp ); + Logger::error( "'{0}' has an incorrect NavMeshSet header.", path ); + return false; + } + + if( header.version != NAVMESHSET_VERSION ) + { + fclose( fp ); + Logger::error( "'{0}' has an incorrect NavMeshSet version. Expected '{1}', got '{2}'", path, NAVMESHSET_VERSION, header.version ); + return false; + } + + if( !m_naviMesh ) + { + m_naviMesh = dtAllocNavMesh(); + if( !m_naviMesh ) + { + fclose( fp ); + Logger::error( "Couldn't allocate dtNavMesh" ); + return false; + } + + dtStatus status = m_naviMesh->init( &header.params ); + if( dtStatusFailed( status ) ) + { + fclose( fp ); + Logger::error( "Couldn't initialise dtNavMesh" ); + return false; + } + } + + // Read tiles. + for( int32_t i = 0; i < header.numTiles; ++i ) + { + NavMeshTileHeader tileHeader; + readLen = fread( &tileHeader, sizeof( tileHeader ), 1, fp ); + if( readLen != 1 ) + { + fclose( fp ); + Logger::error( "Couldn't read NavMeshTileHeader from '{0}'", path ); + return false; + } + + if( !tileHeader.tileRef || !tileHeader.dataSize ) + break; + + auto data = reinterpret_cast< uint8_t* >( dtAlloc( tileHeader.dataSize, DT_ALLOC_PERM ) ); + if( !data ) + break; + memset( data, 0, tileHeader.dataSize ); + readLen = fread( data, tileHeader.dataSize, 1, fp ); + if( readLen != 1 ) + { + dtFree( data ); + fclose( fp ); + + Logger::error( "Couldn't read tile data from '{0}'", path ); + return false; + } + + m_naviMesh->addTile( data, tileHeader.dataSize, DT_TILE_FREE_DATA, tileHeader.tileRef, 0 ); + } + + fclose( fp ); + + return true; +} diff --git a/src/world/Navi/NaviProvider.h b/src/world/Navi/NaviProvider.h new file mode 100644 index 00000000..e69b6671 --- /dev/null +++ b/src/world/Navi/NaviProvider.h @@ -0,0 +1,71 @@ +#ifndef _NAVIPROVIDER_H_ +#define _NAVIPROVIDER_H_ + +#include +#include "ForwardsZone.h" +#include +#include + +namespace Sapphire::World::Navi +{ + const int32_t MAX_POLYS = 256; + const int32_t MAX_SMOOTH = 2048; + + const int32_t NAVMESHSET_MAGIC = 'M' << 24 | 'S' << 16 | 'E' << 8 | 'T'; //'MSET' + const int32_t NAVMESHSET_VERSION = 1; + + class NaviProvider + { + struct NavMeshSetHeader + { + int32_t magic; + int32_t version; + int32_t numTiles; + dtNavMeshParams params; + }; + + struct NavMeshTileHeader + { + dtTileRef tileRef; + int32_t dataSize; + }; + + public: + explicit NaviProvider( const std::string& internalName, FrameworkPtr pFw ); + + bool init(); + bool loadMesh( const std::string& path ); + void initQuery(); + + void toDetourPos( const Common::FFXIVARR_POSITION3& position, float* out ); + Common::FFXIVARR_POSITION3 toGamePos( float* pos ); + + std::vector< Common::FFXIVARR_POSITION3 > findFollowPath( const Common::FFXIVARR_POSITION3& startPos, + const Common::FFXIVARR_POSITION3& endPos ); + Common::FFXIVARR_POSITION3 findRandomPositionInCircle( const Sapphire::Common::FFXIVARR_POSITION3& startPos, + float maxRadius ); + + bool hasNaviMesh() const; + + protected: + std::string m_internalName; + + dtNavMesh* m_naviMesh; + dtNavMeshQuery* m_naviMeshQuery; + + float m_polyFindRange[ 3 ]; + + private: + int32_t fixupCorridor( dtPolyRef* path, int32_t npath, int32_t maxPath, const dtPolyRef* visited, int32_t nvisited ); + int32_t fixupShortcuts( dtPolyRef* path, int32_t npath, dtNavMeshQuery* navQuery ); + inline bool inRange( const float* v1, const float* v2, const float r, const float h ); + bool getSteerTarget( dtNavMeshQuery* navQuery, const float* startPos, const float* endPos, const float minTargetDist, + const dtPolyRef* path, const int32_t pathSize, float* steerPos, uint8_t& steerPosFlag, + dtPolyRef& steerPosRef, float* outPoints = 0, int32_t* outPointCount = 0 ); + + FrameworkPtr m_pFw; + }; + +} + +#endif diff --git a/src/world/Network/Handlers/GMCommandHandlers.cpp b/src/world/Network/Handlers/GMCommandHandlers.cpp index 8291d24d..1526789b 100644 --- a/src/world/Network/Handlers/GMCommandHandlers.cpp +++ b/src/world/Network/Handlers/GMCommandHandlers.cpp @@ -275,8 +275,13 @@ void Sapphire::Network::GameConnection::gm1Handler( FrameworkPtr pFw, } case GmCommand::Hp: { - targetPlayer->setHp( param1 ); - player.sendNotice( "Hp for {0} was set to {1}", targetPlayer->getName(), param1 ); + auto chara = targetActor->getAsChara(); + if( chara ) + { + chara->setHp( param1 ); + player.sendNotice( "Hp for {0} was set to {1}", chara->getName(), param1 ); + } + break; } case GmCommand::Mp: diff --git a/src/world/Network/PacketWrappers/MoveActorPacket.h b/src/world/Network/PacketWrappers/MoveActorPacket.h index a3e09ddb..f4a4f589 100644 --- a/src/world/Network/PacketWrappers/MoveActorPacket.h +++ b/src/world/Network/PacketWrappers/MoveActorPacket.h @@ -19,19 +19,19 @@ namespace Sapphire::Network::Packets::Server public ZoneChannelPacket< FFXIVIpcActorMove > { public: - MoveActorPacket( Entity::Chara& actor, uint8_t unk1, uint8_t unk2, uint8_t unk3, uint16_t unk4 ) : + MoveActorPacket( Entity::Chara& actor, uint8_t unk1, uint8_t animationType, uint8_t unk3, uint16_t unk4 ) : ZoneChannelPacket< FFXIVIpcActorMove >( actor.getId(), actor.getId() ) { - initialize( actor, unk1, unk2, unk3, unk4 ); + initialize( actor, unk1, animationType, unk3, unk4 ); }; private: - void initialize( Entity::Chara& actor, uint8_t unk1, uint8_t unk2, uint8_t unk3, uint16_t unk4 ) + void initialize( Entity::Chara& actor, uint8_t unk1, uint8_t animationType, uint8_t unk3, uint16_t unk4 ) { m_data.rotation = Util::floatToUInt8Rot( actor.getRot() ); m_data.unknown_1 = unk1; - m_data.unknown_2 = unk2; + m_data.animationType = animationType; m_data.unknown_3 = unk3; m_data.unknown_4 = unk4; m_data.posX = Util::floatToUInt16( actor.getPos().x ); diff --git a/src/world/Network/PacketWrappers/NpcSpawnPacket.h b/src/world/Network/PacketWrappers/NpcSpawnPacket.h index f487c50f..77d50e7e 100644 --- a/src/world/Network/PacketWrappers/NpcSpawnPacket.h +++ b/src/world/Network/PacketWrappers/NpcSpawnPacket.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "Actor/Player.h" #include "Actor/BNpc.h" #include "Forwards.h" @@ -39,17 +40,12 @@ namespace Sapphire::Network::Packets::Server m_data.mPMax = bnpc.getMaxMp(); m_data.subtype = 5; - //m_data.tPMax = 3000; m_data.level = bnpc.getLevel(); m_data.pose = bnpc.getPose(); memcpy( m_data.look, bnpc.getLookArray(), sizeof( m_data.look ) ); - - auto models = bnpc.getModelArray(); memcpy( m_data.models, bnpc.getModelArray(), sizeof( m_data.models ) ); - memcpy( m_data.look, bnpc.getLookArray(), sizeof( m_data.look ) ); - m_data.pos.x = bnpc.getPos().x; m_data.pos.y = bnpc.getPos().y; m_data.pos.z = bnpc.getPos().z; @@ -61,10 +57,8 @@ namespace Sapphire::Network::Packets::Server m_data.aggressionMode = bnpc.getAggressionMode(); m_data.classJob = 0; - //m_data.voice = bnpc.getVoiceId(); - //m_data.currentMount = bnpc.getCurrentMount(); - //m_data.onlineStatus = static_cast< uint8_t >( bnpc.getOnlineStatus() ); + m_data.targetId = Common::INVALID_GAME_OBJECT_ID; //m_data.u23 = 0x04; //m_data.u24 = 256; @@ -81,25 +75,13 @@ namespace Sapphire::Network::Packets::Server if( !target.isActorSpawnIdValid( m_data.spawnIndex ) ) return; - // 0x20 == spawn hidden to be displayed by the spawneffect control //m_data.displayFlags = bnpc.getDisplayFlags(); - /*if( bnpc.getZoningType() != Common::ZoneingType::None ) - { - m_data.displayFlags |= static_cast< uint16_t >( Common::DisplayFlags::Invisible ); - }*/ - //m_data.currentMount = bnpc.getCurrentMount(); //m_data.persistentEmote = bnpc.getPersistentEmote(); m_data.targetId = static_cast< uint64_t >( bnpc.getTargetId() ); - //m_data.type = 1; - //m_data.unknown_33 = 4; - //m_data.unknown_38 = 0x70; - //m_data.unknown_60 = 3; - //m_data.unknown_61 = 7; - uint64_t currentTimeMs = Sapphire::Util::getTimeMs(); diff --git a/src/world/Script/ScriptMgr.cpp b/src/world/Script/ScriptMgr.cpp index c1cb2f69..d9021b9f 100644 --- a/src/world/Script/ScriptMgr.cpp +++ b/src/world/Script/ScriptMgr.cpp @@ -53,7 +53,7 @@ bool Sapphire::Scripting::ScriptMgr::init() if( !status ) { - Logger::error( "ScriptMgr: failed to load scripts, the server will not function correctly without scripts loaded." ); + Logger::error( "ScriptMgr: failed to load modules, the server will not function correctly without scripts loaded." ); return false; } diff --git a/src/world/ServerMgr.cpp b/src/world/ServerMgr.cpp index 3de61f73..7149356d 100644 --- a/src/world/ServerMgr.cpp +++ b/src/world/ServerMgr.cpp @@ -41,6 +41,7 @@ #include "Manager/ItemMgr.h" #include "Manager/MarketMgr.h" #include "Manager/RNGMgr.h" +#include "Manager/NaviMgr.h" using namespace Sapphire::World::Manager; @@ -95,6 +96,8 @@ bool Sapphire::World::ServerMgr::loadSettings( int32_t argc, char* argv[] ) m_config.scripts.path = pConfig->getValue< std::string >( "Scripts", "Path", "./compiledscripts/" ); m_config.scripts.cachePath = pConfig->getValue< std::string >( "Scripts", "CachePath", "./cache/" ); + m_config.navigation.meshPath = pConfig->getValue< std::string >( "Navigation", "MeshPath", "navi" ); + m_config.network.disconnectTimeout = pConfig->getValue< uint16_t >( "Network", "DisconnectTimeout", 20 ); m_config.network.listenIp = pConfig->getValue< std::string >( "Network", "ListenIp", "0.0.0.0" ); m_config.network.listenPort = pConfig->getValue< uint16_t >( "Network", "ListenPort", 54992 ); @@ -126,6 +129,8 @@ void Sapphire::World::ServerMgr::run( int32_t argc, char* argv[] ) return; } + Logger::setLogLevel( m_config.global.general.logLevel ); + Logger::info( "Setting up generated EXD data" ); auto pExdData = std::make_shared< Data::ExdDataGenerated >(); auto dataPath = m_config.global.general.dataPath; @@ -166,6 +171,9 @@ void Sapphire::World::ServerMgr::run( int32_t argc, char* argv[] ) loadBNpcTemplates(); + auto pNaviMgr = std::make_shared< Manager::NaviMgr >( framework() ); + framework()->set< Manager::NaviMgr >( pNaviMgr ); + Logger::info( "TerritoryMgr: Setting up zones" ); auto pTeriMgr = std::make_shared< Manager::TerritoryMgr >( framework() ); auto pHousingMgr = std::make_shared< Manager::HousingMgr >( framework() ); diff --git a/src/world/Territory/Zone.cpp b/src/world/Territory/Zone.cpp index 21817559..e7007fff 100644 --- a/src/world/Territory/Zone.cpp +++ b/src/world/Territory/Zone.cpp @@ -79,6 +79,7 @@ Sapphire::Zone::Zone( uint16_t territoryTypeId, uint32_t guId, m_weatherOverride = Weather::None; m_territoryTypeInfo = pExdData->get< Sapphire::Data::TerritoryType >( territoryTypeId ); + m_bgPath = m_territoryTypeInfo->bg; loadWeatherRates(); loadSpawnGroups(); @@ -352,6 +353,11 @@ const std::string& Sapphire::Zone::getInternalName() const return m_internalName; } +const std::string& Sapphire::Zone::getBgPath() const +{ + return m_bgPath; +} + std::size_t Sapphire::Zone::getPopCount() const { return m_playerMap.size(); @@ -381,49 +387,48 @@ bool Sapphire::Zone::checkWeather() void Sapphire::Zone::updateBNpcs( int64_t tickCount ) { - if( ( tickCount - m_lastMobUpdate ) > 250 ) - { - m_lastMobUpdate = tickCount; - uint32_t currTime = Sapphire::Util::getTimeSeconds(); + if( ( tickCount - m_lastMobUpdate ) <= 250 ) + return; - /*for( auto it3 = m_BattleNpcDeadMap.begin(); it3 != m_BattleNpcDeadMap.end(); ++it3 ) + m_lastMobUpdate = tickCount; + uint32_t currTime = Sapphire::Util::getTimeSeconds(); + + for( auto entry : m_bNpcMap ) + { + Entity::BNpcPtr pBNpc = entry.second; + + if( !pBNpc ) + continue; + + if( !pBNpc->isAlive() && currTime - pBNpc->getTimeOfDeath() > 10 ) + { + removeActor( pBNpc ); + break; + } + } + + for( uint32_t x = 0; x < _sizeX; x++ ) + { + for( uint32_t y = 0; y < _sizeY; ++y ) + { + auto cell = getCellPtr( x, y ); + if( !cell ) + continue; + + // todo: this is a pretty shit because we will visit the same cells multiple times over + // ideally we run a pass every tick and cache active cells during that initial pass over every cell + // that way we don't have an expensive lookup for every actor + + if( !isCellActive( x, y ) ) + continue; + + for( const auto& actor : cell->m_actors ) { - - Entity::BattleNpcPtr pBNpc = *it3; - - if( ( currTime - pBNpc->getTimeOfDeath() ) > 60 ) - { - - pBNpc->resetHp(); - pBNpc->resetMp(); - pBNpc->resetPos(); - pushActor( pBNpc ); - - m_BattleNpcDeadMap.erase( it3 ); - - break; - } - }*/ - - - for( auto entry : m_bNpcMap ) - { - Entity::BNpcPtr pBNpc = entry.second; - - if( !pBNpc ) - continue; - - //if( !pBNpc->isAlive() && currTime - pBNpc->getTimeOfDeath() > ( 10 ) ) - //{ - // removeActor( pBNpc ); - // m_BattleNpcDeadMap.insert( pBNpc ); - // break; - //} - - pBNpc->update( tickCount ); - + if( actor->isBattleNpc() ) + actor->getAsBNpc()->update( tickCount ); } - } + } + } } @@ -782,7 +787,7 @@ bool Sapphire::Zone::loadSpawnGroups() m_spawnGroups.emplace_back( id, templateId, level, maxHp ); - Logger::debug( "id: {0}, template: {1}, level: {2}, maxHp: {3}", id, m_spawnGroups.back().getTemplateId(), level, maxHp ); + Logger::trace( "id: {0}, template: {1}, level: {2}, maxHp: {3}", id, m_spawnGroups.back().getTemplateId(), level, maxHp ); } res.reset(); @@ -805,7 +810,7 @@ bool Sapphire::Zone::loadSpawnGroups() group.getSpawnPointList().emplace_back( std::make_shared< Entity::SpawnPoint >( x, y, z, r, gimmickId ) ); - Logger::debug( "id: {0}, x: {1}, y: {2}, z: {3}, gimmickId: {4}", id, x, y, z, gimmickId ); + Logger::trace( "id: {0}, x: {1}, y: {2}, z: {3}, gimmickId: {4}", id, x, y, z, gimmickId ); } } return false; @@ -844,7 +849,13 @@ void Sapphire::Zone::updateSpawnPoints() pushActor( pBNpc ); } + else if( point->getLinkedBNpc() && !point->getLinkedBNpc()->isAlive() ) + { + point->setTimeOfDeath( Util::getTimeSeconds() ); + point->setLinkedBNpc( nullptr ); + } } } } + diff --git a/src/world/Territory/Zone.h b/src/world/Territory/Zone.h index 657b99c9..ee37ce0a 100644 --- a/src/world/Territory/Zone.h +++ b/src/world/Territory/Zone.h @@ -38,6 +38,7 @@ namespace Sapphire std::string m_placeName; std::string m_internalName; + std::string m_bgPath; std::unordered_map< int32_t, Entity::PlayerPtr > m_playerMap; std::unordered_map< int32_t, Entity::BNpcPtr > m_bNpcMap; @@ -135,6 +136,8 @@ namespace Sapphire const std::string& getInternalName() const; + const std::string& getBgPath() const; + std::size_t getPopCount() const; void loadWeatherRates();