From 76ee0e716e8b99f5322040358684971d50ab2170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 12 Jul 2024 01:17:26 -0300 Subject: [PATCH 1/7] NAVMESH OMGG!! --- Maple2.File.Ingest/Maple2.File.Ingest.csproj | 4 + Maple2.File.Ingest/Mapper/NavMeshMapper.cs | 670 +++++++++++++++++- Maple2.File.Ingest/Program.cs | 2 +- Maple2.File.Ingest/Utils/NavmeshHash.cs | 41 ++ Maple2.File.Ingest/Utils/Recast.cs | 129 ++++ .../Manager/Field/AgentNavigation.cs | 340 +++++---- .../Manager/Field/FieldManager.State.cs | 17 +- .../Manager/Field/FieldManager.cs | 2 +- .../Manager/Field/Navigation.cs | 135 ++-- Maple2.Server.Game/Maple2.Server.Game.csproj | 5 +- .../Model/Field/Actor/FieldNpc.cs | 10 +- .../Model/Field/Actor/FieldPet.cs | 6 +- Maple2.Server.Game/Util/LogErrorHandler.cs | 32 - Maple2.Tools/DotRecast/DotRecastHelper.cs | 50 ++ Maple2.Tools/Extensions/VectorExtensions.cs | 3 +- Maple2.Tools/Maple2.Tools.csproj | 2 + Maple2.Tools/Paths.cs | 5 +- 17 files changed, 1176 insertions(+), 277 deletions(-) create mode 100644 Maple2.File.Ingest/Utils/NavmeshHash.cs create mode 100644 Maple2.File.Ingest/Utils/Recast.cs delete mode 100644 Maple2.Server.Game/Util/LogErrorHandler.cs create mode 100644 Maple2.Tools/DotRecast/DotRecastHelper.cs diff --git a/Maple2.File.Ingest/Maple2.File.Ingest.csproj b/Maple2.File.Ingest/Maple2.File.Ingest.csproj index 80dc69990..523bc00fe 100644 --- a/Maple2.File.Ingest/Maple2.File.Ingest.csproj +++ b/Maple2.File.Ingest/Maple2.File.Ingest.csproj @@ -21,6 +21,10 @@ + + + + diff --git a/Maple2.File.Ingest/Mapper/NavMeshMapper.cs b/Maple2.File.Ingest/Mapper/NavMeshMapper.cs index 665dba1b8..78f3df45b 100644 --- a/Maple2.File.Ingest/Mapper/NavMeshMapper.cs +++ b/Maple2.File.Ingest/Mapper/NavMeshMapper.cs @@ -1,20 +1,668 @@ -using Maple2.File.IO; -using Maple2.File.IO.Crypto.Common; -using Maple2.Model.Metadata; +using System.Diagnostics; +using System.Numerics; +using DotRecast.Core; +using DotRecast.Core.Numerics; +using DotRecast.Detour; +using DotRecast.Detour.Extras.Jumplink; +using DotRecast.Detour.Io; +using DotRecast.Recast; +using DotRecast.Recast.Geom; +using DotRecast.Recast.Toolset; +using DotRecast.Recast.Toolset.Builder; +using DotRecast.Recast.Toolset.Tools; +using Maple2.Database.Context; +using Maple2.File.Flat; +using Maple2.File.Flat.maplestory2library; +using Maple2.File.Flat.physxmodellibrary; +using Maple2.File.Flat.standardmodellibrary; +using Maple2.File.Ingest.Helpers; +using Maple2.File.Ingest.Utils; +using Maple2.File.IO; +using Maple2.File.IO.Nif; +using Maple2.File.Parser.Flat; +using Maple2.File.Parser.MapXBlock; +using Maple2.Tools; +using Maple2.Tools.DotRecast; +using Maple2.Tools.VectorMath; namespace Maple2.File.Ingest.Mapper; -public class NavMeshMapper : TypeMapper { - private readonly M2dReader terrainReader; +public class NavMeshMapper { + private readonly HashSet xBlocks; + private readonly XBlockParser mapParser; + private readonly HashSet upsidedownFaces = []; // make top faces of block that have another block on top of them non-walkable - public NavMeshMapper(M2dReader terrainReader) { - this.terrainReader = terrainReader; + private readonly List fileLines = []; // used for debugging + + private readonly List vertexBuffer = + [ + new Vector3(-0.75f, 0.75f, 0.0f), + new Vector3(-0.75f, -0.75f, 0.0f), + new Vector3(-0.75f, -0.75f, 1.5f), + new Vector3(-0.75f, 0.75f, 1.5f), + new Vector3(0.75f, -0.75f, 0.0f), + new Vector3(0.75f, -0.75f, 1.5f), + new Vector3(0.75f, 0.75f, 0.0f), + new Vector3(0.75f, 0.75f, 1.5f), + // bottom face + new Vector3(-0.75f, 0.75f, 0.75f), + new Vector3(-0.75f, -0.75f, 0.75f), + new Vector3(0.75f, -0.75f, 0.75f), + new Vector3(0.75f, 0.75f, 0.75f), + + ]; + + private readonly List indexBuffer = [ + 1, 4, 5, + 4, 6, 7, + 7, 5, 4, + 2, 5, 7, + 6, 4, 1, + 3, 7, 6, + 5, 2, 1, + 7, 3, 2, + 2, 3, 0, + 1, 0, 6, + 6, 0, 3, + 0, 1, 2, + // bottom face + 11, 10, 9, + 9, 8, 11, + ]; + + public NavMeshMapper(MetadataContext db, M2dReader exportedReader) { + xBlocks = db.MapMetadata.Select(metadata => metadata.XBlock).ToHashSet(); + mapParser = new XBlockParser(exportedReader, new FlatTypeIndex(exportedReader)); + + Directory.CreateDirectory(Paths.NAVMESH_DIR); + Directory.CreateDirectory(Paths.NAVMESH_HASH_DIR); + + Map(); + } + + private void Map() { + // string xblock = "02000147_bf"; + // string xblock = "02000001_tw_tria"; + // string xblock = "82000012_survival"; + // mapParser.ParseMap(xblock, (entities) => GenerateNavMesh(xblock, entities)); + // return; + + mapParser.Parse((xblock, entities) => { + if (!xBlocks.Contains(xblock)) { + return; + } + + GenerateNavMesh(xblock, entities); + }); + } + + private void GenerateNavMesh(string xblock, IEnumerable entities) { + if (NavmeshHash.HasValidHash(xblock)) { + Console.WriteLine($"Navmesh already exists for {xblock}"); + return; + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + Console.WriteLine($"Parsing {xblock}..."); + + List verts = []; + List tris = []; + List areas = []; + foreach (IMapEntity entity in entities) { + if (entity is not IMesh mesh || string.IsNullOrEmpty(mesh.NifAsset)) { + continue; + } + + // Only consider entities with 'doesMakeTOK: true' + if (entity is not IMS2PathEngineTOK { doesMakeTOK: true }) { + continue; + } + + if (!mesh.NifAsset.StartsWith("urn:llid")) { + Console.WriteLine($"Invalid asset: {mesh.NifAsset} for {mesh.ModelName}"); + continue; + } + + uint llid = Convert.ToUInt32(mesh.NifAsset.Substring(mesh.NifAsset.LastIndexOf(':') + 1, 8), 16); + if (!NifParserHelper.nifDocuments.TryGetValue(llid, out NifDocument? document)) { + Console.WriteLine($"Failed to find asset: {mesh.NifAsset} for {mesh.ModelName}"); + continue; + } + + if (entity is not IPlaceable placeable) { + continue; + } + + Transform transform = new() { + Position = placeable.Position, + RotationAnglesDegrees = placeable.Rotation + }; + + transform.Transformation *= DotRecastHelper.MapRotation; + + bool isFluid = false; + + if (entity is IPhysXWhitebox whitebox) { + GenerateCube(whitebox, transform, verts, tris, areas); + } else if (entity is IMS2MapProperties mapProperties) { + if (mapProperties.CubeType == "Fluid") { + isFluid = true; + } + GenerateCube(mapProperties, transform, verts, tris, areas); + } + + foreach (NiPhysXProp prop in document.PhysXProps) { + if (prop.Snapshot == null) continue; + + foreach (NiPhysXActorDesc actor in prop.Snapshot.Actors) { + foreach (NiPhysXShapeDesc shape in actor.ShapeDescriptions) { + if (shape.Mesh == null) continue; + + PhysXMesh physXMesh = new PhysXMesh(shape.Mesh.MeshData); + + Matrix4x4 scale = Matrix4x4.CreateScale(prop.PhysXToWorldScale); + Matrix4x4 matrix = shape.LocalPose * actor.Poses[0] * scale * transform.Transformation; + + AddPhysxShape(verts, tris, areas, physXMesh, matrix, isFluid); + } + } + } + } + + if (verts.Count == 0 || tris.Count == 0) { + stopwatch.Stop(); + Console.WriteLine($"No mesh data found for {xblock} in {stopwatch.ElapsedMilliseconds}ms"); + return; + } + + // Used for debugging + // CreateObjFile(xblock); + + InputGeomProvider geomProvider = new InputGeomProvider(verts, tris); + + RcNavMeshBuildSettings settings = DotRecastHelper.NavMeshBuildSettings; + + RcConfig config = CreateRcConfig(settings, SampleAreaModifications.SAMPLE_AREAMOD_WALKABLE); + + RcBuilderConfig bcfg = new RcBuilderConfig(config, geomProvider.GetMeshBoundsMin(), geomProvider.GetMeshBoundsMax()); + + try { + RcBuilder rcBuilder = new RcBuilder(); + RcContext ctx = new RcContext(); + + RcHeightfield solid = BuildSolidHeightfield(tris, areas, geomProvider, bcfg, ctx); + + RcBuilderResult results = rcBuilder.Build(ctx, bcfg.tileX, bcfg.tileZ, geomProvider, bcfg.cfg, solid, keepInterResults: true); + + if (results.SolidHeightfiled == null) { + return; + } + + RcJumpLinkBuilderTool jumpLinkBuilder = new(); + RcJumpLinkBuilderToolConfig jumpLinkBuilderConfig = new() { + buildOffMeshConnections = true, + buildTypes = JumpLinkType.EDGE_JUMP_BIT, + groundTolerance = 1.5f, + edgeJumpEndDistance = 1.5f, + edgeJumpHeight = 2f, + edgeJumpDownMaxHeight = 1f, + edgeJumpUpMaxHeight = 2f, + }; + + // jumpLinkBuilder.Build(geomProvider, settings, [results], jumpLinkBuilderConfig); + + DtMeshData? meshData = BuildMeshData(geomProvider, config.Cs, config.Ch, config.WalkableHeightWorld, config.WalkableRadiusWorld, config.WalkableClimbWorld, results); + if (meshData == null) { + return; + } + + DtNavMesh? navMesh = BuildNavMesh(meshData, DotRecastHelper.VERTS_PER_POLY); + if (navMesh == null) { + return; + } + + string navmeshFilePath = Path.Combine(Paths.NAVMESH_DIR, $"{xblock}.navmesh"); + + using FileStream fs = new FileStream(navmeshFilePath, FileMode.Create, FileAccess.Write); + using BinaryWriter bw = new BinaryWriter(fs); + + DtMeshSetWriter writer = new(); + writer.Write(bw, navMesh, RcByteOrder.LITTLE_ENDIAN, true); + bw.Close(); + fs.Close(); + + NavmeshHash.WriteHash(xblock); + + stopwatch.Stop(); + Console.WriteLine($"Generated navmesh for {xblock} in {stopwatch.ElapsedMilliseconds}ms"); + return; + } catch (Exception ex) { + stopwatch.Stop(); + Console.WriteLine($"Failed to generate navmesh for {xblock} due to {ex.Message}"); + return; + } + } + + private static RcConfig CreateRcConfig(RcNavMeshBuildSettings settings, RcAreaModification walkableAreaMod) { + return new RcConfig( + partitionType: (RcPartition) settings.partitioning, + cellSize: settings.cellSize, + cellHeight: settings.cellHeight, + agentMaxSlope: settings.agentMaxSlope, + agentHeight: settings.agentHeight, + agentRadius: settings.agentRadius, + agentMaxClimb: settings.agentMaxClimb, + regionMinSize: settings.minRegionSize, + regionMergeSize: settings.mergedRegionSize, + edgeMaxLen: settings.edgeMaxLen, + edgeMaxError: settings.edgeMaxError, + vertsPerPoly: settings.vertsPerPoly, + detailSampleDist: settings.detailSampleDist, + detailSampleMaxError: settings.detailSampleMaxError, + filterLowHangingObstacles: settings.filterLowHangingObstacles, + filterLedgeSpans: settings.filterLedgeSpans, + filterWalkableLowHeightSpans: settings.filterWalkableLowHeightSpans, + walkableAreaMod: walkableAreaMod, + buildMeshDetail: true + ); } - protected override IEnumerable Map() { - foreach (PackFileEntry entry in terrainReader.Files) { - string xblock = Path.GetFileNameWithoutExtension(entry.Name); - yield return new NavMesh(xblock, terrainReader.GetBytes(entry)); + private static RcHeightfield BuildSolidHeightfield(List tris, List areas, InputGeomProvider geomProvider, RcBuilderConfig bcfg, RcContext ctx) { + // Allocate voxel heightfield where we rasterize our input data to. + RcHeightfield solid = new RcHeightfield(bcfg.width, bcfg.height, bcfg.bmin, bcfg.bmax, bcfg.cfg.Cs, bcfg.cfg.Ch, bcfg.cfg.BorderSize); + + foreach (RcTriMesh geom in geomProvider.Meshes()) { + float[] vertices = geom.GetVerts(); + + int[] triangles = geom.GetTris(); + + int numTriangles = triangles.Length / 3; + int[] array = CalculateAreasFlags(tris, areas, bcfg.cfg, vertices, numTriangles); + + RcRasterizations.RasterizeTriangles(ctx, vertices, triangles, array, numTriangles, solid, bcfg.cfg.WalkableClimb); } + + return solid; + } + + // Find triangles which are walkable based on their slope and rasterize them. + // Also check if the triangle is water and mark it as non-walkable. + private static int[] CalculateAreasFlags(List tris, List areas, RcConfig cfg, float[] verts2, int ntris) { + int[] array = areas.ToArray(); + float num = MathF.Cos(cfg.WalkableSlopeAngle / 180f * MathF.PI); + RcVec3f norm = default; + for (int i = 0; i < ntris; i++) { + // Skip water triangles. + if ((array[i] & SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) != 0) { + array[i] = 0; + continue; + } + + int num2 = i * 3; + RcRecast.CalcTriNormal(verts2, tris[num2], tris[num2 + 1], tris[num2 + 2], ref norm); + if (norm.Y > num) { + array[i] = cfg.WalkableAreaMod.Apply(array[i]); + } + } + + return array; } + + public static DtMeshData? BuildMeshData(IInputGeomProvider geom, float cellSize, float cellHeight, float agentHeight, + float agentRadius, float agentMaxClimb, RcBuilderResult result) { + int x = result.TileX; + int z = result.TileZ; + RcPolyMesh pmesh = result.Mesh; + RcPolyMeshDetail dmesh = result.MeshDetail; + DtNavMeshCreateParams option = new(); + for (int i = 0; i < pmesh.npolys; ++i) { + pmesh.flags[i] = 1; + } + + option.verts = pmesh.verts; + option.vertCount = pmesh.nverts; + option.polys = pmesh.polys; + option.polyAreas = pmesh.areas; + option.polyFlags = pmesh.flags; + option.polyCount = pmesh.npolys; + option.nvp = pmesh.nvp; + if (dmesh != null) { + option.detailMeshes = dmesh.meshes; + option.detailVerts = dmesh.verts; + option.detailVertsCount = dmesh.nverts; + option.detailTris = dmesh.tris; + option.detailTriCount = dmesh.ntris; + } + + option.walkableHeight = agentHeight; + option.walkableRadius = agentRadius; + option.walkableClimb = agentMaxClimb; + option.bmin = pmesh.bmin; + option.bmax = pmesh.bmax; + option.cs = cellSize; + option.ch = cellHeight; + option.buildBvTree = true; + + var offMeshConnections = geom.GetOffMeshConnections(); + option.offMeshConCount = offMeshConnections.Count; + option.offMeshConVerts = new float[option.offMeshConCount * 6]; + option.offMeshConRad = new float[option.offMeshConCount]; + option.offMeshConDir = new int[option.offMeshConCount]; + option.offMeshConAreas = new int[option.offMeshConCount]; + option.offMeshConFlags = new int[option.offMeshConCount]; + option.offMeshConUserID = new int[option.offMeshConCount]; + for (int i = 0; i < option.offMeshConCount; i++) { + RcOffMeshConnection offMeshCon = offMeshConnections[i]; + for (int j = 0; j < 6; j++) { + option.offMeshConVerts[6 * i + j] = offMeshCon.verts[j]; + } + + option.offMeshConRad[i] = offMeshCon.radius; + option.offMeshConDir[i] = offMeshCon.bidir ? 1 : 0; + option.offMeshConAreas[i] = offMeshCon.area; + option.offMeshConFlags[i] = offMeshCon.flags; + } + + option.tileX = x; + option.tileZ = z; + var dtMeshData = DtNavMeshBuilder.CreateNavMeshData(option); + if (dtMeshData != null) { + return DemoNavMeshBuilder.UpdateAreaAndFlags(dtMeshData); + } + + return null; + } + + public static DtNavMesh? BuildNavMesh(DtMeshData meshData, int vertsPerPoly) { + + DtNavMesh navMesh = new(); + var status = navMesh.Init(meshData, vertsPerPoly, 0); + if (status.Failed()) { + return null; + } + return navMesh; + } + + public static DtMeshData UpdateAreaAndFlags(DtMeshData meshData) { + // Update poly flags from areas. + for (int i = 0; i < meshData.polys.Length; ++i) { + int area = meshData.polys[i].GetArea(); + if (area is SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WALKABLE) { + meshData.polys[i].SetArea(SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND); + } + + if (area is SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GROUND + or SampleAreaModifications.SAMPLE_POLYAREA_TYPE_GRASS + or SampleAreaModifications.SAMPLE_POLYAREA_TYPE_ROAD) { + meshData.polys[i].flags = SampleAreaModifications.SAMPLE_POLYFLAGS_WALK; + } else if (area is SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER) { + meshData.polys[i].flags = SampleAreaModifications.SAMPLE_POLYFLAGS_SWIM; + } else if (area is SampleAreaModifications.SAMPLE_POLYAREA_TYPE_DOOR) { + meshData.polys[i].flags = SampleAreaModifications.SAMPLE_POLYFLAGS_DOOR; + } + } + + return meshData; + } + + private void GenerateCube(IMS2MapProperties mapProperties, Transform transform, List verts, List tris, List areas) { + if (!mapProperties.GeneratePhysX) { + return; + } + + Vector3 generatePhysXDimension = mapProperties.GeneratePhysXDimension; + if (generatePhysXDimension == Vector3.Zero) { + generatePhysXDimension = new Vector3(100f, 100f, 100f); + } + + GenerateCube(generatePhysXDimension, Vector3.Zero, transform, verts, tris, areas); + } + + private void GenerateCube(IPhysXWhitebox physXWhitebox, Transform transform, List verts, List tris, List areas) { + Vector3 offset = new Vector3(0, 0, -0.5f * physXWhitebox.ShapeDimensions.Z); + GenerateCube(physXWhitebox.ShapeDimensions, offset, transform, verts, tris, areas); + } + + private void GenerateCube(Vector3 size, Vector3 offset, Transform transform, List verts, List tris, List areas) { + Matrix4x4 matrix = Matrix4x4.CreateScale(size) * Matrix4x4.CreateTranslation(offset) * transform.Transformation; + + int currentVerticeCount = verts.Count / 3; + + foreach (Vector3 vertex in vertexBuffer) { + Vector3 transformed = Vector3.Transform(vertex, matrix); + verts.AddRange([transformed.X, transformed.Y, transformed.Z]); + fileLines.Add($"v {transformed.X} {transformed.Y} {transformed.Z}"); + } + + for (int i = 0; i < indexBuffer.Count; i += 3) { + tris.AddRange([indexBuffer[i] + currentVerticeCount, indexBuffer[i + 1] + currentVerticeCount, indexBuffer[i + 2] + currentVerticeCount]); + fileLines.Add($"f {indexBuffer[i] + 1 + currentVerticeCount} {indexBuffer[i + 1] + 1 + currentVerticeCount} {indexBuffer[i + 2] + 1 + currentVerticeCount}"); + areas.Add(0); + } + } + + private void AddPhysxShape(List verts, List tris, List areas, PhysXMesh physXMesh, Matrix4x4 matrix, bool isFluid) { + int currentVerticeCount = verts.Count / 3; + + List vertexBuffer2 = []; + + List indexBuffer2 = []; + + Vector3 offset = new Vector3(0, 0.125f, 0.0f); + + foreach (Vector3 vertex in physXMesh.Vertices) { + Vector3 transformed = Vector3.Transform(vertex, matrix); + verts.AddRange([transformed.X, transformed.Y, transformed.Z]); + fileLines.Add($"v {transformed.X} {transformed.Y} {transformed.Z}"); + } + + foreach (PhysXMeshFace face in physXMesh.Faces) { + tris.AddRange([(int) face.Vert0 + currentVerticeCount, (int) face.Vert1 + currentVerticeCount, (int) face.Vert2 + currentVerticeCount]); + fileLines.Add($"f {face.Vert0 + 1 + currentVerticeCount} {face.Vert1 + 1 + currentVerticeCount} {face.Vert2 + 1 + currentVerticeCount}"); + + if (isFluid) { + areas.Add(SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER); + } else { + areas.Add(0); + } + + Vector3 vert0 = Vector3.Transform(physXMesh.Vertices[(int) face.Vert0], matrix); + Vector3 vert1 = Vector3.Transform(physXMesh.Vertices[(int) face.Vert1], matrix); + Vector3 vert2 = Vector3.Transform(physXMesh.Vertices[(int) face.Vert2], matrix); + + Vector3 normal = Vector3.Cross(vert1 - vert0, vert2 - vert0); + normal = Vector3.Normalize(normal); + + if (normal.Y >= -Math.Cos(Math.PI / 2)) { + continue; + } + + int faceStart = vertexBuffer2.Count + (verts.Count / 3); + upsidedownFaces.Add(faceStart); + + indexBuffer2.AddRange([faceStart, faceStart + 1, faceStart + 2]); + vertexBuffer2.AddRange([vert0 + offset, vert1 + offset, vert2 + offset]); + } + + foreach (Vector3 vertex in vertexBuffer2) { + verts.AddRange([vertex.X, vertex.Y, vertex.Z]); + fileLines.Add($"v {vertex.X} {vertex.Y} {vertex.Z}"); + } + + for (int i = 0; i < indexBuffer2.Count; i += 3) { + tris.AddRange([indexBuffer2[i], indexBuffer2[i + 1], indexBuffer2[i + 2]]); + fileLines.Add($"f {indexBuffer2[i] + 1} {indexBuffer2[i + 1] + 1} {indexBuffer2[i + 2] + 1}"); + if (isFluid) { + areas.Add(SampleAreaModifications.SAMPLE_POLYAREA_TYPE_WATER); + } else { + areas.Add(0); + } + } + } + + // used for debugging + private void CreateObjFile(string xblock) { + // create a new file if it doesn't exist + if (System.IO.File.Exists($"{xblock}.obj")) { + System.IO.File.Delete($"{xblock}.obj"); + } + + var file = System.IO.File.Create($"{xblock}.obj"); + using StreamWriter streamWriter = new StreamWriter(file); + foreach (string line in fileLines) { + streamWriter.WriteLine(line); + } + streamWriter.Close(); + fileLines.Clear(); + } + + #region TileMesh configuration + // tiling configuration + // var config = new RcConfig( + // useTiles: true, + // tileSizeX: TileSize, + // tileSizeZ: TileSize, + // borderSize: RcConfig.CalcBorder(0.3f, CellSize), + // partition: RcPartition.WATERSHED, + // cellSize: CellSize, + // cellHeight: CellSize, + // agentMaxSlope: 47f, // generally 45 degrees, but we add a bit more to account for floating point errors + // agentMaxClimb: 0.7f, + // agentHeight: 1.4f, // approximation of character height + // agentRadius: 0.3f, // approximation of character radius + // minRegionArea: 8 * 8 * CellSize * CellSize, + // mergeRegionArea: 20 * 20 * CellSize * CellSize, + // edgeMaxLen: 12.0f, + // edgeMaxError: 1.3f, + // vertsPerPoly: VertsPerPoly, + // detailSampleDist: 6.0f, + // detailSampleMaxError: 1.0f, + // filterLowHangingObstacles: true, + // filterLedgeSpans: true, + // filterWalkableLowHeightSpans: true, + // walkableAreaMod: new RcAreaModification(0x3f), + // buildMeshDetail: true + // ); + + // try { + // RcBuilder rcBuilder = new(); + // List results = rcBuilder.BuildTiles(geomProvider, config, true, true, Environment.ProcessorCount + 1, Task.Factory); + + // List tileMeshData = BuildMeshData(geomProvider, config.Cs, config.Ch, config.WalkableHeightWorld, config.WalkableRadiusWorld, config.WalkableClimbWorld, results); + // DtNavMesh tileNavMesh = BuildNavMesh(geomProvider, tileMeshData, config.Cs, TileSize, VertsPerPoly); + + // string navmeshFilePath = $"navmeshes/{xblock}.navmesh"; + + // using var fs = new FileStream(navmeshFilePath, FileMode.Create, FileAccess.Write); + // using var bw = new BinaryWriter(fs); + + // DtMeshSetWriter writer = new(); + // writer.Write(bw, tileNavMesh, RcByteOrder.LITTLE_ENDIAN, true); + // } catch (Exception ex) { + // Console.WriteLine($"Failed to generate navmesh for {xblock} due to {ex.Message}"); + // return; + // } + // public static List BuildMeshData(IInputGeomProvider geom, float cellSize, float cellHeight, float agentHeight, + // float agentRadius, float agentMaxClimb, IList results) { + // List meshData = []; + // foreach (RcBuilderResult result in results) { + // int x = result.TileX; + // int z = result.TileZ; + // RcPolyMesh pmesh = result.Mesh; + // RcPolyMeshDetail dmesh = result.MeshDetail; + // DtNavMeshCreateParams option = new(); + // for (int i = 0; i < pmesh.npolys; ++i) { + // pmesh.flags[i] = 1; + // } + + // option.verts = pmesh.verts; + // option.vertCount = pmesh.nverts; + // option.polys = pmesh.polys; + // option.polyAreas = pmesh.areas; + // option.polyFlags = pmesh.flags; + // option.polyCount = pmesh.npolys; + // option.nvp = pmesh.nvp; + // if (dmesh != null) { + // option.detailMeshes = dmesh.meshes; + // option.detailVerts = dmesh.verts; + // option.detailVertsCount = dmesh.nverts; + // option.detailTris = dmesh.tris; + // option.detailTriCount = dmesh.ntris; + // } + + // option.walkableHeight = agentHeight; + // option.walkableRadius = agentRadius; + // option.walkableClimb = agentMaxClimb; + // option.bmin = pmesh.bmin; + // option.bmax = pmesh.bmax; + // option.cs = cellSize; + // option.ch = cellHeight; + // option.buildBvTree = true; + + // // TODO: Off-mesh connections + // // var offMeshConnections = geom.GetOffMeshConnections(); + // // option.offMeshConCount = offMeshConnections.Count; + // // option.offMeshConVerts = new float[option.offMeshConCount * 6]; + // // option.offMeshConRad = new float[option.offMeshConCount]; + // // option.offMeshConDir = new int[option.offMeshConCount]; + // // option.offMeshConAreas = new int[option.offMeshConCount]; + // // option.offMeshConFlags = new int[option.offMeshConCount]; + // // option.offMeshConUserID = new int[option.offMeshConCount]; + // // for (int i = 0; i < option.offMeshConCount; i++) { + // // RcOffMeshConnection offMeshCon = offMeshConnections[i]; + // // for (int j = 0; j < 6; j++) { + // // option.offMeshConVerts[6 * i + j] = offMeshCon.verts[j]; + // // } + + // // option.offMeshConRad[i] = offMeshCon.radius; + // // option.offMeshConDir[i] = offMeshCon.bidir ? 1 : 0; + // // option.offMeshConAreas[i] = offMeshCon.area; + // // option.offMeshConFlags[i] = offMeshCon.flags; + // // // option.offMeshConUserID[i] = offMeshCon.userId; + // // } + + // option.tileX = x; + // option.tileZ = z; + // var dtMeshData = DtNavMeshBuilder.CreateNavMeshData(option); + // if (dtMeshData != null) { + // meshData.Add(DemoNavMeshBuilder.UpdateAreaAndFlags(dtMeshData)); + // } + // } + + // return meshData; + // } + + // public static DtNavMesh BuildNavMesh(IInputGeomProvider geom, List meshData, float cellSize, int tileSize, int vertsPerPoly) { + // DtNavMeshParams navMeshParams = new() { + // orig = geom.GetMeshBoundsMin(), + // tileWidth = tileSize * cellSize, + // tileHeight = tileSize * cellSize, + + // maxTiles = GetMaxTiles(geom, cellSize, tileSize), + // maxPolys = GetMaxPolysPerTile(geom, cellSize, tileSize) + // }; + + // DtNavMesh navMesh = new(); + // navMesh.Init(navMeshParams, vertsPerPoly); + // meshData.ForEach(md => navMesh.AddTile(md, 0, 0, out long _)); + // return navMesh; + // } + // public static int GetMaxTiles(IInputGeomProvider geom, float cellSize, int tileSize) { + // int tileBits = GetTileBits(geom, cellSize, tileSize); + // return 1 << tileBits; + // } + + // public static int GetMaxPolysPerTile(IInputGeomProvider geom, float cellSize, int tileSize) { + // int polyBits = 22 - GetTileBits(geom, cellSize, tileSize); + // return 1 << polyBits; + // } + + // private static int GetTileBits(IInputGeomProvider geom, float cellSize, int tileSize) { + // RcRecast.CalcGridSize(geom.GetMeshBoundsMin(), geom.GetMeshBoundsMax(), cellSize, out int gw, out int gh); + // int tw = (gw + tileSize - 1) / tileSize; + // int th = (gh + tileSize - 1) / tileSize; + // int tileBits = Math.Min(DtUtils.Ilog2(DtUtils.NextPow2(tw * th)), 14); + // return tileBits; + // } + #endregion } diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index ccb1f12e9..977552807 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -116,7 +116,7 @@ UpdateDatabase(metadataContext, new NxsMeshMapper()); UpdateDatabase(metadataContext, new MapEntityMapper(metadataContext, exportedReader)); -UpdateDatabase(metadataContext, new NavMeshMapper(terrainReader)); +_ = new NavMeshMapper(metadataContext, exportedReader); UpdateDatabase(metadataContext, new ServerTableMapper(serverReader)); UpdateDatabase(metadataContext, new AiMapper(serverReader)); diff --git a/Maple2.File.Ingest/Utils/NavmeshHash.cs b/Maple2.File.Ingest/Utils/NavmeshHash.cs new file mode 100644 index 000000000..a1bf659ef --- /dev/null +++ b/Maple2.File.Ingest/Utils/NavmeshHash.cs @@ -0,0 +1,41 @@ +using System.Security.Cryptography; +using Maple2.Tools; + +namespace Maple2.File.Ingest.Utils; + +public static class NavmeshHash { + public static bool HasValidHash(string filename) { + string hashPath = Path.Combine(Paths.NAVMESH_HASH_DIR, $"{filename}-hash"); + + if (!System.IO.File.Exists(hashPath)) { + return false; + } + + string currentHash = System.IO.File.ReadAllText(hashPath); + string newHash = GetHash(filename); + + return currentHash.Equals(newHash); + } + + public static void WriteHash(string filename) { + string hashPath = Path.Combine(Paths.NAVMESH_HASH_DIR, $"{filename}-hash"); + + string newHash = GetHash(filename); + + System.IO.File.WriteAllText(hashPath, newHash); + } + + private static string GetHash(string filename) { + string filepath = Path.Combine(Paths.NAVMESH_DIR, $"{filename}.navmesh"); + + if (!System.IO.File.Exists(filepath)) { + return ""; + } + + using MD5 md5 = MD5.Create(); + using FileStream stream = System.IO.File.OpenRead(filepath); + + byte[] hash = md5.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/Maple2.File.Ingest/Utils/Recast.cs b/Maple2.File.Ingest/Utils/Recast.cs new file mode 100644 index 000000000..20ab22b5a --- /dev/null +++ b/Maple2.File.Ingest/Utils/Recast.cs @@ -0,0 +1,129 @@ +using DotRecast.Core.Collections; +using DotRecast.Core.Numerics; +using DotRecast.Recast; +using DotRecast.Recast.Geom; + +namespace Maple2.File.Ingest.Utils; +internal class InputGeomProvider : IInputGeomProvider { + public readonly float[] vertices; + + public readonly int[] faces; + + public readonly float[] normals; + + private RcVec3f bmin; + + private RcVec3f bmax; + + private readonly RcTriMesh mesh; + private readonly List offMeshConnections; + private readonly List convexVolumes; + + + public InputGeomProvider(List verts, List tris) { + vertices = MapVertices(verts); + faces = MapFaces(tris); + + normals = new float[faces.Length]; + CalculateNormals(); + bmin = RcVecUtils.Create(vertices); + bmax = RcVecUtils.Create(vertices); + for (int i = 1; i < vertices.Length / 3; i++) { + bmin = RcVecUtils.Min(bmin, vertices, i * 3); + bmax = RcVecUtils.Max(bmax, vertices, i * 3); + } + + mesh = new RcTriMesh(vertices, faces); + offMeshConnections = []; + convexVolumes = []; + } + + private void CalculateNormals() { + for (int i = 0; i < faces.Length; i += 3) { + int num = faces[i] * 3; + int num2 = faces[i + 1] * 3; + int num3 = faces[i + 2] * 3; + RcVec3f rcVec3f = default; + RcVec3f rcVec3f2 = default; + rcVec3f.X = vertices[num2] - vertices[num]; + rcVec3f.Y = vertices[num2 + 1] - vertices[num + 1]; + rcVec3f.Z = vertices[num2 + 2] - vertices[num + 2]; + rcVec3f2.X = vertices[num3] - vertices[num]; + rcVec3f2.Y = vertices[num3 + 1] - vertices[num + 1]; + rcVec3f2.Z = vertices[num3 + 2] - vertices[num + 2]; + normals[i] = rcVec3f.Y * rcVec3f2.Z - rcVec3f.Z * rcVec3f2.Y; + normals[i + 1] = rcVec3f.Z * rcVec3f2.X - rcVec3f.X * rcVec3f2.Z; + normals[i + 2] = rcVec3f.X * rcVec3f2.Y - rcVec3f.Y * rcVec3f2.X; + float num4 = MathF.Sqrt(normals[i] * normals[i] + normals[i + 1] * normals[i + 1] + normals[i + 2] * normals[i + 2]); + if (num4 > 0f) { + num4 = 1f / num4; + normals[i] *= num4; + normals[i + 1] *= num4; + normals[i + 2] *= num4; + } + } + } + + private static int[] MapFaces(List meshFaces) { + int[] array = new int[meshFaces.Count]; + for (int i = 0; i < array.Length; i++) { + array[i] = meshFaces[i]; + } + + return array; + } + + private static float[] MapVertices(List vertexPositions) { + float[] array = new float[vertexPositions.Count]; + for (int i = 0; i < array.Length; i++) { + array[i] = vertexPositions[i]; + } + + return array; + } + + public List GetOffMeshConnections() { + return offMeshConnections; + } + + public void AddOffMeshConnection(RcVec3f start, RcVec3f end, float radius, bool bidir, int area, int flags) { + offMeshConnections.Add(new RcOffMeshConnection(start, end, radius, true, area, flags)); + } + + public void RemoveOffMeshConnections(Predicate filter) { + offMeshConnections.RemoveAll(filter); + } + + public void AddConvexVolume(float[] verts, float minh, float maxh, RcAreaModification areaMod) { + AddConvexVolume(new RcConvexVolume { + verts = verts, + hmin = minh, + hmax = maxh, + areaMod = areaMod + }); + } + + public void AddConvexVolume(RcConvexVolume volume) { + convexVolumes.Add(volume); + } + + public IList ConvexVolumes() { + return convexVolumes; + } + + public RcTriMesh GetMesh() { + return mesh; + } + + public RcVec3f GetMeshBoundsMax() { + return bmax; + } + + public RcVec3f GetMeshBoundsMin() { + return bmin; + } + + public IEnumerable Meshes() { + return RcImmutableArray.Create(mesh); + } +} \ No newline at end of file diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index 202e29ec2..e98d60ce8 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -1,217 +1,315 @@ using System.Numerics; -using Maple2.PathEngine; -using Maple2.PathEngine.Exception; -using Maple2.PathEngine.Types; +using DotRecast.Core; +using DotRecast.Core.Numerics; +using DotRecast.Detour; +using DotRecast.Detour.Crowd; using Maple2.Server.Game.Model; +using Maple2.Tools.DotRecast; +using Maple2.Tools.Extensions; using Serilog; namespace Maple2.Server.Game.Manager.Field; -public sealed class AgentNavigation : IDisposable { +public sealed class AgentNavigation { private static readonly ILogger Logger = Log.Logger.ForContext(); private readonly FieldNpc npc; - private readonly Agent agent; - private readonly Mesh mesh; - private readonly CollisionContext context; + public readonly DtCrowdAgent agent; + private readonly DtCrowd crowd; - private PathEngine.Path? currentPath; + public List currentPath = []; + private int currentPathIndex = 0; + private float currentPathProgress = 0; - public AgentNavigation(FieldNpc npc, Agent agent, Mesh mesh, CollisionContext context) { - this.npc = npc; - this.agent = agent; - this.mesh = mesh; - this.context = context; + public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) { + npc = fieldNpc; + agent = dtAgent; + crowd = dtCrowd; + } - context.temporarilyIgnoreAgent(agent); + public List FindPath(Vector3 startVec, Vector3 targetVec) { + return FindPath(crowd, DotRecastHelper.ToNavMeshSpace(startVec), DotRecastHelper.ToNavMeshSpace(targetVec)); } - public void Dispose() { - // mesh+context are disposed by Navigation - currentPath?.Dispose(); - currentPath = null; - context.restoreTemporarilyIgnoredAgent(agent); - context.removeAgent(agent); + public List FindPath(RcVec3f startVec, RcVec3f targetVec) { + return FindPath(crowd, startVec, targetVec); } - public bool HasPath => currentPath != null && currentPath.size() >= 2; + private List FindPath(DtCrowd crowd, RcVec3f startVec, RcVec3f targetVec) { + DtNavMesh navMesh = crowd.GetNavMesh(); + DtNavMeshQuery navMeshQuery = crowd.GetNavMeshQuery(); + IDtQueryFilter filter = crowd.GetFilter(0); + if (!FindNearestPoly(startVec, out long pos1Ref, out RcVec3f _)) { + Logger.Error("Failed to find nearest poly at {StartVec}", startVec); + return []; + } + + if (!FindNearestPoly(targetVec, out long posRef2, out RcVec3f _)) { + Logger.Error("Failed to find nearest poly at {TargetVec}", targetVec); + return []; + } + + List pathIterPolys = []; + navMeshQuery.FindPath(pos1Ref, posRef2, startVec, targetVec, filter, ref pathIterPolys, new DtFindPathOption(0, float.MaxValue)); + if (pathIterPolys.Count == 0) { + Logger.Error("Failed to find path from {StartVec} to {TargetVec}", startVec, targetVec); + return []; + } + + int pathIterPolysCount = pathIterPolys.Count; + + navMeshQuery.ClosestPointOnPoly(pos1Ref, startVec, out RcVec3f iterPos, out bool _); + navMeshQuery.ClosestPointOnPoly(pathIterPolys[pathIterPolysCount - 1], targetVec, out RcVec3f endPos, out bool _); + + Span visited = stackalloc long[16]; + int nvisited = 0; + + int MAX_POLYS = 256; + int MAX_SMOOTH = 256; + List smoothPath = []; + while (pathIterPolysCount > 0 && smoothPath.Count < MAX_SMOOTH) { + // Find location to steer towards. + if (!DtPathUtils.GetSteerTarget(navMeshQuery, iterPos, endPos, DotRecastHelper.MIN_TARGET_DIST, + pathIterPolys, pathIterPolysCount, out var steerPos, out int steerPosFlag, out long steerPosRef)) { + break; + } + + bool endOfPath = (steerPosFlag & DtStraightPathFlags.DT_STRAIGHTPATH_END) != 0; + bool offMeshConnection = (steerPosFlag & DtStraightPathFlags.DT_STRAIGHTPATH_OFFMESH_CONNECTION) != 0; + + // Find movement delta. + RcVec3f delta = RcVec3f.Subtract(steerPos, iterPos); + float len = MathF.Sqrt(RcVec3f.Dot(delta, delta)); + // If the steer target is end of path or off-mesh link, do not move past the location. + if ((endOfPath || offMeshConnection) && len < DotRecastHelper.STEP_SIZE) { + len = 1; + } else { + len = DotRecastHelper.STEP_SIZE / len; + } + + RcVec3f moveTgt = RcVecUtils.Mad(iterPos, delta, len); + + // Move + navMeshQuery.MoveAlongSurface(pathIterPolys[0], iterPos, moveTgt, filter, out var result, visited, out nvisited, 16); + + iterPos = result; + + pathIterPolysCount = DtPathUtils.MergeCorridorStartMoved(ref pathIterPolys, pathIterPolysCount, MAX_POLYS, visited, nvisited); + pathIterPolysCount = DtPathUtils.FixupShortcuts(ref pathIterPolys, pathIterPolysCount, navMeshQuery); + + if (navMeshQuery.GetPolyHeight(pathIterPolys[0], result, out float h).Succeeded()) { + iterPos.Y = h; + } + + // Handle end of path and off-mesh links when close enough. + if (endOfPath && DtPathUtils.InRange(iterPos, steerPos, DotRecastHelper.MIN_TARGET_DIST, 1.0f)) { + // Reached end of path. + iterPos = targetVec; + if (smoothPath.Count < MAX_SMOOTH) { + smoothPath.Add(iterPos); + } + + break; + } else if (offMeshConnection && DtPathUtils.InRange(iterPos, steerPos, DotRecastHelper.MIN_TARGET_DIST, 1.0f)) { + // Reached off-mesh connection. + RcVec3f startPosition = RcVec3f.Zero; + RcVec3f endPosition = RcVec3f.Zero; + + // Advance the path up to and over the off-mesh connection. + long prevRef = 0; + long polyRef = pathIterPolys[0]; + int npos = 0; + while (npos < pathIterPolysCount && polyRef != steerPosRef) { + prevRef = polyRef; + polyRef = pathIterPolys[npos]; + npos++; + } + + pathIterPolys = pathIterPolys.GetRange(npos, pathIterPolys.Count - npos); + pathIterPolysCount -= npos; + + // Handle the connection. + var status4 = navMesh.GetOffMeshConnectionPolyEndPoints(prevRef, polyRef, ref startPosition, ref endPosition); + if (status4.Succeeded()) { + if (smoothPath.Count < MAX_SMOOTH) { + smoothPath.Add(startPosition); + // Hack to make the dotted path not visible during off-mesh connection. + if ((smoothPath.Count & 1) != 0) { + smoothPath.Add(startPosition); + } + } + + // Move position at the other side of the off-mesh link. + iterPos = endPosition; + navMeshQuery.GetPolyHeight(pathIterPolys[0], iterPos, out float eh); + iterPos.Y = eh; + } + } + + // Store results. + if (smoothPath.Count < MAX_SMOOTH) { + smoothPath.Add(iterPos); + } + } + + return smoothPath; + } + + public bool HasPath => currentPath != null && currentPath.Count > 0; public bool UpdatePosition() { - Position position = mesh.positionNear3DPoint((int) npc.Position.X, (int) npc.Position.Y, (int) npc.Position.Z, horizontalRange: 500, verticalRange: 50); - if (!mesh.positionIsValid(position)) { + if (!FindNearestPoly(npc.Position, out _, out RcVec3f position)) { Logger.Error("Failed to find valid position from {Source} => {Position}", npc.Position, position); return false; } - agent.moveTo(position); - npc.Position = FromPosition(position); + agent.npos = position; + npc.Position = DotRecastHelper.FromNavMeshSpace(position); + return true; + } + + private bool FindNearestPoly(Vector3 point, out long nearestRef, out RcVec3f position) { + var pointToNavMesh = DotRecastHelper.ToNavMeshSpace(point); + return FindNearestPoly(pointToNavMesh, out nearestRef, out position); + } + + private bool FindNearestPoly(RcVec3f point, out long nearestRef, out RcVec3f position) { + var status = crowd.GetNavMeshQuery().FindNearestPoly(point, new RcVec3f(2, 4, 2), crowd.GetFilter(0), out nearestRef, out position, out _); + if (status.Failed()) { + Logger.Error("Failed to find nearest poly from {Source} => {Position}", point, position); + return false; + } + return true; } public Vector3 GetAgentPosition() { - return FromPosition(agent.getPosition()); + return DotRecastHelper.FromNavMeshSpace(agent.npos); } public (Vector3 Start, Vector3 End) Advance(TimeSpan timeSpan, float speed, out bool followSegment) { followSegment = false; - - if (currentPath == null || currentPath.size() < 2) { + if (currentPath.Count < 2) { return default; } - Position startPosition = agent.getPosition(); - Vector3 start = FromPosition(startPosition); - float distance = (float) timeSpan.TotalSeconds * speed; - using CollisionInfo? info = agent.advanceAlongPath(currentPath, distance, null); - Vector3 end = FromPosition(agent.getPosition()); + Vector3 start = DotRecastHelper.FromNavMeshSpace(currentPath[currentPathIndex]); - // TODO: Requires jump to reach next position. - if (Math.Abs(start.Z - end.Z) > 75) { - agent.moveTo(startPosition); // Revert agent position - currentPath.Dispose(); - currentPath = null; + if (currentPathIndex >= currentPath.Count - 1) { return default; } - if (info != null || currentPath.getLength() < 25) { - currentPath.Dispose(); - currentPath = null; - } - followSegment = true; + float distance = RcVec3f.Distance(currentPath[currentPathIndex], currentPath[currentPathIndex + 1]); - return (start, end); - } + speed *= DotRecastHelper.MapRotation.GetRightAxis().Length(); // changing speed to navmesh space + float timeLeft = distance * (1 - currentPathProgress) / speed; + while (timeLeft < timeSpan.TotalSeconds) { + timeSpan -= TimeSpan.FromSeconds(timeLeft); + currentPathIndex++; + currentPathProgress = 0; - public Vector3 GetRandomPatrolPoint() { - Position origin = ToPosition(npc.Origin); - if (!mesh.positionIsValid(origin)) { - return npc.Position; - } + if (currentPathIndex >= currentPath.Count - 1) { + return (start, DotRecastHelper.FromNavMeshSpace(currentPath[^1])); + } - if (npc.Value.Metadata.Action.MoveArea == 0) { - return FromPosition(origin); + distance = RcVec3f.Distance(currentPath[currentPathIndex], currentPath[currentPathIndex + 1]); + timeLeft = distance * (1 - currentPathProgress) / speed; } - Position end = mesh.generateRandomPositionLocally(origin, npc.Value.Metadata.Action.MoveArea); + currentPathProgress += (float) timeSpan.TotalSeconds * speed / distance; + RcVec3f end = RcVec3f.Lerp(currentPath[currentPathIndex], currentPath[currentPathIndex + 1], currentPathProgress); - return FromPosition(end); + return (start, DotRecastHelper.FromNavMeshSpace(end)); } - public bool RandomPatrol() { - // Npc cannot move? - if (npc.Value.Metadata.Action.MoveArea == 0) { - return false; + public Vector3 GetRandomPatrolPoint() { + if (!FindNearestPoly(npc.Origin, out long startRef, out RcVec3f startVec)) { + return npc.Position; } - if (!mesh.positionIsValid(agent.getPosition())) { - return false; + float moveArea = npc.Value.Metadata.Action.MoveArea; + if (moveArea == 0) { + return npc.Origin; } - Position origin = ToPosition(npc.Origin); - if (!mesh.positionIsValid(origin)) { - return false; + moveArea *= DotRecastHelper.MapRotation.GetRightAxis().Length(); // changing moveArea to navmesh space + DtStatus end = crowd.GetNavMeshQuery().FindRandomPointWithinCircle(startRef, startVec, moveArea, crowd.GetFilter(0), new RcRand(), out _, out RcVec3f randomPt); + if (end.Failed()) { + return npc.Origin; } - Position end = mesh.generateRandomPositionLocally(origin, npc.Value.Metadata.Action.MoveArea); - return SetPathTo(end); + return DotRecastHelper.FromNavMeshSpace(randomPt); } public Vector3 FindClosestPoint(Vector3 point, int maxDistance, Vector3 fallback) { - var capsule = npc.Value.Metadata.Property.Capsule; - Shape shape = npc.Field.Navigation.GetShape((int) capsule.Radius, (int) capsule.Height); - - Position position = ToPosition(point); - - if (!mesh.positionIsValid(position)) { + if (!FindNearestPoly(point, out long closest, out RcVec3f position)) { return fallback; } - Position closest = mesh.findClosestUnobstructedPosition(shape, context, position, maxDistance); + float distance = maxDistance * DotRecastHelper.MapRotation.GetRightAxis().Length(); // changing distance to navmesh space + var status = crowd.GetNavMeshQuery().FindRandomPointAroundCircle(closest, position, distance, crowd.GetFilter(0), new RcRand(), out _, out RcVec3f randomPt); - if (!TryFromPosition(closest, out Vector3 result)) { + if (status.Failed()) { return fallback; } - return result; + return DotRecastHelper.FromNavMeshSpace(randomPt); } public Vector3 FindClosestPoint(Vector3 point, int maxDistance) { - return FindClosestPoint(point, maxDistance, FromPosition(agent.getPosition())); + return FindClosestPoint(point, maxDistance, DotRecastHelper.FromNavMeshSpace(agent.npos)); } public bool PathTo(Vector3 goal) { - if (!mesh.positionIsValid(agent.getPosition())) { + if (!FindNearestPoly(agent.npos, out _, out _)) { return false; } - Position end = ToPosition(goal); - if (!mesh.positionIsValid(end)) { + if (!FindNearestPoly(goal, out _, out RcVec3f end)) { return false; } return SetPathTo(end); } - private bool SetPathTo(Position target) { - currentPath?.Dispose(); - currentPath = null; + private bool SetPathTo(RcVec3f target) { + currentPath = []; + currentPathIndex = 0; + currentPathProgress = 0; try { - currentPath = agent.findShortestPathTo(context, target); - } catch (PathEngineException) { /* ignored */ } + currentPath = FindPath(agent.npos, target); + } catch (Exception ex) { + Logger.Error(ex, "Failed to find path to {Target}", target); + } return currentPath != null; } public bool PathAway(Vector3 goal, int distance) { - if (!mesh.positionIsValid(agent.getPosition())) { + if (!FindNearestPoly(agent.npos, out _, out _)) { return false; } - Position end = ToPosition(goal); - if (!mesh.positionIsValid(end)) { + if (!FindNearestPoly(goal, out _, out _)) { return false; } - return SetPathAway(end, distance); + return SetPathAway(goal, distance); } - private bool SetPathAway(Position target, int distance) { - currentPath?.Dispose(); - currentPath = null; + private bool SetPathAway(Vector3 target, int distance) { + currentPath = []; + currentPathIndex = 0; + currentPathProgress = 0; try { - currentPath = agent.findPathAway(context, target, distance); - } catch (PathEngineException) { /* ignored */ } - - return currentPath != null; - } - - #region Conversion - private Position ToPosition(Vector3 vector) { - return mesh.positionNear3DPoint((int) vector.X, (int) vector.Y, (int) vector.Z, horizontalRange: 25, verticalRange: 5); - } - - private bool TryFromPosition(Position position, out Vector3 result) { - if (!mesh.positionIsValid(position)) { - result = new Vector3(0, 0, 0); - - return false; + // find a random point away from the target + Vector3 randomPoint = FindClosestPoint(target, distance); + currentPath = FindPath(agent.npos, DotRecastHelper.ToNavMeshSpace(randomPoint)); + } catch (Exception ex) { + Logger.Error(ex, "Failed to find path away from {Target}", target); } - float z = mesh.heightAtPositionF(position); - - result = new Vector3(position.X, position.Y, z); - - return true; - } - - private Vector3 FromPosition(Position position) { - if (!mesh.positionIsValid(position)) { - return default; - } - - float z = mesh.heightAtPositionF(position); - return new Vector3(position.X, position.Y, z); + return currentPath != null; } - #endregion } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager.State.cs index 31a9697c8..eefdd7b82 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager.State.cs @@ -2,10 +2,10 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using DotRecast.Detour.Crowd; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; -using Maple2.PathEngine; using Maple2.Server.Game.Model; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Packets; @@ -93,13 +93,10 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId } public FieldNpc? SpawnNpc(NpcMetadata npc, Vector3 position, Vector3 rotation, FieldMobSpawn? owner = null, SpawnPointNPC? spawnPointNpc = null) { - Agent? agent = Navigation.AddAgent(npc, position); + DtCrowdAgent agent = Navigation.AddAgent(npc, position); AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); Vector3 spawnPosition = position; - if (agent is not null) { - spawnPosition = Navigation.FromPosition(agent.getPosition()); - } var fieldNpc = new FieldNpc(this, NextLocalId(), agent, new Npc(npc, animation), npc.AiPath, patrolDataUUID: spawnPointNpc?.PatrolData) { Owner = owner, Position = spawnPosition, @@ -126,20 +123,16 @@ public FieldPlayer SpawnPlayer(GameSession session, Player player, int portalId return null; } - Agent? agent = Navigation.AddAgent(npc, position); - if (agent == null) { - return null; - } + DtCrowdAgent agent = Navigation.AddAgent(npc, position); // We use GlobalId if there is an owner because players can move between maps. int objectId = player != null ? NextGlobalId() : NextLocalId(); AnimationMetadata? animation = NpcMetadata.GetAnimation(npc.Model.Name); - Vector3 spawnPosition = Navigation.FromPosition(agent.getPosition()); var fieldPet = new FieldPet(this, objectId, agent, new Npc(npc, animation), pet, Constant.PetFieldAiPath, player) { Owner = owner, - Position = Navigation.FromPosition(agent.getPosition()), + Position = position, Rotation = rotation, - Origin = owner?.Position ?? spawnPosition, + Origin = owner?.Position ?? position, }; Pets[fieldPet.ObjectId] = fieldPet; diff --git a/Maple2.Server.Game/Manager/Field/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager.cs index 84877b13a..1fdc8f0d8 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager.cs @@ -80,7 +80,7 @@ public FieldManager(MapMetadata metadata, UgcMapMetadata ugcMetadata, MapEntityM ItemDrop = new ItemDropManager(this); - Navigation = new Navigation(metadata.XBlock, entities.NavMesh?.Data); + Navigation = new Navigation(metadata.XBlock); } // Init is separate from constructor to allow properties to be injected first. diff --git a/Maple2.Server.Game/Manager/Field/Navigation.cs b/Maple2.Server.Game/Manager/Field/Navigation.cs index 306f0346a..61e8029e1 100644 --- a/Maple2.Server.Game/Manager/Field/Navigation.cs +++ b/Maple2.Server.Game/Manager/Field/Navigation.cs @@ -1,12 +1,13 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; +using System.Numerics; +using DotRecast.Detour; +using DotRecast.Detour.Crowd; +using DotRecast.Detour.Io; +using DotRecast.Recast.Toolset; +using DotRecast.Recast.Toolset.Builder; using Maple2.Model.Metadata; -using Maple2.PathEngine; -using Maple2.PathEngine.Interface; -using Maple2.PathEngine.Types; using Maple2.Server.Game.Model; -using Maple2.Server.Game.Util; +using Maple2.Tools; +using Maple2.Tools.DotRecast; using Serilog; namespace Maple2.Server.Game.Manager.Field; @@ -14,98 +15,62 @@ namespace Maple2.Server.Game.Manager.Field; public sealed class Navigation : IDisposable { private static readonly ILogger Logger = Log.Logger.ForContext(); - // ErrorHandler must be initialized here to retain ownership. - private static readonly LogErrorHandler ErrorHandler = new(Logger); - private static readonly PathEngine.PathEngine PathEngine = new(ErrorHandler); - public readonly string Name; - private readonly Mesh mesh; - private readonly CollisionContext context; - - private readonly ConcurrentDictionary<(int, int), Shape> shapeCache = new(); + private readonly DtNavMesh navMesh; + private readonly DtNavMeshQuery navMeshQuery; + public DtCrowd Crowd { get; private set; } + private readonly DtCrowdAgentConfig crowdAgentConfig = new DtCrowdAgentConfig(); - public Navigation(string name, byte[]? data = null) { + public Navigation(string name) { Name = name; - if (data == null) { - Logger.Error("No navigation mesh for: {XBlock}", name); - mesh = PathEngine.buildMeshFromContent(Array.Empty()); - } else { - mesh = PathEngine.loadMeshFromBuffer(FileFormat.tok, data); - } - context = mesh.newContext(); - } - - public AgentNavigation ForAgent(FieldNpc npc, Agent agent) { - return new AgentNavigation(npc, agent, mesh, context); + navMesh = LoadNavMesh(); + navMeshQuery = new DtNavMeshQuery(navMesh); + + Crowd = new DtCrowd(new DtCrowdConfig(maxAgentRadius: 0.3f), navMesh, __ => new DtQueryDefaultFilter( + SampleAreaModifications.SAMPLE_POLYFLAGS_ALL, + SampleAreaModifications.SAMPLE_POLYFLAGS_DISABLED, + [1f, 10f, 1f, 1f, 2f, 1.5f]) // TODO: understand what actually these values are + ); } - public Agent? AddAgent(NpcMetadata metadata, Vector3 origin) { - // Using radius for width for now - Shape shape = GetShape((int) metadata.Property.Capsule.Radius, (int) metadata.Property.Capsule.Height); - if (!TryFindPosition(shape, ToPosition(origin), metadata.Action.MoveArea, out Position? position)) { - return null; - } + private DtNavMesh LoadNavMesh() { + FileStream fs = new FileStream(System.IO.Path.Combine(Paths.NAVMESH_DIR, $"{Name}.navmesh"), FileMode.Open, FileAccess.Read); + BinaryReader br = new BinaryReader(fs); + DtMeshSetReader reader = new DtMeshSetReader(); - Agent agent = mesh.placeAgent(shape, (Position) position); - context.addAgent(agent); - return agent; + DtNavMesh dtNavMesh = reader.Read(br, DotRecastHelper.VERTS_PER_POLY); + br.Close(); + fs.Close(); + return dtNavMesh; } - private bool TryFindPosition(Shape? shape, Position origin, int distance, [NotNullWhen(true)] out Position? position) { - position = origin; - if (!mesh.positionIsValid(origin)) { - return false; - } - - if (distance > 0) { - try { - // Unobstructed Position is required for pathfinding, attempt to find one. - position = mesh.findClosestUnobstructedPosition(shape, context, (Position) position, distance); - } catch { /* ignored */ } - } - - if (!mesh.positionIsValid((Position) position)) { - return false; - } - - return true; + public AgentNavigation ForAgent(FieldNpc npc, DtCrowdAgent agent) { + return new AgentNavigation(npc, agent, Crowd); } - public Position ToPosition(Vector3 vector) { - return mesh.positionNear3DPoint((int) vector.X, (int) vector.Y, (int) vector.Z, horizontalRange: 25, verticalRange: 5); + public DtCrowdAgent AddAgent(NpcMetadata metadata, Vector3 origin) { + RcNavMeshBuildSettings settings = DotRecastHelper.NavMeshBuildSettings; + // use metadata speed instead of settings? + DtCrowdAgentParams agentParams = CreateAgentParams(0.3f, 1.4f, settings.agentMaxAcceleration, settings.agentMaxSpeed); + return Crowd.AddAgent(DotRecastHelper.ToNavMeshSpace(origin), agentParams); } - public Vector3 FromPosition(Position position) { - if (!mesh.positionIsValid(position)) { - return default; - } - - float z = mesh.heightAtPositionF(position); - return new Vector3(position.X, position.Y, z); - } - - public Shape GetShape(int width, int height) { - if (shapeCache.TryGetValue((width, height), out Shape? shape)) { - return shape; - } - - int halfWidth = Math.Max(width / 2, 1); - int halfHeight = Math.Max(height / 2, 1); - List vertices = [ - new Point(-halfWidth, -halfHeight), - new Point(-halfWidth, halfHeight), - new Point(halfWidth, halfHeight), - new Point(halfWidth, -halfHeight), - ]; - - shape = PathEngine.newShape(vertices); - mesh.generateUnobstructedSpaceFor(shape, true); - mesh.generatePathfindPreprocessFor(shape); - shapeCache.TryAdd((width, height), shape); - return shape; + private DtCrowdAgentParams CreateAgentParams(float agentRadius, float agentHeight, float agentMaxAcceleration, float agentMaxSpeed) { + DtCrowdAgentParams ap = new() { + radius = agentRadius, + height = agentHeight, + maxAcceleration = agentMaxAcceleration, + maxSpeed = agentMaxSpeed, + updateFlags = crowdAgentConfig.GetUpdateFlags(), + obstacleAvoidanceType = crowdAgentConfig.obstacleAvoidanceType, + separationWeight = crowdAgentConfig.separationWeight + }; + ap.collisionQueryRange = ap.radius * 12.0f; + ap.pathOptimizationRange = ap.radius * 30.0f; + return ap; } public void Dispose() { - mesh.Dispose(); + } } diff --git a/Maple2.Server.Game/Maple2.Server.Game.csproj b/Maple2.Server.Game/Maple2.Server.Game.csproj index 5248b4e31..8f3729f34 100644 --- a/Maple2.Server.Game/Maple2.Server.Game.csproj +++ b/Maple2.Server.Game/Maple2.Server.Game.csproj @@ -27,7 +27,6 @@ - @@ -37,6 +36,10 @@ + + + + diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index a7ce43a80..450cae0e6 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -3,9 +3,7 @@ using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; -using Maple2.PathEngine; using Maple2.Server.Game.Manager.Field; -using Maple2.Server.Game.Model.Routine; using Maple2.Server.Game.Model.Skill; using Maple2.Server.Game.Model.State; using Maple2.Server.Game.Packets; @@ -14,11 +12,11 @@ using Maple2.Server.Game.Session; using Maple2.Server.Game.Model.Field.Actor.ActorStateComponent; using Maple2.Tools.Extensions; -using Maple2.Database.Storage; using static Maple2.Server.Game.Model.Field.Actor.ActorStateComponent.TaskState; using Maple2.Server.Game.Model.Enum; using Maple2.Server.Core.Packets; using Serilog; +using DotRecast.Detour.Crowd; namespace Maple2.Server.Game.Model; @@ -96,7 +94,7 @@ public short SequenceId { public readonly Dictionary AiExtraData = new(); - public FieldNpc(FieldManager field, int objectId, Agent? agent, Npc npc, string aiPath, string spawnAnimation = "", string? patrolDataUUID = null) : base(field, objectId, npc, npc.Metadata.Model.Name, field.NpcMetadata) { + public FieldNpc(FieldManager field, int objectId, DtCrowdAgent? agent, Npc npc, string aiPath, string spawnAnimation = "", string? patrolDataUUID = null) : base(field, objectId, npc, npc.Metadata.Model.Name, field.NpcMetadata) { IdleSequence = npc.Animations.GetValueOrDefault("Idle_A") ?? new AnimationSequence(string.Empty, -1, 1f, null); JumpSequence = npc.Animations.GetValueOrDefault("Jump_A") ?? npc.Animations.GetValueOrDefault("Jump_B"); WalkSequence = npc.Animations.GetValueOrDefault("Walk_A"); @@ -130,9 +128,7 @@ public FieldNpc(FieldManager field, int objectId, Agent? agent, Npc npc, string } - protected override void Dispose(bool disposing) { - Navigation?.Dispose(); - } + protected override void Dispose(bool disposing) { } protected virtual void Remove(int delay) => Field.RemoveNpc(ObjectId, delay); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs index 365ef0791..88011b06f 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldPet.cs @@ -1,8 +1,8 @@ -using Maple2.Database.Storage; +using DotRecast.Detour.Crowd; +using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Game; using Maple2.Model.Metadata; -using Maple2.PathEngine; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Manager.Field; using Maple2.Server.Game.Model.Skill; @@ -24,7 +24,7 @@ public sealed class FieldPet : FieldNpc { public int TamingPoint; private long tamingTick; - public FieldPet(FieldManager field, int objectId, Agent agent, Npc npc, Item pet, string aiPath, FieldPlayer? owner = null) : base(field, objectId, agent, npc, aiPath) { + public FieldPet(FieldManager field, int objectId, DtCrowdAgent agent, Npc npc, Item pet, string aiPath, FieldPlayer? owner = null) : base(field, objectId, agent, npc, aiPath) { this.owner = owner; Pet = pet; diff --git a/Maple2.Server.Game/Util/LogErrorHandler.cs b/Maple2.Server.Game/Util/LogErrorHandler.cs deleted file mode 100644 index 745cd3bf2..000000000 --- a/Maple2.Server.Game/Util/LogErrorHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Maple2.PathEngine.Exception; -using Maple2.PathEngine.Interface; -using Maple2.PathEngine.Types; -using Serilog; - -namespace Maple2.Server.Game.Util; - -public class LogErrorHandler : IErrorHandler { - private readonly ILogger logger; - - public LogErrorHandler(ILogger logger) { - this.logger = logger; - } - - public override ErrorResult handle(ErrorType type, string description, IDictionary attributes) { - if (type is ErrorType.Fatal or ErrorType.Assertion) { - throw new PathEngineException(type, description, attributes); - } - - Action logAction = type switch { - ErrorType.Warning => logger.Warning, - ErrorType.NonFatal => logger.Information, - _ => logger.Error, - }; - logAction($"[{type}] {description}"); - foreach ((string key, string value) in attributes) { - logAction($"- {key}={value}"); - } - - return ErrorResult.Continue; - } -} diff --git a/Maple2.Tools/DotRecast/DotRecastHelper.cs b/Maple2.Tools/DotRecast/DotRecastHelper.cs new file mode 100644 index 000000000..8e6ca95a2 --- /dev/null +++ b/Maple2.Tools/DotRecast/DotRecastHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Numerics; +using DotRecast.Core.Numerics; +using DotRecast.Recast; +using DotRecast.Recast.Toolset; +using Maple2.Tools.Extensions; + +namespace Maple2.Tools.DotRecast; +public static class DotRecastHelper { + public const int VERTS_PER_POLY = 6; + public const float CELL_SIZE = 0.05f; + public static readonly RcNavMeshBuildSettings NavMeshBuildSettings = new() { + cellSize = CELL_SIZE, + cellHeight = CELL_SIZE, + agentHeight = 1.4f, // approximation of character height + agentRadius = 0.3f, // approximation of character radius + agentMaxClimb = 0.7f, + agentMaxSlope = 47f, // generally 45 degrees, but we add a bit more to account for floating point errors + agentMaxAcceleration = 8f, + agentMaxSpeed = 3.5f, + minRegionSize = 8, + mergedRegionSize = 20, + partitioning = (int) RcPartition.WATERSHED, + filterLowHangingObstacles = true, + filterLedgeSpans = true, + filterWalkableLowHeightSpans = true, + edgeMaxLen = 12f, + edgeMaxError = 1.3f, + vertsPerPoly = VERTS_PER_POLY, + detailSampleDist = 6f, + detailSampleMaxError = 3f, + keepInterResults = true, + }; + + public const float STEP_SIZE = 0.5f; + public const float MIN_TARGET_DIST = 0.01f; + + public static readonly Matrix4x4 MapRotation = Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, (float) (-Math.PI / 2)) * Matrix4x4.CreateScale(1 / 100f); + public static readonly Matrix4x4 MapRotationInv = Matrix4x4.CreateScale(100f) * Matrix4x4.CreateFromAxisAngle(Vector3.UnitX, (float) (Math.PI / 2)); + + public static RcVec3f ToNavMeshSpace(Vector3 vector) { + Vector3 transform = Vector3.Transform(vector, MapRotation); + return new RcVec3f(transform.X, transform.Y, transform.Z); + } + + public static Vector3 FromNavMeshSpace(RcVec3f position) { + Vector3 vector = new(position.X, position.Y, position.Z); + return Vector3.Transform(vector, MapRotationInv); + } +} \ No newline at end of file diff --git a/Maple2.Tools/Extensions/VectorExtensions.cs b/Maple2.Tools/Extensions/VectorExtensions.cs index 2d306c447..01ce6b150 100644 --- a/Maple2.Tools/Extensions/VectorExtensions.cs +++ b/Maple2.Tools/Extensions/VectorExtensions.cs @@ -1,5 +1,4 @@ -using Microsoft.VisualBasic; -using System; +using System; using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; diff --git a/Maple2.Tools/Maple2.Tools.csproj b/Maple2.Tools/Maple2.Tools.csproj index 4c5d3910d..9d2a27d0e 100644 --- a/Maple2.Tools/Maple2.Tools.csproj +++ b/Maple2.Tools/Maple2.Tools.csproj @@ -15,6 +15,8 @@ + + diff --git a/Maple2.Tools/Paths.cs b/Maple2.Tools/Paths.cs index a440b61bd..bd05c75ab 100644 --- a/Maple2.Tools/Paths.cs +++ b/Maple2.Tools/Paths.cs @@ -9,6 +9,9 @@ public static class Paths { public static readonly string DB_SEEDS_DIR = Path.Combine(SOLUTION_DIR, "Maple2.Database.Seed", "Seeds"); - public static readonly string WEB_DATA_DIR = Path.Combine(SOLUTION_DIR, "Maple2.Server.Web/Data"); + public static readonly string WEB_DATA_DIR = Path.Combine(SOLUTION_DIR, "Maple2.Server.Web", "Data"); + + public static readonly string NAVMESH_DIR = Path.Combine(SOLUTION_DIR, "Maple2.File.Ingest", "Navmeshes"); + public static readonly string NAVMESH_HASH_DIR = Path.Combine(SOLUTION_DIR, "Maple2.File.Ingest", "Navmeshes", "Hashes"); } From 3f8dc2a4c565d9f9dfc1e5880f1da0bbe4367b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 12 Jul 2024 21:09:49 -0300 Subject: [PATCH 2/7] remove old navmesh db --- Maple2.Database/Context/MetadataContext.cs | 7 ------- Maple2.Database/Storage/Metadata/MapEntityStorage.cs | 1 - Maple2.Model/Metadata/MapEntityMetadata.cs | 1 - Maple2.Model/Metadata/NavMesh.cs | 3 --- 4 files changed, 12 deletions(-) delete mode 100644 Maple2.Model/Metadata/NavMesh.cs diff --git a/Maple2.Database/Context/MetadataContext.cs b/Maple2.Database/Context/MetadataContext.cs index 4c57b8c16..51b788f8d 100644 --- a/Maple2.Database/Context/MetadataContext.cs +++ b/Maple2.Database/Context/MetadataContext.cs @@ -15,7 +15,6 @@ public sealed class MetadataContext(DbContextOptions options) : DbContext(option public DbSet NpcMetadata { get; set; } = null!; public DbSet MapMetadata { get; set; } = null!; public DbSet MapEntity { get; set; } = null!; - public DbSet NavMesh { get; set; } = null!; public DbSet PetMetadata { get; set; } = null!; public DbSet QuestMetadata { get; set; } = null!; public DbSet RideMetadata { get; set; } = null!; @@ -39,7 +38,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(ConfigureNpcMetadata); modelBuilder.Entity(ConfigureMapMetadata); modelBuilder.Entity(ConfigureMapEntity); - modelBuilder.Entity(ConfigureNavMesh); modelBuilder.Entity(ConfigurePetMetadata); modelBuilder.Entity(ConfigureQuestMetadata); modelBuilder.Entity(ConfigureRideMetadata); @@ -133,11 +131,6 @@ private static void ConfigureMapEntity(EntityTypeBuilder builder) { builder.Property(entity => entity.Block).HasJsonConversion().IsRequired(); } - private static void ConfigureNavMesh(EntityTypeBuilder builder) { - builder.ToTable("nav-mesh"); - builder.HasKey(navmesh => navmesh.XBlock); - } - private static void ConfigurePetMetadata(EntityTypeBuilder builder) { builder.ToTable("pet"); builder.HasKey(pet => pet.Id); diff --git a/Maple2.Database/Storage/Metadata/MapEntityStorage.cs b/Maple2.Database/Storage/Metadata/MapEntityStorage.cs index ead22541c..1730875ac 100644 --- a/Maple2.Database/Storage/Metadata/MapEntityStorage.cs +++ b/Maple2.Database/Storage/Metadata/MapEntityStorage.cs @@ -130,7 +130,6 @@ public class MapEntityStorage(MetadataContext context) : MetadataStorage EventItemSpawns { get; init; } public TaxiStation? Taxi { get; init; } public Prism BoundingBox { get; init; } - public NavMesh? NavMesh { get; init; } public required IReadOnlyDictionary BreakableActors { get; init; } diff --git a/Maple2.Model/Metadata/NavMesh.cs b/Maple2.Model/Metadata/NavMesh.cs deleted file mode 100644 index d3f2f0297..000000000 --- a/Maple2.Model/Metadata/NavMesh.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Maple2.Model.Metadata; - -public record NavMesh(string XBlock, byte[] Data); From fc7edd6062f7e06944fc7341ce0f152b2d70f54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sun, 14 Jul 2024 07:38:35 -0300 Subject: [PATCH 3/7] remove usage of precomputed terrain --- Maple2.File.Ingest/Program.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index 977552807..3f6ddb337 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -29,7 +29,6 @@ string xmlPath = Path.Combine(ms2Root, "Xml.m2d"); string exportedPath = Path.Combine(ms2Root, "Resource/Exported.m2d"); -string terrainPath = Path.Combine(ms2Root, "Resource/PrecomputedTerrain.m2d"); string serverPath = Path.Combine(ms2Root, "Server.m2d"); if (!File.Exists(xmlPath)) { @@ -40,10 +39,6 @@ throw new FileNotFoundException("Could not find Exported.m2d file"); } -if (!File.Exists(terrainPath)) { - throw new FileNotFoundException("Could not find PrecomputedTerrain.m2d file"); -} - if (!File.Exists(serverPath)) { throw new FileNotFoundException("Could not find Server.m2d file, check discord for this file. Link in README.md"); } @@ -62,7 +57,6 @@ using var xmlReader = new M2dReader(xmlPath); using var exportedReader = new M2dReader(exportedPath); -using var terrainReader = new M2dReader(terrainPath); using var serverReader = new M2dReader(serverPath); DbContextOptions options = new DbContextOptionsBuilder() From 3ef8df9510806fdaf4e416b6f1572c6f96e0c95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 15 Jul 2024 09:54:14 -0300 Subject: [PATCH 4/7] formatting --- Maple2.File.Ingest/Mapper/NavMeshMapper.cs | 56 ++++++++++++++----- Maple2.File.Ingest/Utils/NavmeshHash.cs | 4 +- Maple2.File.Ingest/Utils/Recast.cs | 4 +- .../20240709075218_RemoveGameEvent.cs | 18 ++---- Maple2.Tools/DotRecast/DotRecastHelper.cs | 4 +- 5 files changed, 54 insertions(+), 32 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/NavMeshMapper.cs b/Maple2.File.Ingest/Mapper/NavMeshMapper.cs index 78f3df45b..ec9c9f2a7 100644 --- a/Maple2.File.Ingest/Mapper/NavMeshMapper.cs +++ b/Maple2.File.Ingest/Mapper/NavMeshMapper.cs @@ -53,21 +53,49 @@ public class NavMeshMapper { ]; private readonly List indexBuffer = [ - 1, 4, 5, - 4, 6, 7, - 7, 5, 4, - 2, 5, 7, - 6, 4, 1, - 3, 7, 6, - 5, 2, 1, - 7, 3, 2, - 2, 3, 0, - 1, 0, 6, - 6, 0, 3, - 0, 1, 2, + 1, + 4, + 5, + 4, + 6, + 7, + 7, + 5, + 4, + 2, + 5, + 7, + 6, + 4, + 1, + 3, + 7, + 6, + 5, + 2, + 1, + 7, + 3, + 2, + 2, + 3, + 0, + 1, + 0, + 6, + 6, + 0, + 3, + 0, + 1, + 2, // bottom face - 11, 10, 9, - 9, 8, 11, + 11, + 10, + 9, + 9, + 8, + 11, ]; public NavMeshMapper(MetadataContext db, M2dReader exportedReader) { diff --git a/Maple2.File.Ingest/Utils/NavmeshHash.cs b/Maple2.File.Ingest/Utils/NavmeshHash.cs index a1bf659ef..a25e1447b 100644 --- a/Maple2.File.Ingest/Utils/NavmeshHash.cs +++ b/Maple2.File.Ingest/Utils/NavmeshHash.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography; +using System.Security.Cryptography; using Maple2.Tools; namespace Maple2.File.Ingest.Utils; @@ -38,4 +38,4 @@ private static string GetHash(string filename) { byte[] hash = md5.ComputeHash(stream); return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } -} \ No newline at end of file +} diff --git a/Maple2.File.Ingest/Utils/Recast.cs b/Maple2.File.Ingest/Utils/Recast.cs index 20ab22b5a..458d045d0 100644 --- a/Maple2.File.Ingest/Utils/Recast.cs +++ b/Maple2.File.Ingest/Utils/Recast.cs @@ -1,4 +1,4 @@ -using DotRecast.Core.Collections; +using DotRecast.Core.Collections; using DotRecast.Core.Numerics; using DotRecast.Recast; using DotRecast.Recast.Geom; @@ -126,4 +126,4 @@ public RcVec3f GetMeshBoundsMin() { public IEnumerable Meshes() { return RcImmutableArray.Create(mesh); } -} \ No newline at end of file +} diff --git a/Maple2.Server.World/Migrations/20240709075218_RemoveGameEvent.cs b/Maple2.Server.World/Migrations/20240709075218_RemoveGameEvent.cs index 3057c222a..5ee19def3 100644 --- a/Maple2.Server.World/Migrations/20240709075218_RemoveGameEvent.cs +++ b/Maple2.Server.World/Migrations/20240709075218_RemoveGameEvent.cs @@ -4,25 +4,20 @@ #nullable disable -namespace Maple2.Server.World.Migrations -{ +namespace Maple2.Server.World.Migrations { /// - public partial class RemoveGameEvent : Migration - { + public partial class RemoveGameEvent : Migration { /// - protected override void Up(MigrationBuilder migrationBuilder) - { + protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "game-event"); } /// - protected override void Down(MigrationBuilder migrationBuilder) - { + protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "game-event", - columns: table => new - { + columns: table => new { Id = table.Column(type: "int", nullable: false) .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), BeginTime = table.Column(type: "datetime(6)", nullable: false), @@ -32,8 +27,7 @@ protected override void Down(MigrationBuilder migrationBuilder) Name = table.Column(type: "longtext", nullable: false) .Annotation("MySql:CharSet", "utf8mb4") }, - constraints: table => - { + constraints: table => { table.PrimaryKey("PK_game-event", x => x.Id); }) .Annotation("MySql:CharSet", "utf8mb4"); diff --git a/Maple2.Tools/DotRecast/DotRecastHelper.cs b/Maple2.Tools/DotRecast/DotRecastHelper.cs index 8e6ca95a2..a74a17dd6 100644 --- a/Maple2.Tools/DotRecast/DotRecastHelper.cs +++ b/Maple2.Tools/DotRecast/DotRecastHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Numerics; using DotRecast.Core.Numerics; using DotRecast.Recast; @@ -47,4 +47,4 @@ public static Vector3 FromNavMeshSpace(RcVec3f position) { Vector3 vector = new(position.X, position.Y, position.Z); return Vector3.Transform(vector, MapRotationInv); } -} \ No newline at end of file +} From c0fae5c9af954c6e27aed7b9dd6f43e20300d42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 2 Aug 2024 16:10:43 -0300 Subject: [PATCH 5/7] path away --- .../Manager/Field/AgentNavigation.cs | 54 +++++++----- .../MovementState.WalkTask.cs | 2 +- .../Model/Field/Actor/FieldNpc.cs | 82 ++++++++++--------- 3 files changed, 77 insertions(+), 61 deletions(-) diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index e98d60ce8..8ac425a42 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Diagnostics; +using System.Numerics; using DotRecast.Core; using DotRecast.Core.Numerics; using DotRecast.Detour; @@ -17,7 +18,7 @@ public sealed class AgentNavigation { public readonly DtCrowdAgent agent; private readonly DtCrowd crowd; - public List currentPath = []; + public List? currentPath = []; private int currentPathIndex = 0; private float currentPathProgress = 0; @@ -27,33 +28,33 @@ public AgentNavigation(FieldNpc fieldNpc, DtCrowdAgent dtAgent, DtCrowd dtCrowd) crowd = dtCrowd; } - public List FindPath(Vector3 startVec, Vector3 targetVec) { + public List? FindPath(Vector3 startVec, Vector3 targetVec) { return FindPath(crowd, DotRecastHelper.ToNavMeshSpace(startVec), DotRecastHelper.ToNavMeshSpace(targetVec)); } - public List FindPath(RcVec3f startVec, RcVec3f targetVec) { + public List? FindPath(RcVec3f startVec, RcVec3f targetVec) { return FindPath(crowd, startVec, targetVec); } - private List FindPath(DtCrowd crowd, RcVec3f startVec, RcVec3f targetVec) { + private List? FindPath(DtCrowd crowd, RcVec3f startVec, RcVec3f targetVec) { DtNavMesh navMesh = crowd.GetNavMesh(); DtNavMeshQuery navMeshQuery = crowd.GetNavMeshQuery(); IDtQueryFilter filter = crowd.GetFilter(0); if (!FindNearestPoly(startVec, out long pos1Ref, out RcVec3f _)) { Logger.Error("Failed to find nearest poly at {StartVec}", startVec); - return []; + return null; } if (!FindNearestPoly(targetVec, out long posRef2, out RcVec3f _)) { Logger.Error("Failed to find nearest poly at {TargetVec}", targetVec); - return []; + return null; } List pathIterPolys = []; navMeshQuery.FindPath(pos1Ref, posRef2, startVec, targetVec, filter, ref pathIterPolys, new DtFindPathOption(0, float.MaxValue)); if (pathIterPolys.Count == 0) { Logger.Error("Failed to find path from {StartVec} to {TargetVec}", startVec, targetVec); - return []; + return null; } int pathIterPolysCount = pathIterPolys.Count; @@ -176,7 +177,7 @@ private bool FindNearestPoly(Vector3 point, out long nearestRef, out RcVec3f pos private bool FindNearestPoly(RcVec3f point, out long nearestRef, out RcVec3f position) { var status = crowd.GetNavMeshQuery().FindNearestPoly(point, new RcVec3f(2, 4, 2), crowd.GetFilter(0), out nearestRef, out position, out _); if (status.Failed()) { - Logger.Error("Failed to find nearest poly from {Source} => {Position}", point, position); + Logger.Error("Failed to find nearest poly from position {Source} for NPC {Npc}", point, npc.Value.Metadata.Name); return false; } @@ -189,11 +190,11 @@ public Vector3 GetAgentPosition() { public (Vector3 Start, Vector3 End) Advance(TimeSpan timeSpan, float speed, out bool followSegment) { followSegment = false; - if (currentPath.Count < 2) { + if (currentPath?.Count < 2) { return default; } - Vector3 start = DotRecastHelper.FromNavMeshSpace(currentPath[currentPathIndex]); + Vector3 start = DotRecastHelper.FromNavMeshSpace(currentPath![currentPathIndex]); if (currentPathIndex >= currentPath.Count - 1) { return default; @@ -283,33 +284,44 @@ private bool SetPathTo(RcVec3f target) { Logger.Error(ex, "Failed to find path to {Target}", target); } - return currentPath != null; + return currentPath is not null; } - public bool PathAway(Vector3 goal, int distance) { - if (!FindNearestPoly(agent.npos, out _, out _)) { + public bool PathAwayFrom(Vector3 goal, int distance) { + if (!FindNearestPoly(agent.npos, out _, out RcVec3f position)) { return false; } - if (!FindNearestPoly(goal, out _, out _)) { + // get target in navmesh space + RcVec3f target = DotRecastHelper.ToNavMeshSpace(goal); + + // get distance in navmesh space + float fDistance = distance * DotRecastHelper.MapRotation.GetRightAxis().Length(); + + // get direction from agent to target + RcVec3f direction = RcVec3f.Subtract(target, position); + + // get the point that is fDistance away from the target in the opposite direction + RcVec3f positionAway = RcVec3f.Add(position, RcVec3f.Normalize(direction) * -fDistance); + + // find the nearest poly to the positionAway + if (!FindNearestPoly(positionAway, out _, out RcVec3f positionAwayNavMesh)) { return false; } - return SetPathAway(goal, distance); + return SetPathAway(positionAwayNavMesh); } - private bool SetPathAway(Vector3 target, int distance) { + private bool SetPathAway(RcVec3f target) { currentPath = []; currentPathIndex = 0; currentPathProgress = 0; try { - // find a random point away from the target - Vector3 randomPoint = FindClosestPoint(target, distance); - currentPath = FindPath(agent.npos, DotRecastHelper.ToNavMeshSpace(randomPoint)); + currentPath = FindPath(agent.npos, target); } catch (Exception ex) { Logger.Error(ex, "Failed to find path away from {Target}", target); } - return currentPath != null; + return currentPath is not null; } } diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs index 4fadafc3c..42d57571e 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/MovementStateTasks/MovementState.WalkTask.cs @@ -158,7 +158,7 @@ private void MoveTargetDistance(NpcTask task, IActor target, float distance, str if (currentDistance < fromDistance * fromDistance) { actor.AppendDebugMessage($"> Pathing away target\n"); - foundPath = actor.Navigation.PathAway(target.Position, (int) distance); + foundPath = actor.Navigation.PathAwayFrom(target.Position, (int) distance); type = WalkType.FromTarget; } else if (currentDistance > toDistance * toDistance) { actor.AppendDebugMessage($"> Pathing to target\n"); diff --git a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs index 450cae0e6..d8945479f 100644 --- a/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs +++ b/Maple2.Server.Game/Model/Field/Actor/FieldNpc.cs @@ -224,47 +224,9 @@ public override void KeyframeEvent(string keyName) { private NpcTask? NextRoutine(long tickCount) { if (Patrol?.WayPoints.Count > 0 && Navigation is not null) { - MS2WayPoint currentWaypoint = Patrol.WayPoints[currentWaypointIndex]; - - if (!string.IsNullOrEmpty(currentWaypoint.ArriveAnimation) && idleTask is not MovementState.NpcEmoteTask) { - if (Value.Animations.TryGetValue(currentWaypoint.ArriveAnimation, out AnimationSequence? arriveSequence)) { - return MovementState.TryEmote(arriveSequence.Name, false); - } - } - - NpcTask? approachTask = null; - - if (Navigation.PathTo(currentWaypoint.Position)) { - if (Value.Animations.TryGetValue(currentWaypoint.ApproachAnimation, out AnimationSequence? patrolSequence)) { - approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, sequence: patrolSequence.Name); - } else if (WalkSequence is not null) { - approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, WalkSequence.Name); - } else { - Log.Logger.Warning("No walk sequence found for npc {NpcId} in patrol {PatrolId}", Value.Metadata.Id, Patrol.Uuid); - } - } - - MS2WayPoint lastWaypoint = Patrol.WayPoints.Last(); - - // if we're at the last waypoint and we're not looping, we're done - if (currentWaypoint.Id == lastWaypoint.Id && !Patrol.IsLoop) { - Patrol = null; - - return approachTask; - } - - currentWaypointIndex = (currentWaypointIndex + 1) % Patrol.WayPoints.Count; - - if ((approachTask?.Status ?? NpcTaskStatus.Cancelled) == NpcTaskStatus.Cancelled) { - Log.Logger.Warning("Failed to path to waypoint id({Id}) coord {Coord} for npc {NpcId} in patrol {PatrolId}", currentWaypoint.Id, currentWaypoint.Position, Value.Metadata.Name, Patrol.Uuid); - - return MovementState.TryStandby(null, true); - } - - return approachTask; + return NextWaypoint(); } - string routineName = defaultRoutines.Get(); if (!Value.Animations.TryGetValue(routineName, out AnimationSequence? sequence)) { Logger.Error("Invalid routine: {Routine} for npc {NpcId}", routineName, Value.Metadata.Id); @@ -294,6 +256,48 @@ public override void KeyframeEvent(string keyName) { return MovementState.TryStandby(null, true); } + private NpcTask? NextWaypoint() { + MS2WayPoint currentWaypoint = Patrol!.WayPoints[currentWaypointIndex]; + + if (!string.IsNullOrEmpty(currentWaypoint.ArriveAnimation) && idleTask is not MovementState.NpcEmoteTask) { + if (Value.Animations.TryGetValue(currentWaypoint.ArriveAnimation, out AnimationSequence? arriveSequence)) { + return MovementState.TryEmote(arriveSequence.Name, false); + } + } + + NpcTask? approachTask = null; + + if (Navigation!.PathTo(currentWaypoint.Position)) { + if (Value.Animations.TryGetValue(currentWaypoint.ApproachAnimation, out AnimationSequence? patrolSequence)) { + approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, sequence: patrolSequence.Name); + } else if (WalkSequence is not null) { + approachTask = MovementState.TryMoveTo(currentWaypoint.Position, false, WalkSequence.Name); + } else { + Logger.Warning("No walk sequence found for npc {NpcId} in patrol {PatrolId}", Value.Metadata.Id, Patrol.Uuid); + } + } else { + Logger.Warning("Failed to path to waypoint id({Id}) coord {Coord} for npc {NpcName} - {NpcId} in patrol {PatrolId}", currentWaypoint.Id, currentWaypoint.Position, Value.Metadata.Name, Value.Metadata.Id, Patrol.Uuid); + } + + MS2WayPoint lastWaypoint = Patrol.WayPoints.Last(); + + // if we're at the last waypoint and we're not looping, we're done + if (currentWaypoint.Id == lastWaypoint.Id && !Patrol.IsLoop) { + Patrol = null; + + return approachTask; + } + + currentWaypointIndex = (currentWaypointIndex + 1) % Patrol.WayPoints.Count; + + if ((approachTask?.Status ?? NpcTaskStatus.Cancelled) == NpcTaskStatus.Cancelled) { + Logger.Warning("Failed to path to waypoint id({Id}) coord {Coord} for npc {NpcName} - {NpcId} in patrol {PatrolId}", currentWaypoint.Id, currentWaypoint.Position, Value.Metadata.Name, Value.Metadata.Id, Patrol.Uuid); + return MovementState.TryStandby(null, true); + } + + return approachTask; + } + protected override void OnDeath() { Owner?.Despawn(ObjectId); SendControl = false; From 5e99bd60b04528b511771094c4993558c138b231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 2 Aug 2024 16:20:18 -0300 Subject: [PATCH 6/7] fix in range --- .../Model/Field/Actor/ActorStateComponent/BattleState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs index eefb9b1e6..90f2d75c8 100644 --- a/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs +++ b/Maple2.Server.Game/Model/Field/Actor/ActorStateComponent/BattleState.cs @@ -89,7 +89,7 @@ public void Update(long tickCount) { } public bool IsInRange(float distanceSquared, float verticalOffset, float radiusSquared, float heightUp, float heightDown) { - return distanceSquared < radiusSquared && verticalOffset <= heightUp && verticalOffset >= heightDown; + return distanceSquared < radiusSquared && verticalOffset <= heightUp && verticalOffset >= heightDown - 10; } public bool ShouldTargetActor(IActor target, float radiusSquared, float heightUp, float heightDown) { From 4f50aed1b10c526cecc5b46e8ad167c4cc90ebae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 2 Aug 2024 16:20:26 -0300 Subject: [PATCH 7/7] normalize direction --- Maple2.Server.Game/Manager/Field/AgentNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs index 8ac425a42..8fac6af44 100644 --- a/Maple2.Server.Game/Manager/Field/AgentNavigation.cs +++ b/Maple2.Server.Game/Manager/Field/AgentNavigation.cs @@ -299,7 +299,7 @@ public bool PathAwayFrom(Vector3 goal, int distance) { float fDistance = distance * DotRecastHelper.MapRotation.GetRightAxis().Length(); // get direction from agent to target - RcVec3f direction = RcVec3f.Subtract(target, position); + RcVec3f direction = RcVec3f.Normalize(RcVec3f.Subtract(target, position)); // get the point that is fDistance away from the target in the opposite direction RcVec3f positionAway = RcVec3f.Add(position, RcVec3f.Normalize(direction) * -fDistance);