From 05ce452e5680ac2669704c83dcbac5c3e5c821c5 Mon Sep 17 00:00:00 2001 From: mettaursp Date: Thu, 11 Jul 2024 22:25:31 -0700 Subject: [PATCH 01/11] got more map entities parsing & sorted into a grid structure or aabb tree --- Maple2.Database/Context/MetadataContext.cs | 2 + Maple2.File.Ingest/Helpers/NifParserHelper.cs | 53 ++++++- Maple2.File.Ingest/Mapper/MapEntityMapper.cs | 134 +++++++++++++++++ Maple2.File.Ingest/Mapper/NifMapper.cs | 9 +- Maple2.File.Ingest/Program.cs | 11 +- Maple2.Model/Metadata/NifMetadata.cs | 5 +- Maple2.Tools/VectorMath/BoundingBox3.cs | 135 ++++++++++++++++++ 7 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 Maple2.Tools/VectorMath/BoundingBox3.cs diff --git a/Maple2.Database/Context/MetadataContext.cs b/Maple2.Database/Context/MetadataContext.cs index 95c7fd8ec..c867028f2 100644 --- a/Maple2.Database/Context/MetadataContext.cs +++ b/Maple2.Database/Context/MetadataContext.cs @@ -220,12 +220,14 @@ private static void ConfigureNifMetadata(EntityTypeBuilder builder) builder.ToTable("nif"); builder.HasKey(nif => nif.Llid); builder.Property(nif => nif.Blocks).HasJsonConversion(); + builder.Property(nif => nif.PhysXBounds).HasJsonConversion(); } private static void ConfigureNXSMeshMetadata(EntityTypeBuilder builder) { builder.ToTable("nxs-mesh"); builder.Property(mesh => mesh.Index).ValueGeneratedNever(); builder.HasKey(mesh => mesh.Index); + builder.Property(nif => nif.Bounds).HasJsonConversion(); } private static void ConfigureFunctionCubeMetadata(EntityTypeBuilder builder) { diff --git a/Maple2.File.Ingest/Helpers/NifParserHelper.cs b/Maple2.File.Ingest/Helpers/NifParserHelper.cs index 7ef9eae28..fe2d4d634 100644 --- a/Maple2.File.Ingest/Helpers/NifParserHelper.cs +++ b/Maple2.File.Ingest/Helpers/NifParserHelper.cs @@ -1,11 +1,16 @@ using Maple2.File.IO.Nif; using Maple2.File.Parser; +using Maple2.Model.Common; using Maple2.Model.Metadata; +using Maple2.Tools.Extensions; +using Maple2.Tools.VectorMath; +using System.Numerics; namespace Maple2.File.Ingest.Helpers; public static class NifParserHelper { public static Dictionary nifDocuments { get; private set; } = []; + public static Dictionary nifBounds { get; private set; } = []; public static Dictionary nxsMeshIndexMap { get; private set; } = []; public static List nxsMeshes { get; private set; } = []; @@ -19,7 +24,7 @@ public static void ParseNif(List modelReaders) { nifDocuments = nifDocuments.OrderBy(item => item.Key).ToDictionary(item => item.Key, item => item.Value); foreach (KeyValuePair nifDocument in nifDocuments) { - GenerateNxsMeshMetadata(nifDocument.Value); + nifBounds.Add(nifDocument.Key, GenerateNxsMeshMetadata(nifDocument.Value)); } } @@ -41,14 +46,56 @@ private static void ParseNifDocument(uint llid, NifDocument document) { } } - private static void GenerateNxsMeshMetadata(NifDocument document) { + private static BoundingBox3 GenerateNxsMeshMetadata(NifDocument document) { foreach (NiPhysXMeshDesc meshDesc in document.Blocks.OfType()) { string meshDataString = Convert.ToBase64String(meshDesc.MeshData); if (!nxsMeshIndexMap.ContainsKey(meshDataString)) { int value = nxsMeshes.Count + 1; // 1-based index nxsMeshIndexMap[meshDataString] = value; - nxsMeshes.Add(new NxsMeshMetadata(value, meshDesc.MeshData)); + + Vector3 min = new Vector3(); + Vector3 max = new Vector3(); + + PhysXMesh mesh = new PhysXMesh(meshDesc.MeshData); + + nxsMeshes.Add(new NxsMeshMetadata(value, meshDesc.MeshData, BoundingBox3.Compute(mesh.Vertices))); + } + } + + BoundingBox3 bounds = new BoundingBox3(); + bool firstSet = true; + + foreach (NifBlock item in document.Blocks) { + if (item is not NiPhysXProp prop) { + continue; + } + + if (prop.Snapshot is null) { + continue; + } + + foreach (NiPhysXActorDesc actorDesc in prop.Snapshot.Actors) { + foreach (NiPhysXShapeDesc shapeDesc in actorDesc.ShapeDescriptions) { + if (shapeDesc.Mesh is null) { + continue; + } + + PhysXMesh mesh = new PhysXMesh(shapeDesc.Mesh.MeshData); + Matrix4x4 transform = Matrix4x4.CreateScale(prop.PhysXToWorldScale) * actorDesc.Poses[0] * shapeDesc.LocalPose; + BoundingBox3 meshBounds = BoundingBox3.Transform(BoundingBox3.Compute(mesh.Vertices), transform); + + if (!firstSet) { + bounds = bounds.Expand(meshBounds); + + continue; + } + + bounds = meshBounds; + firstSet = false; + } } } + + return bounds; } } diff --git a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs index 0535c3285..bc3dc2db3 100644 --- a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs @@ -1,13 +1,19 @@ 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.IO; +using Maple2.File.IO.Nif; using Maple2.File.Parser.Flat; using Maple2.File.Parser.MapXBlock; +using Maple2.Model.Common; using Maple2.Model.Enum; using Maple2.Model.Metadata; using Maple2.Tools.Extensions; +using Maple2.Tools.VectorMath; +using System.Numerics; using static M2dXmlGenerator.FeatureLocaleFilter; namespace Maple2.File.Ingest.Mapper; @@ -38,6 +44,134 @@ private IEnumerable ParseMap(string xblock, IEnumerable e } } + Dictionary> gridAlignedEntities = new Dictionary>(); + List unalignedEntities = new List(); + Vector3S minIndex = new Vector3S(short.MaxValue, short.MaxValue, short.MaxValue); + Vector3S maxIndex = new Vector3S(short.MinValue, short.MinValue, short.MinValue); + + foreach (IMapEntity entity in entities) { + Vector3S nearestCubeIndex = new Vector3S(); + + if (entity is not IPlaceable placeable) { + continue; + } + + Transform transform = new Transform(); + transform.Position = placeable.Position; + transform.RotationAnglesDegrees = placeable.Rotation; + transform.Scale = placeable.Scale; + + Vector3 position = (1 / 150.0f) * (placeable.Position - new Vector3(0, 0, 75)); // offset to round to nearest + nearestCubeIndex = new Vector3S((short) Math.Floor(position.X + 0.5f), (short) Math.Floor(position.Y + 0.5f), (short) Math.Floor(position.Z + 0.5f)); + Vector3 voxelPosition = 150.0f * new Vector3(nearestCubeIndex.X, nearestCubeIndex.Y, nearestCubeIndex.Z); + BoundingBox3 entityBounds = new BoundingBox3(); + + switch (entity) { + /* + PhysXProp | WhiteboxCube + PhysXProp, MS2MapProperties, MS2Vibrate | PhysXCube, DoesMakeTok + MS2MapProperties | PhysXCube + MS2MapProperties, MS2Vibrate | None + MS2MapProperties, MS2Breakable | PhysXCube, NxCube, BothCube, OnlyNxCube + MS2Breakable | NxCube + */ + case IMS2Breakable breakable: + continue; // intentionally skip breakables. these are dynamic so should be handled at run time + case IPhysXWhitebox whitebox: + entityBounds.Min = -new Vector3(whitebox.ShapeDimensions.X, whitebox.ShapeDimensions.Y, 0.5f * whitebox.ShapeDimensions.Z); + entityBounds.Max = new Vector3(whitebox.ShapeDimensions.X, whitebox.ShapeDimensions.Y, 0.5f * whitebox.ShapeDimensions.Z); + break; + case IMesh mesh: + if (entity is IMS2Vibrate vibrate && vibrate.Enabled) { + entityBounds.Min = -new Vector3(75, 75, 0); + entityBounds.Max = new Vector3(75, 75, 150); + + break; + } + + bool isFluid = false; + + if (entity is IMS2MapProperties meshMapProperties) { + if (meshMapProperties.DisableCollision) { + continue; + } + + if (meshMapProperties.GeneratePhysX) { + Vector3 meshPhysXDimension = new Vector3(150, 150, 150); + + if (meshMapProperties.GeneratePhysXDimension != Vector3.Zero) { + meshPhysXDimension = meshMapProperties.GeneratePhysXDimension; + } + + entityBounds.Min = -new Vector3(0.5f * meshPhysXDimension.X, 0.5f * meshPhysXDimension.Y, 0); + entityBounds.Max = new Vector3(0.5f * meshPhysXDimension.X, 0.5f * meshPhysXDimension.Y, meshPhysXDimension.Z); + + break; + } + + isFluid = meshMapProperties.CubeType == "Fluid"; + } + + if (mesh.NifAsset.Length < 9 || mesh.NifAsset.Substring(0, 9).ToLower() != "urn:llid:") { + Console.WriteLine($"Non llid NifAsset: '{mesh.NifAsset}'"); + + continue; + } + + // require length of "urn:llid:XXXXXXXX" + if (mesh.NifAsset.Length < 9 + 8) { + continue; + } + + uint llid = Convert.ToUInt32(mesh.NifAsset.Substring(mesh.NifAsset.LastIndexOf(':') + 1, 8), 16); + + if (!NifParserHelper.nifBounds.TryGetValue(llid, out entityBounds)) { + Console.WriteLine($"NIF with LLID {llid:X} not found"); + + continue; + } + + break; + case IMS2MapProperties mapProperties: // GeneratePhysX + if (mapProperties.DisableCollision) { + continue; + } + + if (!mapProperties.GeneratePhysX) { + continue; + } + + Vector3 physXDimension = new Vector3(150, 150, 150); + + if (mapProperties.GeneratePhysXDimension != Vector3.Zero) { + physXDimension = mapProperties.GeneratePhysXDimension; + } + + entityBounds.Min = -new Vector3(0.5f * physXDimension.X, 0.5f * physXDimension.Y, 0); + entityBounds.Max = new Vector3(0.5f * physXDimension.X, 0.5f * physXDimension.Y, physXDimension.Z); + + break; + default: + continue; + } + + entityBounds = BoundingBox3.Transform(entityBounds, transform.Transformation); + + BoundingBox3 cellBounds = new BoundingBox3(voxelPosition - new Vector3(75, 75, 0), voxelPosition + new Vector3(75, 75, 150)); + + if (!cellBounds.Contains(entityBounds, 1e-5f)) { + // put in list for aabb tree + + continue; + } + + // grid aligned + minIndex = new Vector3S(Math.Min(minIndex.X, nearestCubeIndex.X), Math.Min(minIndex.Y, nearestCubeIndex.Y), Math.Min(minIndex.Z, nearestCubeIndex.Z)); + maxIndex = new Vector3S(Math.Max(maxIndex.X, nearestCubeIndex.X), Math.Max(maxIndex.Y, nearestCubeIndex.Y), Math.Max(maxIndex.Z, nearestCubeIndex.Z)); + } + + maxIndex += new Vector3S(0, 0, 1); // make room for potential spawn tiles + foreach (IMapEntity entity in entities) { switch (entity) { case IMS2InteractObject interactObject: diff --git a/Maple2.File.Ingest/Mapper/NifMapper.cs b/Maple2.File.Ingest/Mapper/NifMapper.cs index 269d44746..a06126280 100644 --- a/Maple2.File.Ingest/Mapper/NifMapper.cs +++ b/Maple2.File.Ingest/Mapper/NifMapper.cs @@ -2,15 +2,20 @@ using Maple2.File.IO.Nif; using Maple2.Model.Enum; using Maple2.Model.Metadata; +using Maple2.Tools.VectorMath; +using System.Numerics; namespace Maple2.File.Ingest.Mapper; public class NifMapper : TypeMapper { protected override IEnumerable Map() { foreach ((uint llid, NifDocument document) in NifParserHelper.nifDocuments) { + BoundingBox3 bounds = NifParserHelper.nifBounds[llid]; + yield return new NifMetadata( - llid, - MapBlockMetadata(document).ToArray() + Llid: llid, + PhysXBounds: bounds, + Blocks: MapBlockMetadata(document).ToArray() ); } } diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index 9144eb22a..17153d477 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -117,12 +117,11 @@ new("/model/character/", Path.Combine(ms2Root, "Resource/Model/Character.m2d")), new("/model/textures/", Path.Combine(ms2Root, "Resource/Model/Textures.m2d")), }; - -UpdateDatabase(metadataContext, new AdditionalEffectMapper(xmlReader)); -UpdateDatabase(metadataContext, new AnimationMapper(xmlReader)); -UpdateDatabase(metadataContext, new ItemMapper(xmlReader)); -UpdateDatabase(metadataContext, new NpcMapper(xmlReader)); -UpdateDatabase(metadataContext, new PetMapper(xmlReader)); +//UpdateDatabase(metadataContext, new AdditionalEffectMapper(xmlReader)); +//UpdateDatabase(metadataContext, new AnimationMapper(xmlReader)); +//UpdateDatabase(metadataContext, new ItemMapper(xmlReader)); +//UpdateDatabase(metadataContext, new NpcMapper(xmlReader)); +//UpdateDatabase(metadataContext, new PetMapper(xmlReader)); UpdateDatabase(metadataContext, new MapMapper(xmlReader)); UpdateDatabase(metadataContext, new UgcMapMapper(xmlReader)); UpdateDatabase(metadataContext, new ExportedUgcMapMapper(xmlReader)); diff --git a/Maple2.Model/Metadata/NifMetadata.cs b/Maple2.Model/Metadata/NifMetadata.cs index 182496c34..f4c1a4e9e 100644 --- a/Maple2.Model/Metadata/NifMetadata.cs +++ b/Maple2.Model/Metadata/NifMetadata.cs @@ -3,11 +3,13 @@ using System.Numerics; using System.Text.Json.Serialization; using Maple2.Model.Enum; +using Maple2.Tools.VectorMath; namespace Maple2.Model.Metadata; public record NifMetadata( uint Llid, + BoundingBox3 PhysXBounds, NifMetadata.NifBlockMetadata[] Blocks ) { @@ -62,5 +64,6 @@ int Mesh // NiPhysXMeshDesc public record NxsMeshMetadata( int Index, - byte[] Data + byte[] Data, + BoundingBox3 Bounds ); diff --git a/Maple2.Tools/VectorMath/BoundingBox3.cs b/Maple2.Tools/VectorMath/BoundingBox3.cs new file mode 100644 index 000000000..344aa7de6 --- /dev/null +++ b/Maple2.Tools/VectorMath/BoundingBox3.cs @@ -0,0 +1,135 @@ +using Maple2.Tools.Extensions; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Maple2.Tools.VectorMath; + +public struct BoundingBox3 { + public Vector3 Min; + public Vector3 Max; + + public Vector3 Size { get => Max - Min; } + public Vector3 Center { get => 0.5f * (Max + Min); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundingBox3(Vector3 min = new Vector3()) { + Min = min; + Max = min; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundingBox3(Vector3 min, Vector3 max) { + Min = min; + Max = max; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundingBox3 Expand(Vector3 point) { + return new BoundingBox3( + Vector3.Min(Min, point), + Vector3.Max(Max, point) + ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundingBox3 Expand(BoundingBox3 box) { + return this.Expand(box.Min).Expand(box.Max); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BoundingBox3 Fatten(float amount) { + return new BoundingBox3( + Min - new Vector3(amount, amount, amount), + Max + new Vector3(amount, amount, amount) + ); + } + + public static BoundingBox3 Transform(BoundingBox3 box, Matrix4x4 matrix) { + Vector3 translation = matrix.Translation; + + BoundingBox3 result = new BoundingBox3(translation); + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + float a = matrix[j, i] * box.Min[j]; + float b = matrix[j, i] * box.Max[j]; + + if (a > b) { + (a, b) = (b, a); + } + + result.Min[i] += a; + result.Max[i] += b; + } + } + + Vector3 size = box.Max - box.Min; + List vertices = new List() { + box.Min, + box.Max, + box.Min + new Vector3(size.X, 0, 0), + box.Min + new Vector3(size.X, size.Y, 0), + box.Min + new Vector3(size.X, 0, size.Z), + box.Min + new Vector3(0, size.Y, 0), + box.Min + new Vector3(0, size.Y, size.Z), + box.Min + new Vector3(0, 0, size.Z) + }; + + for (int i = 0; i < 8; ++i) { + vertices[i] = Vector3.Transform(vertices[i], matrix); + } + + BoundingBox3 result2 = Compute(vertices); + + if (!result.IsNearlyEqual(result2, 1e-2f)) { + throw new System.Exception("possibly wrong axis"); + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(Vector3 point, float epsilon = 0) { + bool withinMinBounds = point.X >= Min.X - epsilon && point.Y >= Min.Y - epsilon && point.Z >= Min.Z - epsilon; + bool withinMaxBounds = point.X <= Max.X + epsilon && point.Y <= Max.Y + epsilon && point.Z <= Max.Z + epsilon; + + return withinMinBounds && withinMaxBounds; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Contains(BoundingBox3 box, float epsilon = 0) { + bool withinMinBounds = box.Min.X >= Min.X - epsilon && box.Min.Y >= Min.Y - epsilon && box.Min.Z >= Min.Z - epsilon; + bool withinMaxBounds = box.Max.X <= Max.X + epsilon && box.Max.Y <= Max.Y + epsilon && box.Max.Z <= Max.Z + epsilon; + + return withinMinBounds && withinMaxBounds; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Intersects(BoundingBox3 box, float epsilon = 0) { + BoundingBox3 compoundBox = new BoundingBox3(Min, Max + box.Size); + + return compoundBox.Contains(box.Max, epsilon); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsNearlyEqual(BoundingBox3 box, float epsilon = 1e-5f) { + return Min.IsNearlyEqual(box.Min, epsilon) && Max.IsNearlyEqual(box.Max, epsilon); + } + + public static BoundingBox3 Compute(List points) { + if (points.Count == 0) { + return new BoundingBox3(); + } + + BoundingBox3 box = new BoundingBox3(points[0]); + + foreach (Vector3 point in points) { + box = box.Expand(point); + } + + return box; + } +} + From 07ba4b1b3896d59e279ae2940aa2c05978b9e71d Mon Sep 17 00:00:00 2001 From: mettaursp Date: Wed, 28 Aug 2024 17:17:46 -0700 Subject: [PATCH 02/11] ui refactor --- .../Graphics/DebugGraphicsContext.cs | 32 +++++++++++++------ .../Graphics/ImGuiController.cs | 5 ++- .../Graphics/Ui/UiUtils.cs | 32 +++++++++++++++++++ .../Ui/{ => Windows}/FieldListWindow.cs | 5 +-- .../Graphics/Ui/{ => Windows}/IUiWindow.cs | 2 +- .../Ui/{ => Windows}/WindowListWindow.cs | 3 +- 6 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 Maple2.Server.DebugGame/Graphics/Ui/UiUtils.cs rename Maple2.Server.DebugGame/Graphics/Ui/{ => Windows}/FieldListWindow.cs (95%) rename Maple2.Server.DebugGame/Graphics/Ui/{ => Windows}/IUiWindow.cs (90%) rename Maple2.Server.DebugGame/Graphics/Ui/{ => Windows}/WindowListWindow.cs (97%) diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index 3877bc519..b82408b62 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -18,6 +18,7 @@ public class DebugGraphicsContext : IGraphicsContext { public static readonly bool ForceDXVK = false; public static readonly Vector2D DefaultWindowSize = new Vector2D(800, 600); public static readonly float[] WindowClearColor = { 0.0f, 0.0f, 0.0f, 1.0f }; + public static readonly ILogger Logger = Log.Logger.ForContext(); public readonly Dictionary Fields; @@ -42,7 +43,16 @@ public class DebugGraphicsContext : IGraphicsContext { private string resourceRootPath = ""; private List fieldRenderers = new(); - public IReadOnlyList FieldRenderers { get => fieldRenderers; } + private Mutex fieldRendererMutex = new(); + public DebugFieldRenderer[] FieldRenderers { + get { + fieldRendererMutex.WaitOne(); + DebugFieldRenderer[] renderers = fieldRenderers.ToArray(); + fieldRendererMutex.ReleaseMutex(); + + return renderers; + } + } private DebugFieldRenderer? selectedRenderer = null; public IReadOnlyList FieldWindows { get => fieldWindows; } private List fieldWindows = new(); @@ -151,19 +161,19 @@ public void Initialize() { DebuggerWindow.Load += OnLoad; DebuggerWindow.Closing += OnClose; - Log.Information("Creating window"); + Logger.Information("Creating window"); new Thread(RunDebugger).Start(); } public static unsafe void DxLog(Message message) { if (message.PDescription is null) { - Log.Information("Null DirectX error"); + Logger.Error("Null DirectX error"); return; } - Log.Information(SilkMarshal.PtrToString((nint) message.PDescription) ?? "Unknown DirectX error"); + Logger.Error(SilkMarshal.PtrToString((nint) message.PDescription) ?? "Unknown DirectX error"); } private void OnClose() { @@ -249,7 +259,7 @@ private void OnLoad() { this.CoreModels = new CoreModels(this); - Log.Information("Graphics context initialized"); + Logger.Information("Graphics context initialized"); SampleTexture = new Texture(this); SampleTexture.Load("sample_derp_wave.png"); @@ -286,7 +296,7 @@ public void CleanUp() { DxSwapChain = default; Input = default; - Log.Information("Graphics context cleaning up"); + Logger.Information("Graphics context cleaning up"); } } @@ -295,7 +305,7 @@ public void CleanUp() { DebuggerWindow = default; - Log.Information("Window cleaning up"); + Logger.Information("Window cleaning up"); } } @@ -320,11 +330,13 @@ private static int CompareRenderers(DebugFieldRenderer item1, DebugFieldRenderer } public IFieldRenderer FieldAdded(FieldManager field) { - Log.Information("Field added {Name} [{Id}]", field.Metadata.Name, field.Metadata.Id); + Logger.Information("Field added {Name} [{Id}]", field.Metadata.Name, field.Metadata.Id); DebugFieldRenderer renderer = new DebugFieldRenderer(this, field); + fieldRendererMutex.WaitOne(); int index = fieldRenderers.AddSorted(renderer, Comparer.Create(CompareRenderers)); + fieldRendererMutex.ReleaseMutex(); return renderer; } @@ -334,11 +346,13 @@ public void FieldRemoved(FieldManager field) { return; } - Log.Information("Field removed {Name} [{Id}]", field.Metadata.Name, field.Metadata.Id); + Logger.Information("Field removed {Name} [{Id}]", field.Metadata.Name, field.Metadata.Id); renderer.CleanUp(); + fieldRendererMutex.WaitOne(); fieldRenderers.RemoveSorted(renderer, Comparer.Create(CompareRenderers)); + fieldRendererMutex.ReleaseMutex(); Fields.Remove(field); } diff --git a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs index 64b09b2df..96443c6b5 100644 --- a/Maple2.Server.DebugGame/Graphics/ImGuiController.cs +++ b/Maple2.Server.DebugGame/Graphics/ImGuiController.cs @@ -1,5 +1,6 @@ using ImGuiNET; -using Maple2.Server.DebugGame.Graphics.UI; +using Maple2.Server.DebugGame.Graphics.Ui.Windows; +using Serilog; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.Windowing; @@ -15,6 +16,8 @@ public enum ImGuiWindowType { } public class ImGuiController { + public static readonly ILogger Logger = Log.Logger.ForContext(); + public DebugGraphicsContext Context { get; init; } public IWindow? ParentWindow { get; private set; } public IInputContext Input { get; init; } diff --git a/Maple2.Server.DebugGame/Graphics/Ui/UiUtils.cs b/Maple2.Server.DebugGame/Graphics/Ui/UiUtils.cs new file mode 100644 index 000000000..dd49cddd7 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Ui/UiUtils.cs @@ -0,0 +1,32 @@ +using System.Reflection; + +namespace Maple2.Server.DebugGame.Graphics.Ui; + +public static class UiUtils { + public static string GetEventName(EventInfo eventInfo) { + return "Event " + (eventInfo.EventHandlerType?.Name ?? ""); + } + + public static string GetMethodName(MethodInfo methodInfo) { + return $"Method <{methodInfo.ReturnType}>"; + } + + public static string GetMemberDisplayName(MemberInfo member) { + string memberType = member.MemberType switch { + MemberTypes.Event => GetEventName((EventInfo) member), + MemberTypes.Field => ((FieldInfo) member).FieldType.Name, + MemberTypes.Method => GetMethodName((MethodInfo) member), + MemberTypes.Property => ((PropertyInfo) member).PropertyType.Name, + _ => "" + }; + + return $"[{member.Module.ScopeName}]: {memberType} {member.DeclaringType?.Name ?? "null"}.{member.Name}"; + } + + public static void ImGuiError(string message) { + ImGuiController.Logger.Error(message); + + throw new InvalidDataException(message); + } +} + diff --git a/Maple2.Server.DebugGame/Graphics/Ui/FieldListWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs similarity index 95% rename from Maple2.Server.DebugGame/Graphics/Ui/FieldListWindow.cs rename to Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs index 2c489b1bf..bd2abb7ba 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/FieldListWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/FieldListWindow.cs @@ -1,10 +1,7 @@ using ImGuiNET; -using Maple2.Server.DebugGame.Graphics.UI; -using Maple2.Server.Game.DebugGraphics; -using Maple2.Server.Game.Manager.Field; using System.Numerics; -namespace Maple2.Server.DebugGame.Graphics.Ui; +namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; public class FieldListWindow : IUiWindow { public bool AllowMainWindow { get => true; } diff --git a/Maple2.Server.DebugGame/Graphics/Ui/IUiWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs similarity index 90% rename from Maple2.Server.DebugGame/Graphics/Ui/IUiWindow.cs rename to Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs index 9517ffd97..f4813694b 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/IUiWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/IUiWindow.cs @@ -1,4 +1,4 @@ -namespace Maple2.Server.DebugGame.Graphics.UI; +namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; public interface IUiWindow { public bool AllowMainWindow { get => false; } diff --git a/Maple2.Server.DebugGame/Graphics/Ui/WindowListWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs similarity index 97% rename from Maple2.Server.DebugGame/Graphics/Ui/WindowListWindow.cs rename to Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs index 6e9dd98d2..6b9f81e5e 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/WindowListWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/Windows/WindowListWindow.cs @@ -1,8 +1,7 @@ using ImGuiNET; -using Maple2.Server.DebugGame.Graphics.UI; using System.Numerics; -namespace Maple2.Server.DebugGame.Graphics.Ui; +namespace Maple2.Server.DebugGame.Graphics.Ui.Windows; public class WindowListWindow : IUiWindow { public bool AllowMainWindow { get => true; } From 77f73c6710ce66783e8da555e6d1b2d2fb993db9 Mon Sep 17 00:00:00 2001 From: mettaursp Date: Wed, 28 Aug 2024 19:18:45 -0700 Subject: [PATCH 03/11] render primitives --- .../Graphics/Assets/CoreModels.cs | 102 ++++++++++++++++++ .../Graphics/Scene/Camera.cs | 23 ++++ Maple2.Tools/VectorMath/Aabb.cs | 5 + Maple2.Tools/VectorMath/Ray.cs | 10 ++ 4 files changed, 140 insertions(+) create mode 100644 Maple2.Server.DebugGame/Graphics/Scene/Camera.cs create mode 100644 Maple2.Tools/VectorMath/Aabb.cs create mode 100644 Maple2.Tools/VectorMath/Ray.cs diff --git a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs index aab6fb1a2..23330803c 100644 --- a/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs +++ b/Maple2.Server.DebugGame/Graphics/Assets/CoreModels.cs @@ -7,10 +7,17 @@ namespace Maple2.Server.DebugGame.Graphics.Assets; public class CoreModels { public DebugGraphicsContext Context { get; init; } public Mesh Quad { get; init; } + public Mesh Cube { get; init; } + public Mesh WireCube { get; init; } public CoreModels(DebugGraphicsContext context) { Context = context; Quad = CreateQuad(); + + var cubes = CreateCubes(); + + Cube = cubes.solid; + WireCube = cubes.wire; } private Mesh CreateQuad() { @@ -43,5 +50,100 @@ private Mesh CreateQuad() { return mesh; } + + private (Mesh solid, Mesh wire) CreateCubes() { + Ms2MeshData meshData = new Ms2MeshData(); + + meshData.PrimitiveCount = 2; + + PositionBinding[] cubeVertices = { + new PositionBinding(new Vector3(-0.5f, 0.5f, 0)), + new PositionBinding(new Vector3( 0.5f, 0.5f, 0)), + new PositionBinding(new Vector3(-0.5f, -0.5f, 0)), + new PositionBinding(new Vector3( 0.5f, -0.5f, 0)), + new PositionBinding(new Vector3(-0.5f, 0.5f, 1)), + new PositionBinding(new Vector3( 0.5f, 0.5f, 1)), + new PositionBinding(new Vector3(-0.5f, -0.5f, 1)), + new PositionBinding(new Vector3( 0.5f, -0.5f, 1)) + }; + + uint[] cubeSolidIndices = { + 0, 1, 3, + 0, 3, 2, + 4, 7, 5, + 4, 6, 7, + 0, 4, 5, + 0, 5, 1, + 1, 5, 7, + 1, 7, 3, + 3, 7, 6, + 3, 6, 2, + 0, 2, 6, + 0, 6, 4 + }; + + meshData.SetPositionBinding(cubeVertices); + + meshData.SetAttributeBinding(new Data.VertexBuffer.AttributeBinding[] { + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)), + new AttributeBinding(new Vector3(0, 0, -1), 0xFFFFFFFF, new Vector2(0, 0)) + }); + + meshData.IsTriangleMesh = false; + + meshData.SetIndexBuffer(new uint[] { + 0, 1, + 1, 3, + 3, 2, + 2, 0, + 0, 4, + 1, 5, + 3, 7, + 2, 6, + 4, 5, + 5, 7, + 7, 6, + 6, 4 + }); + + Mesh cubeWireMesh = new Mesh(Context); + + cubeWireMesh.UploadData(meshData); + + List positionBinding = new List(); + List attributeBinding = new List(); + + for (int i = 0; i < cubeSolidIndices.Length; i += 3) { + PositionBinding vertA = cubeVertices[i + 0]; + PositionBinding vertB = cubeVertices[i + 1]; + PositionBinding vertC = cubeVertices[i + 2]; + + positionBinding.Add(vertA); + positionBinding.Add(vertB); + positionBinding.Add(vertC); + + Vector3 normal = Vector3.Normalize(Vector3.Cross(vertC.Position - vertA.Position, vertB.Position - vertA.Position)); + + attributeBinding.Add(new AttributeBinding(normal, 0xFFFFFFFF, new Vector2(0, 0))); + attributeBinding.Add(new AttributeBinding(normal, 0xFFFFFFFF, new Vector2(0, 0))); + attributeBinding.Add(new AttributeBinding(normal, 0xFFFFFFFF, new Vector2(0, 0))); + } + + meshData.IsTriangleMesh = true; + meshData.SetPositionBinding(positionBinding.ToArray()); + meshData.SetIndexBuffer(cubeSolidIndices); + + Mesh cubeSolidMesh = new Mesh(Context); + + cubeSolidMesh.UploadData(meshData); + + return (cubeSolidMesh, cubeWireMesh); + } } diff --git a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs new file mode 100644 index 000000000..bfe387b09 --- /dev/null +++ b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs @@ -0,0 +1,23 @@ +using Maple2.Tools.VectorMath; +using System.Numerics; + +namespace Maple2.Server.DebugGame.Graphics.Scene; + +public class Camera { + public Transform Transform { get; init; } = new Transform(); + public Matrix4x4 ProjectionMatrix { get; private set; } + + public float AspectRatio { get; private set; } + public float NearPlane { get; private set; } + public float FarPlane { get; private set; } + public float FieldOfView { get; private set; } + + public void SetProperties(float fieldOfView, float aspectRatio, float nearPlane, float farPlane) { + + } + + public void SetProperties(float width, float height, float projectionPlane, float nearPlane, float farPlane) { + + } +} + diff --git a/Maple2.Tools/VectorMath/Aabb.cs b/Maple2.Tools/VectorMath/Aabb.cs new file mode 100644 index 000000000..d9c616dee --- /dev/null +++ b/Maple2.Tools/VectorMath/Aabb.cs @@ -0,0 +1,5 @@ +namespace Maple2.Tools.VectorMath; + +public class Aabb { +} + diff --git a/Maple2.Tools/VectorMath/Ray.cs b/Maple2.Tools/VectorMath/Ray.cs new file mode 100644 index 000000000..e2de06f3e --- /dev/null +++ b/Maple2.Tools/VectorMath/Ray.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Maple2.Tools.VectorMath { + internal class Ray { + } +} From 749b31c2b2ab95316e5cbcab4a01d0f9744e6314 Mon Sep 17 00:00:00 2001 From: mettaursp Date: Tue, 24 Sep 2024 18:26:34 -0700 Subject: [PATCH 04/11] added FieldAccelerationStructure & map entity parsing + storage --- Maple2.Database/Context/MetadataContext.cs | 8 + .../Storage/Metadata/MapDataStorage.cs | 39 ++ Maple2.File.Ingest/Mapper/MapDataMapper.cs | 334 +++++++++++ Maple2.File.Ingest/Mapper/MapEntityMapper.cs | 142 +---- Maple2.File.Ingest/Program.cs | 17 +- Maple2.Model/Common/Vector.cs | 2 + .../Game/Field/FieldAccelerationStructure.cs | 532 ++++++++++++++++++ Maple2.Model/Metadata/MapDataMetadata.cs | 5 + .../Metadata/MapEntity/FieldEntity.cs | 74 +++ .../Graphics/DebugGraphicsContext.cs | 2 - .../Graphics/Ui/FieldPropertiesWindow.cs | 4 +- Maple2.Tools/Extensions/StringExtension.cs | 8 + Maple2.Tools/VectorMath/Aabb.cs | 5 - 13 files changed, 1022 insertions(+), 150 deletions(-) create mode 100644 Maple2.Database/Storage/Metadata/MapDataStorage.cs create mode 100644 Maple2.File.Ingest/Mapper/MapDataMapper.cs create mode 100644 Maple2.Model/Game/Field/FieldAccelerationStructure.cs create mode 100644 Maple2.Model/Metadata/MapDataMetadata.cs create mode 100644 Maple2.Model/Metadata/MapEntity/FieldEntity.cs delete mode 100644 Maple2.Tools/VectorMath/Aabb.cs diff --git a/Maple2.Database/Context/MetadataContext.cs b/Maple2.Database/Context/MetadataContext.cs index c867028f2..0b964d00a 100644 --- a/Maple2.Database/Context/MetadataContext.cs +++ b/Maple2.Database/Context/MetadataContext.cs @@ -1,5 +1,6 @@ using Maple2.Database.Extensions; using Maple2.Database.Model.Metadata; +using Maple2.Model.Game.Field; using Maple2.Model.Metadata; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -28,6 +29,7 @@ public sealed class MetadataContext(DbContextOptions options) : DbContext(option public DbSet NifMetadata { get; set; } = null!; public DbSet NXSMeshMetadata { get; set; } = null!; public DbSet FunctionCubeMetadata { get; set; } = null!; + public DbSet MapDataMetadata { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -39,6 +41,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(ConfigureNpcMetadata); modelBuilder.Entity(ConfigureMapMetadata); modelBuilder.Entity(ConfigureMapEntity); + modelBuilder.Entity(ConfigureMapData); modelBuilder.Entity(ConfigurePetMetadata); modelBuilder.Entity(ConfigureQuestMetadata); modelBuilder.Entity(ConfigureRideMetadata); @@ -134,6 +137,11 @@ private static void ConfigureMapEntity(EntityTypeBuilder builder) { builder.Property(entity => entity.Block).HasJsonConversion().IsRequired(); } + private static void ConfigureMapData(EntityTypeBuilder builder) { + builder.ToTable("map-data"); + builder.HasKey(entity => entity.XBlock); + } + private static void ConfigurePetMetadata(EntityTypeBuilder builder) { builder.ToTable("pet"); builder.HasKey(pet => pet.Id); diff --git a/Maple2.Database/Storage/Metadata/MapDataStorage.cs b/Maple2.Database/Storage/Metadata/MapDataStorage.cs new file mode 100644 index 000000000..bcde05821 --- /dev/null +++ b/Maple2.Database/Storage/Metadata/MapDataStorage.cs @@ -0,0 +1,39 @@ +using Maple2.Database.Context; +using Maple2.Model.Game.Field; +using Maple2.Model.Metadata; +using Maple2.PacketLib.Tools; +using Maple2.Tools.Extensions; +using System.Diagnostics.CodeAnalysis; + +namespace Maple2.Database.Storage.Metadata; + +public class RideMetadataStorage(MetadataContext context) : MetadataStorage(context, CACHE_SIZE) { + private const int CACHE_SIZE = 500; // ~500 total items + + public bool TryGet(string xblock, [NotNullWhen(true)] out FieldAccelerationStructure? mapData) { + if (Cache.TryGet(xblock, out mapData)) { + return true; + } + + lock (Context) { + // Double-checked locking + if (Cache.TryGet(xblock, out mapData)) { + return true; + } + + MapDataMetadata? data = Context.MapDataMetadata.Find(xblock); + + if (data == null) { + return false; + } + + ByteReader reader = new ByteReader(data.Data); + + mapData = reader.ReadClass(); + + Cache.AddReplace(xblock, mapData); + } + + return true; + } +} diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs new file mode 100644 index 000000000..2fb3cb66a --- /dev/null +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -0,0 +1,334 @@ +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.IO; +using Maple2.File.Parser.MapXBlock; +using Maple2.Model.Common; +using Maple2.Model.Game.Field; +using Maple2.Model.Metadata; +using Maple2.Model.Metadata.FieldEntities; +using Maple2.PacketLib.Tools; +using Maple2.Tools.Extensions; +using Maple2.Tools.VectorMath; +using Pastel; +using System.Linq; +using System.Numerics; + +namespace Maple2.File.Ingest.Mapper; + +public class MapDataMapper : TypeMapper { + private readonly HashSet xBlocks; + private readonly XBlockParser parser; + + private readonly StatsTracker mapByteStats = new(); + private readonly StatsTracker mapXStats = new(); + private readonly StatsTracker mapYStats = new(); + private readonly StatsTracker mapZStats = new(); + private readonly StatsTracker alignedStats = new(); + private readonly StatsTracker unalignedStats = new(); + private readonly HashSet invalidLlids = new(); + private readonly HashSet missingLlids = new(); + + public MapDataMapper(MetadataContext db, M2dReader exportedReader, XBlockParser parser) { + xBlocks = db.MapMetadata.Select(metadata => metadata.XBlock).ToHashSet(); + + this.parser = parser; + } + + public class StatsTracker { + + public ulong MinValue = ulong.MaxValue; + public ulong MaxValue; + public ulong AvgValue { + get { + if (Entries == 0) return 0; + return TotalValue / Entries; + } + } + public ulong Entries; + public ulong TotalValue; + + public StatsTracker() { } + + public void AddValue(ulong value) { + ++Entries; + TotalValue += value; + MinValue = ulong.Min(MinValue, value); + MaxValue = ulong.Max(MaxValue, value); + } + } + + private FieldAccelerationStructure ParseMapEntities(string xblock, IEnumerable entities) { + Dictionary> gridAlignedEntities = new Dictionary>(); + List unalignedEntities = new List(); + Vector3S minIndex = new Vector3S(short.MaxValue, short.MaxValue, short.MaxValue); + Vector3S maxIndex = new Vector3S(short.MinValue, short.MinValue, short.MinValue); + + foreach (IMapEntity entity in entities) { + Vector3S nearestCubeIndex = new Vector3S(); + + if (entity is not IPlaceable placeable) { + continue; + } + + Transform transform = new Transform(); + transform.Position = placeable.Position; + transform.RotationAnglesDegrees = placeable.Rotation; + transform.Scale = placeable.Scale; + + Vector3 position = (1 / 150.0f) * (placeable.Position - new Vector3(0, 0, 75)); // offset to round to nearest + nearestCubeIndex = new Vector3S((short) Math.Floor(position.X + 0.5f), (short) Math.Floor(position.Y + 0.5f), (short) Math.Floor(position.Z + 0.5f)); + Vector3 voxelPosition = 150.0f * new Vector3(nearestCubeIndex.X, nearestCubeIndex.Y, nearestCubeIndex.Z); + BoundingBox3 entityBounds = new BoundingBox3(); + + ulong idHigh = 0; + ulong idLow = 0; + + bool isHexId = entity.EntityId.Length == 32; + + for (int i = 0; isHexId && i < entity.EntityId.Length; ++i) { + isHexId = entity.EntityId[i].IsHexDigit(); + } + + if (isHexId) { + idHigh = Convert.ToUInt64(entity.EntityId.Substring(0, 16), 16); + idLow = Convert.ToUInt64(entity.EntityId.Substring(16, 16), 16); + } + + FieldEntity? fieldEntity; + FieldEntityId entityId = new FieldEntityId(idHigh, idLow); + + switch (entity) { + /* + PhysXProp | WhiteboxCube + PhysXProp, MS2MapProperties, MS2Vibrate | PhysXCube, DoesMakeTok + MS2MapProperties | PhysXCube + MS2MapProperties, MS2Vibrate | None + MS2MapProperties, MS2Breakable | PhysXCube, NxCube, BothCube, OnlyNxCube + MS2Breakable | NxCube + */ + case IMS2Breakable breakable: + continue; // intentionally skip breakables. these are dynamic so should be handled at run time + case IPhysXWhitebox whitebox: + entityBounds.Min = -new Vector3(whitebox.ShapeDimensions.X, whitebox.ShapeDimensions.Y, 0.5f * whitebox.ShapeDimensions.Z); + entityBounds.Max = new Vector3(whitebox.ShapeDimensions.X, whitebox.ShapeDimensions.Y, 0.5f * whitebox.ShapeDimensions.Z); + fieldEntity = new FieldBoxColliderEntity( + Id: entityId, + Position: placeable.Position - new Vector3(0, 0, 0.5f * whitebox.ShapeDimensions.Z), + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds, + Size: whitebox.ShapeDimensions, + IsWhiteBox: true); + break; + case IMesh mesh: + if (entity is IMS2Vibrate vibrate && vibrate.Enabled) { + entityBounds.Min = -new Vector3(75, 75, 0); + entityBounds.Max = new Vector3(75, 75, 150); + + fieldEntity = new FieldVibrateEntity( + Id: entityId, + Position: placeable.Position, + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds); + + break; + } + + bool isFluid = false; + + if (entity is IMS2MapProperties meshMapProperties) { + if (meshMapProperties.DisableCollision) { + continue; + } + + if (meshMapProperties.GeneratePhysX) { + Vector3 meshPhysXDimension = new Vector3(150, 150, 150); + + if (meshMapProperties.GeneratePhysXDimension != Vector3.Zero) { + meshPhysXDimension = meshMapProperties.GeneratePhysXDimension; + } + + entityBounds.Min = -new Vector3(0.5f * meshPhysXDimension.X, 0.5f * meshPhysXDimension.Y, 0); + entityBounds.Max = new Vector3(0.5f * meshPhysXDimension.X, 0.5f * meshPhysXDimension.Y, meshPhysXDimension.Z); + + fieldEntity = new FieldBoxColliderEntity( + Id: entityId, + Position: placeable.Position, + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds, + Size: meshPhysXDimension, + IsWhiteBox: false); + + break; + } + + isFluid = meshMapProperties.CubeType == "Fluid"; + } + + if (mesh.NifAsset.Length < 9 || mesh.NifAsset.Substring(0, 9).ToLower() != "urn:llid:") { + if (!invalidLlids.Contains(mesh.NifAsset)) { + invalidLlids.Add(mesh.NifAsset); + Console.WriteLine($"Non llid NifAsset: '{mesh.NifAsset}'"); + } + + continue; + } + + // require length of "urn:llid:XXXXXXXX" + if (mesh.NifAsset.Length < 9 + 8) { + continue; + } + + uint llid = Convert.ToUInt32(mesh.NifAsset.Substring(mesh.NifAsset.LastIndexOf(':') + 1, 8), 16); + + if (!NifParserHelper.nifBounds.TryGetValue(llid, out entityBounds)) { + if (!missingLlids.Contains(llid)) { + missingLlids.Add(llid); + Console.WriteLine($"NIF with LLID {llid:X} not found"); + } + + continue; + } + + if (isFluid) { + fieldEntity = new FieldFluidEntity( + Id: entityId, + Position: placeable.Position, + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds, + MeshLlid: llid); + + continue; + } + + fieldEntity = new FieldMeshColliderEntity( + Id: entityId, + Position: placeable.Position, + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds, + MeshLlid: llid); + + break; + case IMS2MapProperties mapProperties: // GeneratePhysX + if (mapProperties.DisableCollision) { + continue; + } + + if (!mapProperties.GeneratePhysX) { + continue; + } + + Vector3 physXDimension = new Vector3(150, 150, 150); + + if (mapProperties.GeneratePhysXDimension != Vector3.Zero) { + physXDimension = mapProperties.GeneratePhysXDimension; + } + + entityBounds.Min = -new Vector3(0.5f * physXDimension.X, 0.5f * physXDimension.Y, 0); + entityBounds.Max = new Vector3(0.5f * physXDimension.X, 0.5f * physXDimension.Y, physXDimension.Z); + + fieldEntity = new FieldBoxColliderEntity( + Id: entityId, + Position: placeable.Position, + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds, + Size: physXDimension, + IsWhiteBox: false); + + break; + default: + continue; + } + + entityBounds = BoundingBox3.Transform(entityBounds, transform.Transformation); + + BoundingBox3 cellBounds = new BoundingBox3(voxelPosition - new Vector3(75, 75, 0), voxelPosition + new Vector3(75, 75, 150)); + + if (!cellBounds.Contains(entityBounds, 1e-5f)) { + // put in list for aabb tree + unalignedEntities.Add(fieldEntity); + + continue; + } + + // grid aligned + minIndex = new Vector3S(Math.Min(minIndex.X, nearestCubeIndex.X), Math.Min(minIndex.Y, nearestCubeIndex.Y), Math.Min(minIndex.Z, nearestCubeIndex.Z)); + maxIndex = new Vector3S(Math.Max(maxIndex.X, nearestCubeIndex.X), Math.Max(maxIndex.Y, nearestCubeIndex.Y), Math.Max(maxIndex.Z, nearestCubeIndex.Z)); + + if (!gridAlignedEntities.TryGetValue(nearestCubeIndex, out List? cellEntities)) { + cellEntities = new List(); + gridAlignedEntities.Add(nearestCubeIndex, cellEntities); + } + + cellEntities.Add(fieldEntity); + } + + maxIndex += new Vector3S(0, 0, 1); // make room for potential spawn tiles + + FieldAccelerationStructure fieldData = new FieldAccelerationStructure(); + + fieldData.AddEntities(gridAlignedEntities, minIndex, maxIndex, unalignedEntities); + + return fieldData; + } + + private byte[] GetEmptyMap() { + FieldAccelerationStructure mapData = new(); + + ByteWriter writer = new ByteWriter(); + + writer.WriteClass(mapData); + + return writer.ToArray(); + } + + protected override IEnumerable Map() { + return parser.Parallel().Select(map => { + string xblock = map.xblock.ToLower(); + if (!xBlocks.Contains(xblock)) { + return new MapDataMetadata(xblock, GetEmptyMap()); + } + + FieldAccelerationStructure mapData = ParseMapEntities(xblock, map.entities); + + ByteWriter writer = new ByteWriter(); + + writer.WriteClass(mapData); + + byte[] data = writer.ToArray(); + + lock (this) { + mapByteStats.AddValue((ulong) data.LongLength); + mapXStats.AddValue((ulong) mapData.GridSize.X); + mapYStats.AddValue((ulong) mapData.GridSize.Y); + mapZStats.AddValue((ulong) mapData.GridSize.Z); + alignedStats.AddValue((ulong) mapData.alignedEntities.Count); + unalignedStats.AddValue((ulong) mapData.unalignedEntities.Count); + } + + return new MapDataMetadata(xblock, data); + }); + } + + public void ReportStats() { + string blue = "".ColorBlue(); + Console.WriteLine($"Total maps parsed: {blue}{mapByteStats.Entries}"); + Console.WriteLine($"Total bytes: {blue}{mapByteStats.TotalValue}"); + Console.WriteLine($"Average map bytes: {blue}{mapByteStats.AvgValue}"); + Console.WriteLine($"Largest map dimensions: {blue}< {mapXStats.MaxValue}, {mapYStats.MaxValue}, {mapZStats.MaxValue} >"); + Console.WriteLine($"Average map dimensions: {blue}< {mapXStats.AvgValue}, {mapYStats.AvgValue}, {mapZStats.AvgValue} >"); + Console.WriteLine($"Largest aligned entities: {blue}{alignedStats.MaxValue}"); + Console.WriteLine($"Average aligned entities: {blue}{alignedStats.AvgValue}"); + Console.WriteLine($"Largest unaligned entities: {blue}{unalignedStats.MaxValue}"); + Console.WriteLine($"Average unaligned entities: {blue}{unalignedStats.AvgValue}"); + } +} diff --git a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs index bc3dc2db3..c3ca701dd 100644 --- a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs @@ -1,19 +1,12 @@ 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.IO; -using Maple2.File.IO.Nif; -using Maple2.File.Parser.Flat; using Maple2.File.Parser.MapXBlock; -using Maple2.Model.Common; using Maple2.Model.Enum; using Maple2.Model.Metadata; using Maple2.Tools.Extensions; -using Maple2.Tools.VectorMath; -using System.Numerics; using static M2dXmlGenerator.FeatureLocaleFilter; namespace Maple2.File.Ingest.Mapper; @@ -22,11 +15,10 @@ public class MapEntityMapper : TypeMapper { private readonly HashSet xBlocks; private readonly XBlockParser parser; - public MapEntityMapper(MetadataContext db, M2dReader exportedReader) { + public MapEntityMapper(MetadataContext db, M2dReader exportedReader, XBlockParser parser) { xBlocks = db.MapMetadata.Select(metadata => metadata.XBlock).ToHashSet(); - var index = new FlatTypeIndex(exportedReader); - // index.CliExplorer(); - parser = new XBlockParser(exportedReader, index); + + this.parser = parser; } private IEnumerable ParseMap(string xblock, IEnumerable entities) { @@ -44,134 +36,6 @@ private IEnumerable ParseMap(string xblock, IEnumerable e } } - Dictionary> gridAlignedEntities = new Dictionary>(); - List unalignedEntities = new List(); - Vector3S minIndex = new Vector3S(short.MaxValue, short.MaxValue, short.MaxValue); - Vector3S maxIndex = new Vector3S(short.MinValue, short.MinValue, short.MinValue); - - foreach (IMapEntity entity in entities) { - Vector3S nearestCubeIndex = new Vector3S(); - - if (entity is not IPlaceable placeable) { - continue; - } - - Transform transform = new Transform(); - transform.Position = placeable.Position; - transform.RotationAnglesDegrees = placeable.Rotation; - transform.Scale = placeable.Scale; - - Vector3 position = (1 / 150.0f) * (placeable.Position - new Vector3(0, 0, 75)); // offset to round to nearest - nearestCubeIndex = new Vector3S((short) Math.Floor(position.X + 0.5f), (short) Math.Floor(position.Y + 0.5f), (short) Math.Floor(position.Z + 0.5f)); - Vector3 voxelPosition = 150.0f * new Vector3(nearestCubeIndex.X, nearestCubeIndex.Y, nearestCubeIndex.Z); - BoundingBox3 entityBounds = new BoundingBox3(); - - switch (entity) { - /* - PhysXProp | WhiteboxCube - PhysXProp, MS2MapProperties, MS2Vibrate | PhysXCube, DoesMakeTok - MS2MapProperties | PhysXCube - MS2MapProperties, MS2Vibrate | None - MS2MapProperties, MS2Breakable | PhysXCube, NxCube, BothCube, OnlyNxCube - MS2Breakable | NxCube - */ - case IMS2Breakable breakable: - continue; // intentionally skip breakables. these are dynamic so should be handled at run time - case IPhysXWhitebox whitebox: - entityBounds.Min = -new Vector3(whitebox.ShapeDimensions.X, whitebox.ShapeDimensions.Y, 0.5f * whitebox.ShapeDimensions.Z); - entityBounds.Max = new Vector3(whitebox.ShapeDimensions.X, whitebox.ShapeDimensions.Y, 0.5f * whitebox.ShapeDimensions.Z); - break; - case IMesh mesh: - if (entity is IMS2Vibrate vibrate && vibrate.Enabled) { - entityBounds.Min = -new Vector3(75, 75, 0); - entityBounds.Max = new Vector3(75, 75, 150); - - break; - } - - bool isFluid = false; - - if (entity is IMS2MapProperties meshMapProperties) { - if (meshMapProperties.DisableCollision) { - continue; - } - - if (meshMapProperties.GeneratePhysX) { - Vector3 meshPhysXDimension = new Vector3(150, 150, 150); - - if (meshMapProperties.GeneratePhysXDimension != Vector3.Zero) { - meshPhysXDimension = meshMapProperties.GeneratePhysXDimension; - } - - entityBounds.Min = -new Vector3(0.5f * meshPhysXDimension.X, 0.5f * meshPhysXDimension.Y, 0); - entityBounds.Max = new Vector3(0.5f * meshPhysXDimension.X, 0.5f * meshPhysXDimension.Y, meshPhysXDimension.Z); - - break; - } - - isFluid = meshMapProperties.CubeType == "Fluid"; - } - - if (mesh.NifAsset.Length < 9 || mesh.NifAsset.Substring(0, 9).ToLower() != "urn:llid:") { - Console.WriteLine($"Non llid NifAsset: '{mesh.NifAsset}'"); - - continue; - } - - // require length of "urn:llid:XXXXXXXX" - if (mesh.NifAsset.Length < 9 + 8) { - continue; - } - - uint llid = Convert.ToUInt32(mesh.NifAsset.Substring(mesh.NifAsset.LastIndexOf(':') + 1, 8), 16); - - if (!NifParserHelper.nifBounds.TryGetValue(llid, out entityBounds)) { - Console.WriteLine($"NIF with LLID {llid:X} not found"); - - continue; - } - - break; - case IMS2MapProperties mapProperties: // GeneratePhysX - if (mapProperties.DisableCollision) { - continue; - } - - if (!mapProperties.GeneratePhysX) { - continue; - } - - Vector3 physXDimension = new Vector3(150, 150, 150); - - if (mapProperties.GeneratePhysXDimension != Vector3.Zero) { - physXDimension = mapProperties.GeneratePhysXDimension; - } - - entityBounds.Min = -new Vector3(0.5f * physXDimension.X, 0.5f * physXDimension.Y, 0); - entityBounds.Max = new Vector3(0.5f * physXDimension.X, 0.5f * physXDimension.Y, physXDimension.Z); - - break; - default: - continue; - } - - entityBounds = BoundingBox3.Transform(entityBounds, transform.Transformation); - - BoundingBox3 cellBounds = new BoundingBox3(voxelPosition - new Vector3(75, 75, 0), voxelPosition + new Vector3(75, 75, 150)); - - if (!cellBounds.Contains(entityBounds, 1e-5f)) { - // put in list for aabb tree - - continue; - } - - // grid aligned - minIndex = new Vector3S(Math.Min(minIndex.X, nearestCubeIndex.X), Math.Min(minIndex.Y, nearestCubeIndex.Y), Math.Min(minIndex.Z, nearestCubeIndex.Z)); - maxIndex = new Vector3S(Math.Max(maxIndex.X, nearestCubeIndex.X), Math.Max(maxIndex.Y, nearestCubeIndex.Y), Math.Max(maxIndex.Z, nearestCubeIndex.Z)); - } - - maxIndex += new Vector3S(0, 0, 1); // make room for potential spawn tiles - foreach (IMapEntity entity in entities) { switch (entity) { case IMS2InteractObject interactObject: diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index 17153d477..cb5220759 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; -using System.Text; using Maple2.Database.Context; using Maple2.Database.Extensions; using Maple2.Database.Model.Metadata; @@ -9,6 +8,8 @@ using Maple2.File.Ingest.Mapper; using Maple2.File.IO; using Maple2.File.IO.Nif; +using Maple2.File.Parser.Flat; +using Maple2.File.Parser.MapXBlock; using Maple2.File.Parser.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; @@ -138,7 +139,18 @@ UpdateDatabase(metadataContext, new NifMapper()); UpdateDatabase(metadataContext, new NxsMeshMapper()); -UpdateDatabase(metadataContext, new MapEntityMapper(metadataContext, exportedReader)); +var index = new FlatTypeIndex(exportedReader); + +XBlockParser parser = new XBlockParser(exportedReader, index); + +UpdateDatabase(metadataContext, new MapEntityMapper(metadataContext, exportedReader, parser)); + +MapDataMapper mapDataMapper = new MapDataMapper(metadataContext, exportedReader, parser); + +UpdateDatabase(metadataContext, mapDataMapper); + +mapDataMapper.ReportStats(); + if (runNavmesh) { _ = new NavMeshMapper(metadataContext, exportedReader); } @@ -146,6 +158,7 @@ UpdateDatabase(metadataContext, new ServerTableMapper(serverReader)); UpdateDatabase(metadataContext, new AiMapper(serverReader)); + // new MusicScoreParser(xmlReader).Parse().ToList(); // new ScriptParser(xmlReader).ParseNpc().ToList(); // new ScriptParser(xmlReader).ParseQuest().ToList(); diff --git a/Maple2.Model/Common/Vector.cs b/Maple2.Model/Common/Vector.cs index 9eab7342d..4f8f20aeb 100644 --- a/Maple2.Model/Common/Vector.cs +++ b/Maple2.Model/Common/Vector.cs @@ -40,6 +40,8 @@ public readonly record struct Vector3S(short X, short Y, short Z) { // This offset is used to correct rounding errors due to floating point arithmetic. private const float OFFSET = 0.001f; + public Vector3 Vector3 { get => new Vector3(X, Y, Z); } + public static implicit operator Vector3S(Vector3 vector) { return new Vector3S( (short) MathF.Round(vector.X), diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs new file mode 100644 index 000000000..071b5c9ef --- /dev/null +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -0,0 +1,532 @@ +using Maple2.Model.Common; +using Maple2.Model.Metadata.FieldEntities; +using Maple2.PacketLib.Tools; +using Maple2.Tools; +using Maple2.Tools.VectorMath; +using System.Numerics; + +namespace Maple2.Model.Game.Field; + +internal enum FieldEntityMembers : byte { + None = 0x0, + Id = 0x1, + Position = 0x2, + Rotation = 0x4, + Scale = 0x8, + Bounds = 0x10, + Llid = 0x20 +} + +public class FieldAccelerationStructure : IByteSerializable, IByteDeserializable { + public const int AXIS_TRIM_ENTITY_COUNT = 10; + + public Vector3S GridSize { get; private set; } = new Vector3S(); + public Vector3S MinIndex { get; private set; } = new Vector3S(); + public Vector3S MaxIndex { get; private set; } = new Vector3S(); + public List alignedEntities; + public List unalignedEntities; // TODO: add AABB tree implementation for querying unaligned objects + private int[,,] cellGrid; + + public FieldAccelerationStructure() { + alignedEntities = new(); + unalignedEntities = new(); + cellGrid = new int[0, 0, 0]; + } + + // used to guarantee deterministic output when parsing maps + private void SortEntityList(List entityList) { + if (entityList.Count <= 1) { + return; + } + + entityList.Sort((entity1, entity2) => { + int comparison = entity1.Id.High.CompareTo(entity2.Id.High); + + if (comparison == 0) { + return entity1.Id.Low.CompareTo(entity2.Id.Low); + } + + return comparison; + }); + } + + private void GenerateSpawnLocations(Dictionary> gridAlignedEntities, Vector3S minIndex, Vector3S maxIndex, List unalignedEntities) { + Dictionary occupancyMap = new(); + List<(Vector3S index, FieldSpawnTile tile)> spawnTiles = new(); + + foreach ((Vector3S coord, List entityList) in gridAlignedEntities) { + if (!occupancyMap.TryGetValue(coord, out (bool isOccupied, bool isGround) occupancy)) { + occupancy = (false, false); + } + + foreach (FieldEntity entity in entityList) { + if (entity is FieldBoxColliderEntity boxEntity && !boxEntity.IsWhiteBox) { + occupancyMap[coord] = (occupancy.isOccupied, true); + + continue; + } + + if (entity is FieldVibrateEntity) { + continue; + } + + occupancyMap[coord] = (occupancy.isOccupied, true); + } + } + + foreach (FieldEntity entity in unalignedEntities) { + Vector3 minPosition = (1 / 150.0f) * entity.Bounds.Min; + Vector3 maxPosition = (1 / 150.0f) * entity.Bounds.Max; + Vector3S minCubeIndex = new Vector3S((short) Math.Floor(minPosition.X + 0.5f), (short) Math.Floor(minPosition.Y + 0.5f), (short) Math.Floor(minPosition.Z + 0.5f)); + Vector3S maxCubeIndex = new Vector3S((short) Math.Floor(maxPosition.X + 0.5f), (short) Math.Floor(maxPosition.Y + 0.5f), (short) Math.Floor(maxPosition.Z + 0.5f)); + + for (short x = minCubeIndex.X; x <= maxCubeIndex.X; x++) { + for (short y = minCubeIndex.Y; y <= maxCubeIndex.Y; y++) { + for (short z = minCubeIndex.Z; z <= maxCubeIndex.Z; z++) { + Vector3S coord = new Vector3S(x, y, z); + + if (!occupancyMap.TryGetValue(coord, out (bool isOccupied, bool isGround) occupancy)) { + occupancy = (false, false); + } + + occupancyMap[coord] = (true, occupancy.isGround); + } + } + } + } + + for (short x = minIndex.X; x <= maxIndex.X; x++) { + for (short y = minIndex.Y; y <= maxIndex.Y; y++) { + for (short z = (short) (minIndex.Z + 1); z <= maxIndex.Z; z++) { + Vector3S coord = new Vector3S(x, y, z); + Vector3S groundCoord = new Vector3S(x, y, (short) (z - 1)); + + if (!occupancyMap.TryGetValue(coord, out (bool isOccupied, bool isGround) occupancy)) { + occupancy = (false, false); + } + + if (!occupancyMap.TryGetValue(groundCoord, out (bool isOccupied, bool isGround) groundOccupancy)) { + groundOccupancy = (false, false); + } + + if (!occupancy.isGround && !occupancy.isOccupied && groundOccupancy.isGround) { + if (!gridAlignedEntities.TryGetValue(coord, out List? entities)) { + entities = new(); + + gridAlignedEntities.Add(coord, entities); + } + + Vector3 cellPosition = 150.0f * coord.Vector3; + BoundingBox3 bounds = new BoundingBox3(cellPosition - new Vector3(75, 75, 0), cellPosition + new Vector3(75, 75, 150)); + + entities.Add(new FieldSpawnTile( + Id: new FieldEntityId(0, 0), + Position: cellPosition, + Rotation: new Vector3(0, 0, 0), + Scale: 1, + Bounds: bounds)); + } + } + } + } + } + + public void TrimGridSize(Dictionary> gridAlignedEntities, ref Vector3S minIndex, ref Vector3S maxIndex, List unalignedEntities) { + Vector3S gridSize = maxIndex - minIndex + new Vector3S(1, 1, 1); + int cellCount = gridSize.X * gridSize.Y * (gridSize.Z - 1); + int[] axisXCount = new int[gridSize.X]; + int[] axisYCount = new int[gridSize.Y]; + int[] axisZCount = new int[gridSize.Z]; + + foreach ((Vector3S coord, List entityList) in gridAlignedEntities) { + Vector3S index = coord - minIndex; + + axisXCount[index.X] += entityList.Count; + axisYCount[index.Y] += entityList.Count; + axisZCount[index.Z] += entityList.Count; + } + + int trimmedCount = 0; + short trimmedMinX = 0; + short trimmedMinY = 0; + short trimmedMinZ = 0; + short trimmedMaxX = (short) (maxIndex.X - minIndex.X); + short trimmedMaxY = (short) (maxIndex.Y - minIndex.Y); + short trimmedMaxZ = (short) (maxIndex.Z - minIndex.Z); + + for (int i = 0, cumulative = 0; i < axisXCount.Length; i++) { + int currentAxisCount = axisXCount[i]; + int remaining = gridAlignedEntities.Count - cumulative; + cumulative += currentAxisCount; + + // only cull isolated cells + if (currentAxisCount != 0) { + continue; + } + + if (cumulative < AXIS_TRIM_ENTITY_COUNT) { + trimmedMinX = (short) i; + } + + if (remaining < AXIS_TRIM_ENTITY_COUNT) { + trimmedMaxX = (short) (i - 1); + + break; + } + } + + for (int i = 0, cumulative = 0; i < axisYCount.Length; i++) { + int currentAxisCount = axisYCount[i]; + int remaining = gridAlignedEntities.Count - cumulative; + cumulative += currentAxisCount; + + // only cull isolated cells + if (currentAxisCount != 0) { + continue; + } + + if (cumulative < AXIS_TRIM_ENTITY_COUNT) { + trimmedMinY = (short) i; + } + + if (remaining < AXIS_TRIM_ENTITY_COUNT) { + trimmedMaxY = (short) (i - 1); + + break; + } + } + + for (int i = 0, cumulative = 0; i < axisZCount.Length; i++) { + int currentAxisCount = axisZCount[i]; + int remaining = gridAlignedEntities.Count - cumulative; + cumulative += currentAxisCount; + + // only cull isolated cells + if (currentAxisCount != 0) { + continue; + } + + if (cumulative < AXIS_TRIM_ENTITY_COUNT) { + trimmedMinZ = (short) i; + } + + if (remaining < AXIS_TRIM_ENTITY_COUNT) { + trimmedMaxZ = (short) (i - 1); + + break; + } + } + + Vector3S newMinIndex = new Vector3S(trimmedMinX, trimmedMinY, trimmedMinZ) + minIndex; + Vector3S newMaxIndex = new Vector3S(trimmedMaxX, trimmedMaxY, trimmedMaxZ) + minIndex; + Vector3S newGridSize = newMaxIndex - newMinIndex + new Vector3S(1, 1, 1); + int newCellCount = newGridSize.X * newGridSize.Y * newGridSize.Z; + int culledCells = cellCount - newCellCount; + + foreach ((Vector3S coord, List entityList) in gridAlignedEntities) { + Vector3S index = coord - minIndex; + + bool isTrimmed = index.X < trimmedMinX; + isTrimmed |= index.X > trimmedMaxX; + isTrimmed |= index.Y < trimmedMinY; + isTrimmed |= index.Y > trimmedMaxY; + isTrimmed |= index.Z < trimmedMinZ; + isTrimmed |= index.Z > trimmedMaxZ; + + if (isTrimmed && entityList.Count > 0) { + trimmedCount += entityList.Count; + + Vector3 cellPosition = 150.0f * coord.Vector3; + BoundingBox3 bounds = entityList.First().Bounds; + + foreach (FieldEntity entity in entityList) { + bounds = bounds.Expand(entity.Bounds); + } + + if (entityList.Count == 1) { + unalignedEntities.Add(entityList.First()); + + continue; + } + + SortEntityList(entityList); + + // limit the number of nodes in the AABB tree by bundling cells together + FieldCellEntities cell = new FieldCellEntities( + Id: new FieldEntityId(0, 0), + Position: cellPosition, + Rotation: new Vector3(0, 0, 0), + Scale: 1, + Bounds: bounds, + Entities: entityList); + + unalignedEntities.Add(cell); + } + } + } + + // cell grid contains ints that contain both list start index & entity count for the cell + // top byte is used for entity count, the 3 least significant bytes are used for list start index: CC II II II + // compare cell data with 0 to check if it is empty: 00 00 00 00 + public static (byte count, int startIndex) GetCellInfo(int cellData) { + byte count = (byte) (cellData >> 24); + int startIndex = cellData & 0xFFFFFF; + + return (count, startIndex); + } + + public static int WriteCellInfo(int count, int startIndex) { + return ((count & 0xFF) << 24) | (startIndex & 0xFFFFFF); + } + + public void AddEntities(Dictionary> gridAlignedEntities, Vector3S minIndex, Vector3S maxIndex, List unalignedEntities) { + if (minIndex.X == short.MaxValue) { + minIndex = new Vector3S(0, 0, 0); + maxIndex = new Vector3S(0, 0, 0); + } + + GenerateSpawnLocations(gridAlignedEntities, minIndex, maxIndex, unalignedEntities); + TrimGridSize(gridAlignedEntities, ref minIndex, ref maxIndex, unalignedEntities); + + GridSize = maxIndex - minIndex + new Vector3S(1, 1, 1); + MinIndex = minIndex; + MaxIndex = maxIndex; + alignedEntities.Clear(); + this.unalignedEntities = unalignedEntities; + cellGrid = new int[GridSize.X, GridSize.Y, GridSize.Z]; + + // opting to order list entries by z index first, then by y, and last by x, the same way the memory will be laid out + // this will dramatically speed up both load times and cell access times by storing them in a defragmented format from the start + // the reason why is to reduce cache misses + for (short x = minIndex.X; x <= maxIndex.X; x++) { + for (short y = minIndex.Y; y <= maxIndex.Y; y++) { + for (short z = minIndex.Z; z <= maxIndex.Z; z++) { + Vector3S coord = new Vector3S(x, y, z); + + if (!gridAlignedEntities.TryGetValue(coord, out List? entities) || entities.Count == 0) { + continue; + } + + SortEntityList(entities); + + Vector3S index = coord - minIndex; + + cellGrid[index.X, index.Y, index.Z] = WriteCellInfo(entities.Count, alignedEntities.Count); + alignedEntities.AddRange(entities); + } + } + } + + SortEntityList(unalignedEntities); + + GenerateAabbTree(); + } + + public void GenerateAabbTree() { + // TODO: generate AABB tree from unaligned objects list + } + + public void WriteTo(IByteWriter writer) { + writer.WriteShort(GridSize.X); + writer.WriteShort(GridSize.Y); + writer.WriteShort(GridSize.Z); + + writer.WriteShort(MinIndex.X); + writer.WriteShort(MinIndex.Y); + writer.WriteShort(MinIndex.Z); + + for (short x = 0; x < GridSize.X; x++) { + for (short y = 0; y < GridSize.Y; y++) { + for (short z = 0; z < GridSize.Z; z++) { + if (cellGrid[x, y, z] != 0) { + writer.WriteInt(cellGrid[x, y, z]); + + continue; + } + + int emptyCount = 0; + + while (z < GridSize.Z && cellGrid[x, y, z] == 0) { + ++emptyCount; + ++z; + } + + // use list start index as empty count for byte streams + writer.WriteInt(WriteCellInfo(0, emptyCount)); + + --z; // don't skip first occupied cell + } + } + } + + writer.WriteInt(alignedEntities.Count); + + foreach (FieldEntity entity in alignedEntities) { + WriteTo(entity, writer); + } + + writer.WriteInt(unalignedEntities.Count); + + foreach (FieldEntity entity in unalignedEntities) { + WriteTo(entity, writer); + } + } + + public void WriteTo(FieldEntity entity, IByteWriter writer) { + FieldEntityType type = entity switch { + FieldVibrateEntity => FieldEntityType.Vibrate, + FieldSpawnTile => FieldEntityType.SpawnTile, + FieldBoxColliderEntity => FieldEntityType.BoxCollider, + FieldFluidEntity => FieldEntityType.Fluid, + FieldMeshColliderEntity => FieldEntityType.MeshCollider, + FieldCellEntities => FieldEntityType.Cell, + _ => FieldEntityType.Unknown + }; + + writer.Write(type); + writer.Write(entity.Id.High); + writer.Write(entity.Id.Low); + writer.Write(entity.Position); + writer.Write(entity.Rotation); + writer.Write(entity.Scale); + writer.Write(entity.Bounds.Min); + writer.Write(entity.Bounds.Max); + + switch(entity) { + case FieldVibrateEntity vibrateEntity: + break; + case FieldSpawnTile spawnTile: + break; + case FieldBoxColliderEntity boxCollider: + writer.Write(boxCollider.Size); + writer.Write(boxCollider.IsWhiteBox); + break; + case FieldMeshColliderEntity meshCollider: + writer.Write(meshCollider.MeshLlid); + break; + case FieldCellEntities cell: + writer.WriteInt(cell.Entities.Count); + foreach(FieldEntity childEntity in cell.Entities) { + WriteTo(childEntity, writer); + } + break; + default: + throw new InvalidDataException($"Writing unhandled field entity type: {entity.GetType().FullName}"); + } + } + + public void ReadFrom(IByteReader reader) { + GridSize = reader.Read(); + MinIndex = reader.Read(); + MaxIndex = MinIndex + GridSize - new Vector3S(1, 1, 1); + cellGrid = new int[GridSize.X, GridSize.Y, GridSize.Z]; + + if (alignedEntities is null || unalignedEntities is null) { + alignedEntities = new(); + unalignedEntities = new(); + } + + alignedEntities.Clear(); + unalignedEntities.Clear(); + + for (short x = 0; x < GridSize.X; x++) { + for (short y = 0; y < GridSize.Y; y++) { + for (short z = 0; z < GridSize.Z; z++) { + int cellData = reader.ReadInt(); + (byte count, int startIndex) cell = GetCellInfo(cellData); + + if (cell.count == 0) { + // use list start index as empty count for byte streams + z += (short)(cell.startIndex - 1); + + continue; + } + + cellGrid[x, y, z] = cellData; + } + } + } + + int alignedEntityCount = reader.ReadInt(); + + for (int i = 0; i < alignedEntityCount; ++i) { + alignedEntities.Add(ReadEntity(reader)); + } + + int unalignedEntityCount = reader.ReadInt(); + + for (int i = 0; i < unalignedEntityCount; ++i) { + unalignedEntities.Add(ReadEntity(reader)); + } + + GenerateAabbTree(); + } + + public FieldEntity ReadEntity(IByteReader reader) { + FieldEntityType type = reader.Read(); + FieldEntityId id = new FieldEntityId(reader.Read(), reader.Read()); + Vector3 position = reader.Read(); + Vector3 rotation = reader.Read(); + float scale = reader.ReadFloat(); + BoundingBox3 bounds = new BoundingBox3( + min: reader.Read(), + max: reader.Read()); + + switch (type) { + case FieldEntityType.Vibrate: + return new FieldVibrateEntity( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds); + case FieldEntityType.SpawnTile: + return new FieldSpawnTile( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds); + case FieldEntityType.BoxCollider: + return new FieldBoxColliderEntity( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds, + Size: reader.Read(), + IsWhiteBox: reader.Read()); + case FieldEntityType.MeshCollider: + return new FieldMeshColliderEntity( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds, + MeshLlid: reader.Read()); + case FieldEntityType.Fluid: + return new FieldFluidEntity( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds, + MeshLlid: reader.Read()); + case FieldEntityType.Cell: + int childCount = reader.ReadInt(); + List children = new List(); + for (int i = 0; i < childCount; ++i) { + children.Add(ReadEntity(reader)); + } + return new FieldCellEntities( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds, + Entities: children); + default: + throw new InvalidDataException($"Reading unhandled field entity type: {type}"); + } + } +} diff --git a/Maple2.Model/Metadata/MapDataMetadata.cs b/Maple2.Model/Metadata/MapDataMetadata.cs new file mode 100644 index 000000000..f344c39f9 --- /dev/null +++ b/Maple2.Model/Metadata/MapDataMetadata.cs @@ -0,0 +1,5 @@ +namespace Maple2.Model.Metadata; + +public record MapDataMetadata( + string XBlock, + byte[] Data); diff --git a/Maple2.Model/Metadata/MapEntity/FieldEntity.cs b/Maple2.Model/Metadata/MapEntity/FieldEntity.cs new file mode 100644 index 000000000..48a36e72e --- /dev/null +++ b/Maple2.Model/Metadata/MapEntity/FieldEntity.cs @@ -0,0 +1,74 @@ +using Maple2.Tools.VectorMath; +using System.Numerics; + +namespace Maple2.Model.Metadata.FieldEntities; + +public enum FieldEntityType : byte { + Unknown, + Vibrate, + SpawnTile, // for mob spawns + BoxCollider, // for cube tiles (IsWhiteBox = false) & arbitrarily sized white boxes + MeshCollider, + Fluid, + Cell // for cells culled from the grid & demoted to AABB tree +} + +public record FieldEntityId( + ulong High, + ulong Low) { + public bool IsNull { get => High == 0 && Low == 0; } +} + +public record FieldEntity( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds); + +public record FieldVibrateEntity( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + +public record FieldSpawnTile( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + +public record FieldBoxColliderEntity( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds, + Vector3 Size, + bool IsWhiteBox) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + +public record FieldMeshColliderEntity( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds, + uint MeshLlid) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + +public record FieldFluidEntity( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds, + uint MeshLlid) : FieldMeshColliderEntity(Id, Position, Rotation, Scale, Bounds, MeshLlid); + +public record FieldCellEntities( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds, + List Entities) : FieldEntity(Id, Position, Rotation, Scale, Bounds); diff --git a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs index b82408b62..8edd20cf7 100644 --- a/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs +++ b/Maple2.Server.DebugGame/Graphics/DebugGraphicsContext.cs @@ -53,10 +53,8 @@ public DebugFieldRenderer[] FieldRenderers { return renderers; } } - private DebugFieldRenderer? selectedRenderer = null; public IReadOnlyList FieldWindows { get => fieldWindows; } private List fieldWindows = new(); - private DebugFieldWindow? selectedWindow = null; private HashSet updatedFields = new(); private int deltaIndex = 0; private List deltaTimes = new(); diff --git a/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs b/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs index ad3fec705..62ee44eb6 100644 --- a/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs +++ b/Maple2.Server.DebugGame/Graphics/Ui/FieldPropertiesWindow.cs @@ -1,12 +1,12 @@ using ImGuiNET; -using Maple2.Server.DebugGame.Graphics.UI; +using Maple2.Server.DebugGame.Graphics.Ui.Windows; using Maple2.Server.Game.Model; namespace Maple2.Server.DebugGame.Graphics.Ui; public class FieldPropertiesWindow : IUiWindow { public bool AllowMainWindow { get => false; } - public bool AllowFieldWindow { get => true; } + public bool AllowFieldWindow { get => false; } public bool Enabled { get; set; } = true; public string TypeName { get => "Field Properties"; } public DebugGraphicsContext? Context { get; set; } diff --git a/Maple2.Tools/Extensions/StringExtension.cs b/Maple2.Tools/Extensions/StringExtension.cs index 2c2527258..bdf47074c 100644 --- a/Maple2.Tools/Extensions/StringExtension.cs +++ b/Maple2.Tools/Extensions/StringExtension.cs @@ -3,9 +3,17 @@ namespace Maple2.Tools.Extensions; public static class StringExtension { + public static string ColorBlue(this string input) { + return input.Pastel("#00d7ff"); + } + public static string ColorGreen(this string input) { return input.Pastel("#aced66"); } + + public static string ColorPurple(this string input) { + return input.Pastel("#ff00d7"); + } public static string ColorRed(this string input) { return input.Pastel("#E05561"); diff --git a/Maple2.Tools/VectorMath/Aabb.cs b/Maple2.Tools/VectorMath/Aabb.cs deleted file mode 100644 index d9c616dee..000000000 --- a/Maple2.Tools/VectorMath/Aabb.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Maple2.Tools.VectorMath; - -public class Aabb { -} - From dc9a51c0290e2170225ec16cff58693c07cf8dcf Mon Sep 17 00:00:00 2001 From: mettaursp Date: Sat, 28 Sep 2024 03:13:39 -0700 Subject: [PATCH 05/11] field acceleration structure changes --- .../Storage/Metadata/MapDataStorage.cs | 10 +- Maple2.File.Ingest/Helpers/NifParserHelper.cs | 12 ++ Maple2.File.Ingest/Mapper/MapDataMapper.cs | 35 +++- .../Game/Field/FieldAccelerationStructure.cs | 194 ++++++++++++++++-- 4 files changed, 223 insertions(+), 28 deletions(-) diff --git a/Maple2.Database/Storage/Metadata/MapDataStorage.cs b/Maple2.Database/Storage/Metadata/MapDataStorage.cs index bcde05821..0bf2da121 100644 --- a/Maple2.Database/Storage/Metadata/MapDataStorage.cs +++ b/Maple2.Database/Storage/Metadata/MapDataStorage.cs @@ -4,6 +4,7 @@ using Maple2.PacketLib.Tools; using Maple2.Tools.Extensions; using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; namespace Maple2.Database.Storage.Metadata; @@ -27,7 +28,14 @@ public bool TryGet(string xblock, [NotNullWhen(true)] out FieldAccelerationStruc return false; } - ByteReader reader = new ByteReader(data.Data); + MemoryStream input = new MemoryStream(data.Data); + MemoryStream output = new MemoryStream(); + + using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress)) { + dstream.CopyTo(output); + } + + ByteReader reader = new ByteReader(output.ToArray()); mapData = reader.ReadClass(); diff --git a/Maple2.File.Ingest/Helpers/NifParserHelper.cs b/Maple2.File.Ingest/Helpers/NifParserHelper.cs index fe2d4d634..693a8abd9 100644 --- a/Maple2.File.Ingest/Helpers/NifParserHelper.cs +++ b/Maple2.File.Ingest/Helpers/NifParserHelper.cs @@ -34,6 +34,18 @@ private static void ParseNifDocument(uint llid, NifDocument document) { } catch (InvalidOperationException ex) { if (ex.InnerException is NifVersionNotSupportedException) { #if DEBUG + if (ex.InnerException.Message.StartsWith("[/library/triggerslibrary/gamebryodata/generic")) { + return; + } + + if (ex.InnerException.Message.StartsWith("[/model/tool/shadersphere.nif]:")) { + return; + } + + if (ex.InnerException.Message.StartsWith("[/model/tool/triggerproxy_")) { + return; + } + Console.WriteLine(ex.InnerException.Message); #endif return; diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 2fb3cb66a..592c65c3e 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -14,6 +14,7 @@ using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; using Pastel; +using System.IO.Compression; using System.Linq; using System.Numerics; @@ -24,6 +25,8 @@ public class MapDataMapper : TypeMapper { private readonly XBlockParser parser; private readonly StatsTracker mapByteStats = new(); + private readonly StatsTracker mapGridByteStats = new(); + private readonly StatsTracker mapGridBytePercentStats = new(); private readonly StatsTracker mapXStats = new(); private readonly StatsTracker mapYStats = new(); private readonly StatsTracker mapZStats = new(); @@ -305,9 +308,18 @@ protected override IEnumerable Map() { writer.WriteClass(mapData); byte[] data = writer.ToArray(); + MemoryStream dataStream = new MemoryStream(); + + using (DeflateStream dstream = new DeflateStream(dataStream, CompressionLevel.SmallestSize)) { + dstream.Write(data, 0, data.Length); + } + + data = dataStream.ToArray(); lock (this) { mapByteStats.AddValue((ulong) data.LongLength); + mapGridByteStats.AddValue(mapData.GridBytesWritten); + mapGridBytePercentStats.AddValue((ulong) (10000 * (float) mapData.GridBytesWritten / data.LongLength)); mapXStats.AddValue((ulong) mapData.GridSize.X); mapYStats.AddValue((ulong) mapData.GridSize.Y); mapZStats.AddValue((ulong) mapData.GridSize.Z); @@ -320,15 +332,18 @@ protected override IEnumerable Map() { } public void ReportStats() { - string blue = "".ColorBlue(); - Console.WriteLine($"Total maps parsed: {blue}{mapByteStats.Entries}"); - Console.WriteLine($"Total bytes: {blue}{mapByteStats.TotalValue}"); - Console.WriteLine($"Average map bytes: {blue}{mapByteStats.AvgValue}"); - Console.WriteLine($"Largest map dimensions: {blue}< {mapXStats.MaxValue}, {mapYStats.MaxValue}, {mapZStats.MaxValue} >"); - Console.WriteLine($"Average map dimensions: {blue}< {mapXStats.AvgValue}, {mapYStats.AvgValue}, {mapZStats.AvgValue} >"); - Console.WriteLine($"Largest aligned entities: {blue}{alignedStats.MaxValue}"); - Console.WriteLine($"Average aligned entities: {blue}{alignedStats.AvgValue}"); - Console.WriteLine($"Largest unaligned entities: {blue}{unalignedStats.MaxValue}"); - Console.WriteLine($"Average unaligned entities: {blue}{unalignedStats.AvgValue}"); + string blue = "\u001b[38;2;0;215;255m"; + string white = "\u001b[0m"; + + Console.WriteLine($"Total maps parsed:{blue} {mapByteStats.Entries} {white}"); + Console.WriteLine($"Total bytes:{blue} {mapByteStats.TotalValue} {white}"); + Console.WriteLine($"Average map bytes:{blue} {mapByteStats.AvgValue} {white}"); + Console.WriteLine($"Largest map bytes:{blue} {mapByteStats.MaxValue} {white}"); + Console.WriteLine($"Largest map dimensions:{blue} < {mapXStats.MaxValue}, {mapYStats.MaxValue}, {mapZStats.MaxValue} > {white}"); + Console.WriteLine($"Average map dimensions:{blue} < {mapXStats.AvgValue}, {mapYStats.AvgValue}, {mapZStats.AvgValue} > {white}"); + Console.WriteLine($"Largest aligned entities:{blue} {alignedStats.MaxValue} {white}"); + Console.WriteLine($"Average aligned entities:{blue} {alignedStats.AvgValue} {white}"); + Console.WriteLine($"Largest unaligned entities:{blue} {unalignedStats.MaxValue} {white}"); + Console.WriteLine($"Average unaligned entities:{blue} {unalignedStats.AvgValue} {white}"); } } diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs index 071b5c9ef..649da1842 100644 --- a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -2,7 +2,9 @@ using Maple2.Model.Metadata.FieldEntities; using Maple2.PacketLib.Tools; using Maple2.Tools; +using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; +using System.Collections.ObjectModel; using System.Numerics; namespace Maple2.Model.Game.Field; @@ -17,22 +19,71 @@ internal enum FieldEntityMembers : byte { Llid = 0x20 } +/* + * FieldAccelerationStructure is a helper class that specializes in various spatial queries for objects. + * It's designed to maximize performance for finding objects in a 3D space with desired constraints. + * + * Internally it uses specialized storage techniques with fast look up times, but knowledge on these + * techniques isn't necessary for use. The Query functions allow you to find the objects you need without + * knowing any of the implementation details. + * + * You can do general queries or queries for specific entity types for all types of queries available. + * Every entity matching the query constraints will be reported back through a callback, or in a list with + * Query___List() methods. + * + * Available query types include: + * - Cell: Captures all entities in a specific grid cell, or range of cells. Doesn't capture freely floating entities. + * - Point: Captures all entities intersecting with a specific point. + * - CellAtPoint: Captures all entities in the grid cell intersecting with a specific point. Doesn't capture freely floating entities. + * - BoundingBox: Captures all entities intersecting with a bounding box. + * - CellsInBoundingBox: Captures all entities in grid cells intersecting with a bounding box. Doesn't capture freely floating entities. + * - Sphere: Captures all entities intersecting with a sphere. + * - CellsInSphere: Captures all entities in grid cells intersecting with a sphere. Doesn't capture freely floating entities. + * - Ray: Captures all entities intersecting with a ray. Object results may not be in order. + * - CellsOnRay: Captures all entities intersecting with a ray. Object results may not be in order. Doesn't capture freely floating entities. + * - RayCast: Captures all boxes & meshes intersecting with a ray in order until false is returned by the callback. + * - CellRayCast: Captures all boxes & meshes intersecting with a ray in order until -1 is returned by the callback. Doesn't capture freely floating entities. + * + * Special purpose queries: + * - Spawns: Captures all mob spawn candidates in a sphere. + * - Fluids: Captures all fluids within a bounding box. + * - VibrateObjects: Captures all vibrate objects within a bounding box. +*/ public class FieldAccelerationStructure : IByteSerializable, IByteDeserializable { public const int AXIS_TRIM_ENTITY_COUNT = 10; public Vector3S GridSize { get; private set; } = new Vector3S(); public Vector3S MinIndex { get; private set; } = new Vector3S(); public Vector3S MaxIndex { get; private set; } = new Vector3S(); + + public ReadOnlyCollection AlignedEntities { get => alignedEntities.AsReadOnly(); } + public ReadOnlyCollection UnalignedEntities { get => unalignedEntities.AsReadOnly(); } + public List alignedEntities; public List unalignedEntities; // TODO: add AABB tree implementation for querying unaligned objects private int[,,] cellGrid; + public ulong GridBytesWritten { get; private set; } = 0; + public FieldAccelerationStructure() { alignedEntities = new(); unalignedEntities = new(); cellGrid = new int[0, 0, 0]; } + #region QueryApi + + + public void QueryCell(Vector3 point, Action callback) { + + } + + public void QueryCells + + #endregion + + #region Initialization + // used to guarantee deterministic output when parsing maps private void SortEntityList(List entityList) { if (entityList.Count <= 1) { @@ -334,7 +385,7 @@ public void WriteTo(IByteWriter writer) { writer.WriteShort(MinIndex.X); writer.WriteShort(MinIndex.Y); writer.WriteShort(MinIndex.Z); - + for (short x = 0; x < GridSize.X; x++) { for (short y = 0; y < GridSize.Y; y++) { for (short z = 0; z < GridSize.Z; z++) { @@ -359,6 +410,10 @@ public void WriteTo(IByteWriter writer) { } } + if (writer is ByteWriter byteWriter) { + GridBytesWritten = (ulong)byteWriter.Length; + } + writer.WriteInt(alignedEntities.Count); foreach (FieldEntity entity in alignedEntities) { @@ -372,6 +427,37 @@ public void WriteTo(IByteWriter writer) { } } +#endregion + + #region Serialization + + private Vector3S GetWorldGridIndex(Vector3 position) { + int x = (int) Math.Round(position.X) / 150; + int y = (int) Math.Round(position.Y) / 150; + int z = (int) Math.Round(position.Z) / 150; + + return new Vector3S((short)x, (short)y, (short)z); + } + + private bool IsGridAligned(Vector3 position) { + int x = (int) Math.Round(position.X) / 150; + int y = (int) Math.Round(position.Y) / 150; + int z = (int) Math.Round(position.Z) / 150; + + return position.IsNearlyEqual(150 * new Vector3(x, y, z), 0.1f); + } + + private bool IsCellBounds(Vector3 position, BoundingBox3 bounds) { + if (!IsGridAligned(position)) { + return false; + } + + bool isMinOnCell = bounds.Min.IsNearlyEqual(position - new Vector3(75, 75, 0), 0.1f); + bool isMaxOnCell = bounds.Max.IsNearlyEqual(position + new Vector3(75, 75, 150), 0.1f); + + return isMinOnCell && isMaxOnCell; + } + public void WriteTo(FieldEntity entity, IByteWriter writer) { FieldEntityType type = entity switch { FieldVibrateEntity => FieldEntityType.Vibrate, @@ -383,14 +469,48 @@ public void WriteTo(FieldEntity entity, IByteWriter writer) { _ => FieldEntityType.Unknown }; + FieldEntityMembers memberFlags = FieldEntityMembers.None; + + memberFlags |= (entity.Id.High == 0 && entity.Id.Low == 0) ? 0 : FieldEntityMembers.Id; + memberFlags |= IsGridAligned(entity.Position) ? 0 : FieldEntityMembers.Position; + memberFlags |= entity.Rotation.IsNearlyEqual(new Vector3(0, 0, 0), 1e-3f) ? 0 : FieldEntityMembers.Rotation; + memberFlags |= entity.Scale.IsNearlyEqual(1, 1e-3f) ? 0 : FieldEntityMembers.Scale; + memberFlags |= IsCellBounds(entity.Position, entity.Bounds) ? 0 : FieldEntityMembers.Bounds; + + switch (entity) { + case FieldMeshColliderEntity meshCollider: + memberFlags |= (meshCollider.MeshLlid == 0) ? 0 : FieldEntityMembers.Llid; + break; + default: + break; + } + writer.Write(type); - writer.Write(entity.Id.High); - writer.Write(entity.Id.Low); - writer.Write(entity.Position); - writer.Write(entity.Rotation); - writer.Write(entity.Scale); - writer.Write(entity.Bounds.Min); - writer.Write(entity.Bounds.Max); + writer.Write(memberFlags); + + if ((memberFlags & FieldEntityMembers.Id) != 0) { + writer.Write(entity.Id.High); + writer.Write(entity.Id.Low); + } + + if ((memberFlags & FieldEntityMembers.Position) != 0) { + writer.Write(entity.Position); + } else { + writer.Write(GetWorldGridIndex(entity.Position)); + } + + if ((memberFlags & FieldEntityMembers.Rotation) != 0) { + writer.Write(entity.Rotation); + } + + if ((memberFlags & FieldEntityMembers.Scale) != 0) { + writer.Write(entity.Scale); + } + + if ((memberFlags & FieldEntityMembers.Bounds) != 0) { + writer.Write(entity.Bounds.Min); + writer.Write(entity.Bounds.Max); + } switch(entity) { case FieldVibrateEntity vibrateEntity: @@ -464,13 +584,42 @@ public void ReadFrom(IByteReader reader) { public FieldEntity ReadEntity(IByteReader reader) { FieldEntityType type = reader.Read(); - FieldEntityId id = new FieldEntityId(reader.Read(), reader.Read()); - Vector3 position = reader.Read(); - Vector3 rotation = reader.Read(); - float scale = reader.ReadFloat(); - BoundingBox3 bounds = new BoundingBox3( - min: reader.Read(), - max: reader.Read()); + FieldEntityMembers memberFlags = reader.Read(); + + FieldEntityId id = new FieldEntityId(0, 0); + Vector3 position; + Vector3 rotation = new Vector3(0, 0, 0); + float scale = 1; + BoundingBox3 bounds; + uint llid = 0; + + if ((memberFlags & FieldEntityMembers.Id) != 0) { + id = new FieldEntityId(reader.Read(), reader.Read()); + } + + if ((memberFlags & FieldEntityMembers.Id) != 0) { + position = reader.Read(); + } else { + position = 150 * reader.Read().Vector3; + } + + if ((memberFlags & FieldEntityMembers.Rotation) != 0) { + rotation = reader.Read(); + } + + if ((memberFlags & FieldEntityMembers.Rotation) != 0) { + scale = reader.ReadFloat(); + } + + if ((memberFlags & FieldEntityMembers.Bounds) != 0) { + bounds = new BoundingBox3( + min: reader.Read(), + max: reader.Read()); + } else { + bounds = new BoundingBox3( + min: position - new Vector3(75, 75, 0), + max: position + new Vector3(75, 75, 150)); + } switch (type) { case FieldEntityType.Vibrate: @@ -497,21 +646,29 @@ public FieldEntity ReadEntity(IByteReader reader) { Size: reader.Read(), IsWhiteBox: reader.Read()); case FieldEntityType.MeshCollider: + if ((memberFlags & FieldEntityMembers.Llid) != 0) { + llid = reader.Read(); + } + return new FieldMeshColliderEntity( Id: id, Position: position, Rotation: rotation, Scale: scale, Bounds: bounds, - MeshLlid: reader.Read()); + MeshLlid: llid); case FieldEntityType.Fluid: + if ((memberFlags & FieldEntityMembers.Llid) != 0) { + llid = reader.Read(); + } + return new FieldFluidEntity( Id: id, Position: position, Rotation: rotation, Scale: scale, Bounds: bounds, - MeshLlid: reader.Read()); + MeshLlid: llid); case FieldEntityType.Cell: int childCount = reader.ReadInt(); List children = new List(); @@ -529,4 +686,7 @@ public FieldEntity ReadEntity(IByteReader reader) { throw new InvalidDataException($"Reading unhandled field entity type: {type}"); } } + + #endregion + } From 2231eaf0f63bcb1c683662dc4c2c9324d5f1da9c Mon Sep 17 00:00:00 2001 From: mettaursp Date: Sun, 29 Sep 2024 20:03:37 -0700 Subject: [PATCH 06/11] tested & fixed queries --- .../Storage/Metadata/MapDataStorage.cs | 2 +- Maple2.File.Ingest/Helpers/NifParserHelper.cs | 3 - Maple2.File.Ingest/Mapper/MapDataMapper.cs | 41 ++- Maple2.File.Ingest/Program.cs | 12 +- .../Game/Field/FieldAccelerationStructure.cs | 319 ++++++++++++++++-- .../Metadata/MapEntity/FieldEntity.cs | 17 +- Maple2.Server.Core/Modules/DataDbModule.cs | 2 + Maple2.Server.Game/Commands/DebugCommand.cs | 160 ++++++++- .../Manager/Field/FieldManager.Factory.cs | 2 + .../Manager/Field/FieldManager.cs | 2 + .../Model/Field/Entity/FieldMobSpawn.cs | 88 ++++- Maple2.Tools/VectorMath/BoundingBox3.cs | 44 +++ Maple2.Tools/VectorMath/Transform.cs | 6 +- 13 files changed, 631 insertions(+), 67 deletions(-) diff --git a/Maple2.Database/Storage/Metadata/MapDataStorage.cs b/Maple2.Database/Storage/Metadata/MapDataStorage.cs index 0bf2da121..dccec2cb3 100644 --- a/Maple2.Database/Storage/Metadata/MapDataStorage.cs +++ b/Maple2.Database/Storage/Metadata/MapDataStorage.cs @@ -8,7 +8,7 @@ namespace Maple2.Database.Storage.Metadata; -public class RideMetadataStorage(MetadataContext context) : MetadataStorage(context, CACHE_SIZE) { +public class MapDataStorage(MetadataContext context) : MetadataStorage(context, CACHE_SIZE) { private const int CACHE_SIZE = 500; // ~500 total items public bool TryGet(string xblock, [NotNullWhen(true)] out FieldAccelerationStructure? mapData) { diff --git a/Maple2.File.Ingest/Helpers/NifParserHelper.cs b/Maple2.File.Ingest/Helpers/NifParserHelper.cs index 693a8abd9..cd3d37d67 100644 --- a/Maple2.File.Ingest/Helpers/NifParserHelper.cs +++ b/Maple2.File.Ingest/Helpers/NifParserHelper.cs @@ -65,9 +65,6 @@ private static BoundingBox3 GenerateNxsMeshMetadata(NifDocument document) { int value = nxsMeshes.Count + 1; // 1-based index nxsMeshIndexMap[meshDataString] = value; - Vector3 min = new Vector3(); - Vector3 max = new Vector3(); - PhysXMesh mesh = new PhysXMesh(meshDesc.MeshData); nxsMeshes.Add(new NxsMeshMetadata(value, meshDesc.MeshData, BoundingBox3.Compute(mesh.Vertices))); diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 592c65c3e..95514c6e7 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -31,6 +31,7 @@ public class MapDataMapper : TypeMapper { private readonly StatsTracker mapYStats = new(); private readonly StatsTracker mapZStats = new(); private readonly StatsTracker alignedStats = new(); + private readonly StatsTracker alignedTrimmedStats = new(); private readonly StatsTracker unalignedStats = new(); private readonly HashSet invalidLlids = new(); private readonly HashSet missingLlids = new(); @@ -69,6 +70,9 @@ private FieldAccelerationStructure ParseMapEntities(string xblock, IEnumerable unalignedEntities = new List(); Vector3S minIndex = new Vector3S(short.MaxValue, short.MaxValue, short.MaxValue); Vector3S maxIndex = new Vector3S(short.MinValue, short.MinValue, short.MinValue); + Transform transform = new Transform(); + + int vibrateObjectId = 0; foreach (IMapEntity entity in entities) { Vector3S nearestCubeIndex = new Vector3S(); @@ -77,7 +81,7 @@ private FieldAccelerationStructure ParseMapEntities(string xblock, IEnumerable Map() { mapYStats.AddValue((ulong) mapData.GridSize.Y); mapZStats.AddValue((ulong) mapData.GridSize.Z); alignedStats.AddValue((ulong) mapData.alignedEntities.Count); + alignedTrimmedStats.AddValue((ulong) mapData.alignedEntities.Count); unalignedStats.AddValue((ulong) mapData.unalignedEntities.Count); } @@ -343,6 +350,8 @@ public void ReportStats() { Console.WriteLine($"Average map dimensions:{blue} < {mapXStats.AvgValue}, {mapYStats.AvgValue}, {mapZStats.AvgValue} > {white}"); Console.WriteLine($"Largest aligned entities:{blue} {alignedStats.MaxValue} {white}"); Console.WriteLine($"Average aligned entities:{blue} {alignedStats.AvgValue} {white}"); + Console.WriteLine($"Largest trimmed aligned entities:{blue} {alignedTrimmedStats.MaxValue} {white}"); + Console.WriteLine($"Average trimmed aligned entities:{blue} {alignedTrimmedStats.AvgValue} {white}"); Console.WriteLine($"Largest unaligned entities:{blue} {unalignedStats.MaxValue} {white}"); Console.WriteLine($"Average unaligned entities:{blue} {unalignedStats.AvgValue} {white}"); } diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index cb5220759..7c2a5bbf5 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -118,11 +118,12 @@ new("/model/character/", Path.Combine(ms2Root, "Resource/Model/Character.m2d")), new("/model/textures/", Path.Combine(ms2Root, "Resource/Model/Textures.m2d")), }; -//UpdateDatabase(metadataContext, new AdditionalEffectMapper(xmlReader)); -//UpdateDatabase(metadataContext, new AnimationMapper(xmlReader)); -//UpdateDatabase(metadataContext, new ItemMapper(xmlReader)); -//UpdateDatabase(metadataContext, new NpcMapper(xmlReader)); -//UpdateDatabase(metadataContext, new PetMapper(xmlReader)); + +UpdateDatabase(metadataContext, new AdditionalEffectMapper(xmlReader)); +UpdateDatabase(metadataContext, new AnimationMapper(xmlReader)); +UpdateDatabase(metadataContext, new ItemMapper(xmlReader)); +UpdateDatabase(metadataContext, new NpcMapper(xmlReader)); +UpdateDatabase(metadataContext, new PetMapper(xmlReader)); UpdateDatabase(metadataContext, new MapMapper(xmlReader)); UpdateDatabase(metadataContext, new UgcMapMapper(xmlReader)); UpdateDatabase(metadataContext, new ExportedUgcMapMapper(xmlReader)); @@ -158,7 +159,6 @@ UpdateDatabase(metadataContext, new ServerTableMapper(serverReader)); UpdateDatabase(metadataContext, new AiMapper(serverReader)); - // new MusicScoreParser(xmlReader).Parse().ToList(); // new ScriptParser(xmlReader).ParseNpc().ToList(); // new ScriptParser(xmlReader).ParseQuest().ToList(); diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs index 649da1842..116d13270 100644 --- a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -1,11 +1,15 @@ -using Maple2.Model.Common; +using DotRecast.Detour.Dynamic.Colliders; +using Maple2.Model.Common; using Maple2.Model.Metadata.FieldEntities; using Maple2.PacketLib.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; using System.Collections.ObjectModel; +using System.Net; using System.Numerics; +using System.Runtime.InteropServices; +using static Maple2.Model.Metadata.WorldMapTable; namespace Maple2.Model.Game.Field; @@ -35,14 +39,22 @@ internal enum FieldEntityMembers : byte { * - Cell: Captures all entities in a specific grid cell, or range of cells. Doesn't capture freely floating entities. * - Point: Captures all entities intersecting with a specific point. * - CellAtPoint: Captures all entities in the grid cell intersecting with a specific point. Doesn't capture freely floating entities. - * - BoundingBox: Captures all entities intersecting with a bounding box. - * - CellsInBoundingBox: Captures all entities in grid cells intersecting with a bounding box. Doesn't capture freely floating entities. + * - TreeAtPoint: Captures all entities in the AABB tree intersecting with a specific point. Doesn't capture cells. + * - Box: Captures all entities intersecting with a bounding box. + * - CellsInBox: Captures all entities in grid cells intersecting with a bounding box. Doesn't capture freely floating entities. + * - TreeInBox: Captures all entities in the AABB tree intersecting with a bounding box. Doesn't capture cells. * - Sphere: Captures all entities intersecting with a sphere. * - CellsInSphere: Captures all entities in grid cells intersecting with a sphere. Doesn't capture freely floating entities. + * - TreeInSphere: Captures all entities in the AABB tree intersecting with a sphere. Doesn't capture cells. * - Ray: Captures all entities intersecting with a ray. Object results may not be in order. * - CellsOnRay: Captures all entities intersecting with a ray. Object results may not be in order. Doesn't capture freely floating entities. + * - TreeOnRay: Captures all entities in the AABB tree intersecting with a ray. Doesn't capture cells. * - RayCast: Captures all boxes & meshes intersecting with a ray in order until false is returned by the callback. * - CellRayCast: Captures all boxes & meshes intersecting with a ray in order until -1 is returned by the callback. Doesn't capture freely floating entities. + * - TreeRayCast: Captures all entities in the AABB tree intersecting with a ray in order until -1 is returned by the callback. Doesn't capture cells. + * - Frustum: Captures all entities overlapping with a frustum. Useful for map rendering. + * - CellsInFrustum: Captures all entities overlapping with a frustum. Useful for map rendering. Doesn't capture freely floating entities. + * - TreeInFrustum: Captures all entities overlapping with a frustum. Useful for map rendering. Doesn't capture cells. * * Special purpose queries: * - Spawns: Captures all mob spawn candidates in a sphere. @@ -56,29 +68,185 @@ public class FieldAccelerationStructure : IByteSerializable, IByteDeserializable public Vector3S MinIndex { get; private set; } = new Vector3S(); public Vector3S MaxIndex { get; private set; } = new Vector3S(); - public ReadOnlyCollection AlignedEntities { get => alignedEntities.AsReadOnly(); } - public ReadOnlyCollection UnalignedEntities { get => unalignedEntities.AsReadOnly(); } - - public List alignedEntities; - public List unalignedEntities; // TODO: add AABB tree implementation for querying unaligned objects + public ReadOnlySpan AlignedEntities { get => CollectionsMarshal.AsSpan(alignedEntities); } + public ReadOnlySpan AlignedTrimmedEntities { get => CollectionsMarshal.AsSpan(alignedTrimmedEntities); } + public ReadOnlySpan UnalignedEntities { get => CollectionsMarshal.AsSpan(unalignedEntities); } + // Make a list of vibrate objects on the field with the same size & order as this list + // Then in queries use field.VibrateObjects[vibrateEntity.VibrateIndex] to retrieve the right one + public ReadOnlySpan VibrateEntities { get => CollectionsMarshal.AsSpan(vibrateEntities); } + + private List alignedEntities; + private List alignedTrimmedEntities; + private List unalignedEntities; // TODO: add AABB tree implementation for querying unaligned objects + private List vibrateEntities; private int[,,] cellGrid; public ulong GridBytesWritten { get; private set; } = 0; public FieldAccelerationStructure() { alignedEntities = new(); + alignedTrimmedEntities = new(); unalignedEntities = new(); + vibrateEntities = new(); cellGrid = new int[0, 0, 0]; } + public static Vector3S PointToCell(Vector3 point) { + point *= (1 / 150.0f); + return new Vector3S((short) Math.Floor(point.X + 0.5f), (short) Math.Floor(point.Y + 0.5f), (short) Math.Floor(point.Z)); + } + #region QueryApi + public void QueryCells(Vector3 min, Vector3 max, Action callback) { + Vector3S minIndex = PointToCell(min) - MinIndex; + Vector3S maxIndex = PointToCell(max) - MinIndex; + + for (short x = short.Max(0, minIndex.X); x < short.Min((short)(maxIndex.X + 1), GridSize.X); ++x) { + for (short y = short.Max(0, minIndex.Y); y < short.Min((short) (maxIndex.Y + 1), GridSize.Y); ++y) { + for (short z = short.Max(0, minIndex.Z); z < short.Min((short) (maxIndex.Z + 1), GridSize.Z); ++z) { + (byte count, int startIndex) = GetCellInfo(cellGrid[x, y, z]); + + for (byte i = 0; i < count; ++i) { + callback(alignedEntities[startIndex + i]); + } + } + } + } + + // TODO: query aabb tree + foreach (FieldEntity entity in alignedTrimmedEntities) { + if (entity.Bounds.Intersects(new BoundingBox3(min, max))) { + if (entity is FieldCellEntities cell) { + foreach (FieldEntity child in cell.Entities) { + callback(child); + } + + continue; + } + + callback(entity); + } + } + } + + public void QueryTreeInBox(Vector3 min, Vector3 max, Action callback) { + // TODO: query aabb tree + foreach (FieldEntity entity in unalignedEntities) { + if (entity.Bounds.Intersects(new BoundingBox3(min, max))) { + callback(entity); + } + } + } + + public void QueryBox(Vector3 min, Vector3 max, Action callback) { + QueryCells(min, max, callback); + QueryTreeInBox(min, max, callback); + } + + public void CellsInSphere(Vector3 center, float radius, Action callback) { + QueryCells(center - new Vector3(radius, radius, radius), center + new Vector3(radius, radius, radius), (entity) => { + if (entity.Bounds.IntersectsSphere(center, radius)) { + callback(entity); + } + }); + } + + public void QuerySpawns(Vector3 center, float radius, Action callback) { + CellsInSphere(center, radius, (entity) => { + if (entity is FieldSpawnTile spawn) { + callback(spawn); + } + }); + } + + public List QuerySpawnsList(Vector3 center, float radius) { + List spawns = new(); + + QuerySpawns(center, radius, spawns.Add); + + return spawns; + } + + public void QueryFluids(BoundingBox3 box, Action callback) { + QueryFluids(box.Min, box.Max, callback); + } + + public List QueryFluidsList(BoundingBox3 box, Action callback) { + List fluids = new(); + + QueryFluids(box, fluids.Add); + + return fluids; + } + + public void QueryFluids(Vector3 min, Vector3 max, Action callback) { + QueryCells(min, max, (entity) => { + if (entity is FieldFluidEntity fluid && fluid.IsSurface && !fluid.IsShallow) { + callback(fluid); + } + }); + } + + public List QueryFluidsList(Vector3 min, Vector3 max, Action callback) { + List fluids = new(); + + QueryFluids(min, max, fluids.Add); + + return fluids; + } + + public void QueryFluidsCenter(Vector3 center, Vector3 size, Action callback) { + QueryFluids(center - 0.5f * size, center + 0.5f * size, callback); + } + + public List QueryFluidsCenterList(Vector3 center, Vector3 size, Action callback) { + List fluids = new(); - public void QueryCell(Vector3 point, Action callback) { + QueryFluidsCenter(center, size, fluids.Add); + return fluids; } - public void QueryCells + public void QueryVibrateObjects(BoundingBox3 box, Action callback) { + QueryVibrateObjects(box.Min, box.Max, callback); + } + + public List QueryVibrateObjectsList(BoundingBox3 box, Action callback) { + List vibrateObjects = new(); + + QueryVibrateObjects(box, vibrateObjects.Add); + + return vibrateObjects; + } + + public void QueryVibrateObjects(Vector3 min, Vector3 max, Action callback) { + QueryBox(min, max, (entity) => { + if (entity is FieldVibrateEntity vibrateObject) { + callback(vibrateObject); + } + }); + } + + public List QueryVibrateObjectsList(Vector3 min, Vector3 max, Action callback) { + List vibrateObjects = new(); + + QueryVibrateObjects(min, max, vibrateObjects.Add); + + return vibrateObjects; + } + + public void QueryVibrateObjectsCenter(Vector3 center, Vector3 size, Action callback) { + QueryVibrateObjects(center - 0.5f * size, center + 0.5f * size, callback); + } + + public List QueryVibrateObjectsCenterList(Vector3 center, Vector3 size, Action callback) { + List vibrateObjects = new(); + + QueryVibrateObjectsCenter(center, size, vibrateObjects.Add); + + return vibrateObjects; + } #endregion @@ -112,7 +280,7 @@ private void GenerateSpawnLocations(Dictionary> grid foreach (FieldEntity entity in entityList) { if (entity is FieldBoxColliderEntity boxEntity && !boxEntity.IsWhiteBox) { - occupancyMap[coord] = (occupancy.isOccupied, true); + occupancyMap[coord] = (true, true); continue; } @@ -121,15 +289,16 @@ private void GenerateSpawnLocations(Dictionary> grid continue; } - occupancyMap[coord] = (occupancy.isOccupied, true); + occupancyMap[coord] = (true, occupancy.isGround); + } } foreach (FieldEntity entity in unalignedEntities) { Vector3 minPosition = (1 / 150.0f) * entity.Bounds.Min; Vector3 maxPosition = (1 / 150.0f) * entity.Bounds.Max; - Vector3S minCubeIndex = new Vector3S((short) Math.Floor(minPosition.X + 0.5f), (short) Math.Floor(minPosition.Y + 0.5f), (short) Math.Floor(minPosition.Z + 0.5f)); - Vector3S maxCubeIndex = new Vector3S((short) Math.Floor(maxPosition.X + 0.5f), (short) Math.Floor(maxPosition.Y + 0.5f), (short) Math.Floor(maxPosition.Z + 0.5f)); + Vector3S minCubeIndex = PointToCell(minPosition); + Vector3S maxCubeIndex = PointToCell(maxPosition); for (short x = minCubeIndex.X; x <= maxCubeIndex.X; x++) { for (short y = minCubeIndex.Y; y <= maxCubeIndex.Y; y++) { @@ -177,6 +346,27 @@ private void GenerateSpawnLocations(Dictionary> grid Scale: 1, Bounds: bounds)); } + + bool isSurface = !occupancy.isOccupied; + bool isShallow = isSurface && groundOccupancy.isGround; + + if ((!isSurface || isShallow) && gridAlignedEntities.TryGetValue(groundCoord, out List? entityList)) { + for (int i = 0; i < entityList.Count; ++i) { + FieldEntity entity = entityList[i]; + + if (entity is FieldFluidEntity fluid) { + entityList[i] = new FieldFluidEntity( + Id: fluid.Id, + Position: fluid.Position, + Rotation: fluid.Rotation, + Scale: fluid.Scale, + Bounds: fluid.Bounds, + MeshLlid: fluid.MeshLlid, + IsShallow: isShallow, + IsSurface: isSurface); + } + } + } } } } @@ -295,7 +485,7 @@ public void TrimGridSize(Dictionary> gridAlignedEnti } if (entityList.Count == 1) { - unalignedEntities.Add(entityList.First()); + alignedTrimmedEntities.Add(entityList.First()); continue; } @@ -311,7 +501,7 @@ public void TrimGridSize(Dictionary> gridAlignedEnti Bounds: bounds, Entities: entityList); - unalignedEntities.Add(cell); + alignedTrimmedEntities.Add(cell); } } } @@ -330,12 +520,34 @@ public static int WriteCellInfo(int count, int startIndex) { return ((count & 0xFF) << 24) | (startIndex & 0xFFFFFF); } - public void AddEntities(Dictionary> gridAlignedEntities, Vector3S minIndex, Vector3S maxIndex, List unalignedEntities) { + public void AddVibrateEntities(List entities) { + foreach (FieldEntity entity in entities) { + if (entity is FieldVibrateEntity vibrate) { + vibrateEntities[vibrate.VibrateIndex] = vibrate; + } + + if (entity is FieldCellEntities cell) { + AddVibrateEntities(cell.Entities); + } + } + } + + public void AddEntities(Dictionary> gridAlignedEntities, Vector3S minIndex, Vector3S maxIndex, List unalignedEntities, int vibrateCount) { if (minIndex.X == short.MaxValue) { minIndex = new Vector3S(0, 0, 0); maxIndex = new Vector3S(0, 0, 0); } + for (int i = 0; i < vibrateCount; ++i) { + vibrateEntities.Add(new FieldVibrateEntity( + Id: new FieldEntityId(0, 0), + Position: new Vector3(0, 0, 0), + Rotation: new Vector3(0, 0, 0), + Scale: 1, + Bounds: new BoundingBox3(), + VibrateIndex: i)); + } + GenerateSpawnLocations(gridAlignedEntities, minIndex, maxIndex, unalignedEntities); TrimGridSize(gridAlignedEntities, ref minIndex, ref maxIndex, unalignedEntities); @@ -368,8 +580,13 @@ public void AddEntities(Dictionary> gridAlignedEntit } } + SortEntityList(alignedTrimmedEntities); SortEntityList(unalignedEntities); + AddVibrateEntities(alignedEntities); + AddVibrateEntities(alignedTrimmedEntities); + AddVibrateEntities(unalignedEntities); + GenerateAabbTree(); } @@ -378,13 +595,9 @@ public void GenerateAabbTree() { } public void WriteTo(IByteWriter writer) { - writer.WriteShort(GridSize.X); - writer.WriteShort(GridSize.Y); - writer.WriteShort(GridSize.Z); - - writer.WriteShort(MinIndex.X); - writer.WriteShort(MinIndex.Y); - writer.WriteShort(MinIndex.Z); + writer.Write(GridSize); + writer.Write(MinIndex); + writer.Write(vibrateEntities.Count); for (short x = 0; x < GridSize.X; x++) { for (short y = 0; y < GridSize.Y; y++) { @@ -420,6 +633,12 @@ public void WriteTo(IByteWriter writer) { WriteTo(entity, writer); } + writer.WriteInt(alignedTrimmedEntities.Count); + + foreach (FieldEntity entity in alignedTrimmedEntities) { + WriteTo(entity, writer); + } + writer.WriteInt(unalignedEntities.Count); foreach (FieldEntity entity in unalignedEntities) { @@ -514,15 +733,23 @@ public void WriteTo(FieldEntity entity, IByteWriter writer) { switch(entity) { case FieldVibrateEntity vibrateEntity: + writer.Write(vibrateEntity.VibrateIndex); break; case FieldSpawnTile spawnTile: break; case FieldBoxColliderEntity boxCollider: writer.Write(boxCollider.Size); writer.Write(boxCollider.IsWhiteBox); + writer.Write(boxCollider.IsFluid); break; case FieldMeshColliderEntity meshCollider: - writer.Write(meshCollider.MeshLlid); + if ((memberFlags & FieldEntityMembers.Llid) != 0) { + writer.Write(meshCollider.MeshLlid); + } + if (entity is FieldFluidEntity fluid) { + writer.Write(fluid.IsShallow); + writer.Write(fluid.IsSurface); + } break; case FieldCellEntities cell: writer.WriteInt(cell.Entities.Count); @@ -541,13 +768,29 @@ public void ReadFrom(IByteReader reader) { MaxIndex = MinIndex + GridSize - new Vector3S(1, 1, 1); cellGrid = new int[GridSize.X, GridSize.Y, GridSize.Z]; - if (alignedEntities is null || unalignedEntities is null) { + if (alignedEntities is null || unalignedEntities is null || alignedTrimmedEntities is null || vibrateEntities is null) { alignedEntities = new(); + alignedTrimmedEntities = new(); unalignedEntities = new(); + vibrateEntities = new(); } alignedEntities.Clear(); + alignedTrimmedEntities.Clear(); unalignedEntities.Clear(); + vibrateEntities.Clear(); + + int vibrateCount = reader.Read(); + + for (int i = 0; i < vibrateCount; ++i) { + vibrateEntities.Add(new FieldVibrateEntity( + Id: new FieldEntityId(0, 0), + Position: new Vector3(0, 0, 0), + Rotation: new Vector3(0, 0, 0), + Scale: 1, + Bounds: new BoundingBox3(), + VibrateIndex: i)); + } for (short x = 0; x < GridSize.X; x++) { for (short y = 0; y < GridSize.Y; y++) { @@ -573,6 +816,12 @@ public void ReadFrom(IByteReader reader) { alignedEntities.Add(ReadEntity(reader)); } + int alignedTrimmedEntityCount = reader.ReadInt(); + + for (int i = 0; i < alignedTrimmedEntityCount; ++i) { + alignedTrimmedEntities.Add(ReadEntity(reader)); + } + int unalignedEntityCount = reader.ReadInt(); for (int i = 0; i < unalignedEntityCount; ++i) { @@ -580,6 +829,10 @@ public void ReadFrom(IByteReader reader) { } GenerateAabbTree(); + + AddVibrateEntities(alignedEntities); + AddVibrateEntities(alignedTrimmedEntities); + AddVibrateEntities(unalignedEntities); } public FieldEntity ReadEntity(IByteReader reader) { @@ -597,7 +850,7 @@ public FieldEntity ReadEntity(IByteReader reader) { id = new FieldEntityId(reader.Read(), reader.Read()); } - if ((memberFlags & FieldEntityMembers.Id) != 0) { + if ((memberFlags & FieldEntityMembers.Position) != 0) { position = reader.Read(); } else { position = 150 * reader.Read().Vector3; @@ -607,7 +860,7 @@ public FieldEntity ReadEntity(IByteReader reader) { rotation = reader.Read(); } - if ((memberFlags & FieldEntityMembers.Rotation) != 0) { + if ((memberFlags & FieldEntityMembers.Scale) != 0) { scale = reader.ReadFloat(); } @@ -628,7 +881,8 @@ public FieldEntity ReadEntity(IByteReader reader) { Position: position, Rotation: rotation, Scale: scale, - Bounds: bounds); + Bounds: bounds, + VibrateIndex: reader.ReadInt()); case FieldEntityType.SpawnTile: return new FieldSpawnTile( Id: id, @@ -644,7 +898,8 @@ public FieldEntity ReadEntity(IByteReader reader) { Scale: scale, Bounds: bounds, Size: reader.Read(), - IsWhiteBox: reader.Read()); + IsWhiteBox: reader.Read(), + IsFluid: reader.Read()); case FieldEntityType.MeshCollider: if ((memberFlags & FieldEntityMembers.Llid) != 0) { llid = reader.Read(); @@ -668,7 +923,9 @@ public FieldEntity ReadEntity(IByteReader reader) { Rotation: rotation, Scale: scale, Bounds: bounds, - MeshLlid: llid); + MeshLlid: llid, + IsShallow: reader.Read(), + IsSurface: reader.Read()); case FieldEntityType.Cell: int childCount = reader.ReadInt(); List children = new List(); diff --git a/Maple2.Model/Metadata/MapEntity/FieldEntity.cs b/Maple2.Model/Metadata/MapEntity/FieldEntity.cs index 48a36e72e..3a21a13a3 100644 --- a/Maple2.Model/Metadata/MapEntity/FieldEntity.cs +++ b/Maple2.Model/Metadata/MapEntity/FieldEntity.cs @@ -17,6 +17,13 @@ public record FieldEntityId( ulong High, ulong Low) { public bool IsNull { get => High == 0 && Low == 0; } + + public static FieldEntityId FromString(string id) { + ulong High = Convert.ToUInt64(id.Substring(0, 16), 16); + ulong Low = Convert.ToUInt64(id.Substring(16, 16), 16); + + return new FieldEntityId(High, Low); + } } public record FieldEntity( @@ -31,7 +38,8 @@ public record FieldVibrateEntity( Vector3 Position, Vector3 Rotation, float Scale, - BoundingBox3 Bounds) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + BoundingBox3 Bounds, + int VibrateIndex) : FieldEntity(Id, Position, Rotation, Scale, Bounds); public record FieldSpawnTile( FieldEntityId Id, @@ -47,7 +55,8 @@ public record FieldBoxColliderEntity( float Scale, BoundingBox3 Bounds, Vector3 Size, - bool IsWhiteBox) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + bool IsWhiteBox, + bool IsFluid) : FieldEntity(Id, Position, Rotation, Scale, Bounds); public record FieldMeshColliderEntity( FieldEntityId Id, @@ -63,7 +72,9 @@ public record FieldFluidEntity( Vector3 Rotation, float Scale, BoundingBox3 Bounds, - uint MeshLlid) : FieldMeshColliderEntity(Id, Position, Rotation, Scale, Bounds, MeshLlid); + uint MeshLlid, + bool IsShallow, + bool IsSurface) : FieldMeshColliderEntity(Id, Position, Rotation, Scale, Bounds, MeshLlid); public record FieldCellEntities( FieldEntityId Id, diff --git a/Maple2.Server.Core/Modules/DataDbModule.cs b/Maple2.Server.Core/Modules/DataDbModule.cs index 8b5ced84c..e1912e20a 100644 --- a/Maple2.Server.Core/Modules/DataDbModule.cs +++ b/Maple2.Server.Core/Modules/DataDbModule.cs @@ -2,6 +2,7 @@ using Autofac; using Maple2.Database.Context; using Maple2.Database.Storage; +using Maple2.Database.Storage.Metadata; using Microsoft.EntityFrameworkCore; using Module = Autofac.Module; @@ -36,6 +37,7 @@ protected override void Load(ContainerBuilder builder) { builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); + builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); diff --git a/Maple2.Server.Game/Commands/DebugCommand.cs b/Maple2.Server.Game/Commands/DebugCommand.cs index f1e30697e..457b63a11 100644 --- a/Maple2.Server.Game/Commands/DebugCommand.cs +++ b/Maple2.Server.Game/Commands/DebugCommand.cs @@ -4,6 +4,12 @@ using Maple2.Database.Storage; using System.CommandLine.IO; using Maple2.Server.Game.Util; +using Maple2.Model.Metadata; +using Maple2.Database.Storage.Metadata; +using Maple2.Model.Game.Field; +using System.Numerics; +using Maple2.Model.Common; +using System; namespace Maple2.Server.Game.Commands; @@ -13,7 +19,7 @@ public class DebugCommand : Command { private readonly NpcMetadataStorage npcStorage; - public DebugCommand(GameSession session, NpcMetadataStorage npcStorage) : base(NAME, DESCRIPTION) { + public DebugCommand(GameSession session, NpcMetadataStorage npcStorage, MapDataStorage mapDataStorage) : base(NAME, DESCRIPTION) { this.npcStorage = npcStorage; AddCommand(new DebugNpcAiCommand(session, npcStorage)); @@ -21,6 +27,7 @@ public DebugCommand(GameSession session, NpcMetadataStorage npcStorage) : base(N AddCommand(new DebugSkillsCommand(session)); AddCommand(new SendRawPacketCommand(session)); AddCommand(new ResolvePacketCommand(session)); + AddCommand(new DebugQueryCommand(session, mapDataStorage)); } public class DebugNpcAiCommand : Command { @@ -137,4 +144,155 @@ private void Handle(InvocationContext ctx, string packet) { resolver.Start(session); } } + + private class DebugQueryCommand : Command + { + public DebugQueryCommand(GameSession session, MapDataStorage mapDataStorage) : base("query", "Tests entity spatial queries.") + { + AddCommand(new DebugQuerySpawnCommand(session, mapDataStorage)); + AddCommand(new DebugQueryFluidCommand(session, mapDataStorage)); + AddCommand(new DebugQueryVibrateCommand(session, mapDataStorage)); + } + + private class DebugQuerySpawnCommand : Command + { + private readonly GameSession session; + private readonly MapDataStorage mapDataStorage; + + public DebugQuerySpawnCommand(GameSession session, MapDataStorage mapDataStorage) : base("spawns", "Searches for nearby valid mob spawn points.") + { + this.session = session; + this.mapDataStorage = mapDataStorage; + + var radius = new Argument("radius", () => 300, "Sphere radius of query."); + + AddArgument(radius); + + this.SetHandler(Handle, radius); + } + + private void Handle(InvocationContext ctx, float radius) + { + if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) + { + return; + } + + if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) + { + return; + } + + Vector3 center = session.Player.Position; + Vector3S cell = FieldAccelerationStructure.PointToCell(center); + + ctx.Console.Out.WriteLine($"Player at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); + + mapData.QuerySpawns(session.Player.Position, radius, (spawn) => + { + Vector3 center = spawn.Position; + Vector3S cell = FieldAccelerationStructure.PointToCell(center); + + ctx.Console.Out.WriteLine($"Mob spawn found at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); + }); + } + } + + private class DebugQueryFluidCommand : Command + { + private readonly GameSession session; + private readonly MapDataStorage mapDataStorage; + + public DebugQueryFluidCommand(GameSession session, MapDataStorage mapDataStorage) : base("fluids", "Searches for nearby fluids.") + { + this.session = session; + this.mapDataStorage = mapDataStorage; + + var x = new Argument("x", () => 300, "How far along the x axis to search from the player."); + var y = new Argument("y", () => 300, "How far along the y axis to search from the player."); + var z = new Argument("z", () => 300, "How far along the z axis to search from the player."); + + AddArgument(x); + AddArgument(y); + AddArgument(z); + + this.SetHandler(Handle, x, y, z); + } + + private void Handle(InvocationContext ctx, float x, float y, float z) + { + if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) + { + return; + } + + if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) + { + return; + } + + Vector3 center = session.Player.Position; + Vector3S cell = FieldAccelerationStructure.PointToCell(center); + + ctx.Console.Out.WriteLine($"Player at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); + + mapData.QueryFluidsCenter(session.Player.Position, 2 * new Vector3(x, y, z), (fluid) => + { + Vector3 center = fluid.Position; + Vector3S cell = FieldAccelerationStructure.PointToCell(center); + string fluidType = !fluid.IsSurface ? "Deep fluid" : fluid.IsShallow ? "Shallow fluid" : "Fluid"; + + ctx.Console.Out.WriteLine($"{fluidType} found at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); + }); + } + } + + private class DebugQueryVibrateCommand : Command + { + private readonly GameSession session; + private readonly MapDataStorage mapDataStorage; + + public DebugQueryVibrateCommand(GameSession session, MapDataStorage mapDataStorage) : base("vibrate", "Searches for nearby vibrate objects.") + { + this.session = session; + this.mapDataStorage = mapDataStorage; + + var x = new Argument("x", () => 300, "How far along the x axis to search from the player."); + var y = new Argument("y", () => 300, "How far along the y axis to search from the player."); + var z = new Argument("z", () => 300, "How far along the z axis to search from the player."); + + AddArgument(x); + AddArgument(y); + AddArgument(z); + + this.SetHandler(Handle, x, y, z); + } + + private void Handle(InvocationContext ctx, float x, float y, float z) + { + if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) + { + return; + } + + if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) + { + return; + } + + Vector3 center = session.Player.Position; + Vector3S cell = FieldAccelerationStructure.PointToCell(center); + + ctx.Console.Out.WriteLine($"Player at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); + + mapData.QueryVibrateObjectsCenter(session.Player.Position, 2 * new Vector3(x, y, z), (vibrate) => + { + Vector3 center = vibrate.Position; + Vector3S cell = FieldAccelerationStructure.PointToCell(center); + + ctx.Console.Out.WriteLine($"Vibrate object found at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); + }); + } + } + } } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager.Factory.cs b/Maple2.Server.Game/Manager/Field/FieldManager.Factory.cs index e66313ad7..30fee8144 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager.Factory.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager.Factory.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using Autofac; using Maple2.Database.Storage; +using Maple2.Database.Storage.Metadata; using Maple2.Model.Enum; using Maple2.Model.Metadata; using Serilog; @@ -14,6 +15,7 @@ public sealed class Factory : IDisposable { // ReSharper disable MemberCanBePrivate.Global public required MapMetadataStorage MapMetadata { private get; init; } public required MapEntityStorage MapEntities { private get; init; } + public required MapDataStorage MapData { private get; init; } public required ServerTableMetadataStorage ServerTableMetadata { private get; init; } public required NpcMetadataStorage NpcMetadata { get; init; } = null!; // ReSharper restore All diff --git a/Maple2.Server.Game/Manager/Field/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager.cs index f366805d3..16d220eb4 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using Maple2.Database.Storage; +using Maple2.Database.Storage.Metadata; using Maple2.Model.Common; using Maple2.Model.Enum; using Maple2.Model.Error; @@ -35,6 +36,7 @@ public sealed partial class FieldManager : IDisposable { public GameStorage GameStorage { get; init; } = null!; public ItemMetadataStorage ItemMetadata { get; init; } = null!; public MapMetadataStorage MapMetadata { get; init; } = null!; + public MapDataStorage MapData { get; init; } = null!; public NpcMetadataStorage NpcMetadata { get; init; } = null!; public AiMetadataStorage AiMetadata { get; init; } = null!; public SkillMetadataStorage SkillMetadata { get; init; } = null!; diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs b/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs index 59429455b..9c2b94ea5 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs @@ -1,6 +1,9 @@ using System.Numerics; +using System.Reflection.Metadata.Ecma335; using Maple2.Model.Game; +using Maple2.Model.Game.Field; using Maple2.Model.Metadata; +using Maple2.Model.Metadata.FieldEntities; using Maple2.Server.Game.Manager.Field; using Maple2.Server.Game.Packets; using Maple2.Tools; @@ -20,16 +23,21 @@ public class FieldMobSpawn : FieldEntity { private readonly WeightedSet pets; private readonly List spawnedMobs; private readonly List spawnedPets; + private readonly List validSpawns; private long spawnTick; + private int spawnId; public FieldMobSpawn(FieldManager field, int objectId, MapMetadataSpawn metadata, WeightedSet npcs, WeightedSet pets) : base(field, objectId, metadata) { this.npcs = npcs; this.pets = pets; spawnedMobs = new List(metadata.Population); spawnedPets = new List(metadata.PetPopulation); + spawnId = metadata.Id; if (Value.Cooldown <= 0) { Log.Logger.Information("No respawn for mapId:{MapId} spawnId:{SpawnId}", Field.MapId, Value.Id); } + + validSpawns = new(); } public void Despawn(int objectId) { @@ -59,14 +67,88 @@ public void Despawn(int objectId) { } } + private List GetRandomSpawns(int count) { + List spawnsPicked = new(); + Vector3[] spawnsRemaining = new Vector3[validSpawns.Count]; + + validSpawns.CopyTo(spawnsRemaining); + + int selectSpawns = int.Min(count, validSpawns.Count); + int remainder = count - selectSpawns; + + for (int i = 0; i < selectSpawns; ++i) { + int picked = Random.Shared.Next(0, spawnsRemaining.Length - i); + + spawnsPicked.Add(spawnsRemaining[picked]); + spawnsRemaining[picked] = spawnsRemaining[selectSpawns - i - 1]; // remove picked from list by replacing with last in list + } + + if (remainder > 0) { + Log.Logger.Error("Ran out of valid spawns to pick for spawn {SpawnId} in map {MapId}; valid spawns: {SpawnCount}; picking: {Picking}", spawnId, Field.MapId, validSpawns.Count, count); + } + + // ran out of spawns to pick from so we are picking any duplicate now + for (int i = 0; i < remainder; ++i) { + int picked = Random.Shared.Next(0, validSpawns.Count); + + spawnsPicked.Add(validSpawns[picked]); + } + + return spawnsPicked; + } + + private void InitializeSpawns() { + if (validSpawns.Count > 0) { + return; + } + + if (!Field.MapMetadata.TryGet(Field.MapId, out MapMetadata? map)) { + Log.Logger.Error("Failed to get map xblock name for map {MapId}", Field.MapId); + + validSpawns.Add(Position); + + return; + } + + if (!Field.MapData.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) { + Log.Logger.Error("Failed to get map xblock name for map {MapId}", Field.MapId); + + validSpawns.Add(Position); + + return; + } + + foreach (FieldEntity entity in mapData.QuerySpawnsList(Position, SPAWN_DISTANCE)) { + validSpawns.Add(entity.Position); + } + + if (validSpawns.Count == 0) { + Log.Logger.Error("Failed to find spawns for spawn {SpawnId} in map {MapId}", spawnId, Field.MapId); + + validSpawns.Add(Position); + } + } + public override void Update(long tickCount) { if (tickCount < spawnTick) { return; } + InitializeSpawns(); + spawnTick = long.MaxValue; + + int spawnMobCount = Value.Population - spawnedMobs.Count; + bool doSpawnPet = Random.Shared.Next(PET_SPAWN_RATE_TOTAL) < Value.PetSpawnRate; + + if (doSpawnPet) { + ++spawnMobCount; + } + + List pickedSpawns = GetRandomSpawns(spawnMobCount); + int spawnIndex = 0; for (int i = spawnedMobs.Count; i < Value.Population; i++) { - FieldNpc? fieldNpc = Field.SpawnNpc(npcs.Get(), GetRandomSpawn(), Rotation, owner: this); + FieldNpc? fieldNpc = Field.SpawnNpc(npcs.Get(), pickedSpawns[spawnIndex++], Rotation, owner: this); if (fieldNpc == null) { continue; } @@ -81,10 +163,10 @@ public override void Update(long tickCount) { return; } - if (Random.Shared.Next(PET_SPAWN_RATE_TOTAL) < Value.PetSpawnRate) { + if (doSpawnPet) { // Any stats are computed after pet is captured since that's when rarity is determined. var pet = new Item(pets.Get()); - FieldPet? fieldPet = Field.SpawnPet(pet, GetRandomSpawn(), Rotation, owner: this); + FieldPet? fieldPet = Field.SpawnPet(pet, pickedSpawns.Last(), Rotation, owner: this); if (fieldPet == null) { return; } diff --git a/Maple2.Tools/VectorMath/BoundingBox3.cs b/Maple2.Tools/VectorMath/BoundingBox3.cs index 344aa7de6..a6fc86f90 100644 --- a/Maple2.Tools/VectorMath/BoundingBox3.cs +++ b/Maple2.Tools/VectorMath/BoundingBox3.cs @@ -65,6 +65,28 @@ public static BoundingBox3 Transform(BoundingBox3 box, Matrix4x4 matrix) { } } + return result; + } + + public static BoundingBox3 TransformAndTest(BoundingBox3 box, Matrix4x4 matrix) { + Vector3 translation = matrix.Translation; + + BoundingBox3 result = new BoundingBox3(translation); + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + float a = matrix[j, i] * box.Min[j]; + float b = matrix[j, i] * box.Max[j]; + + if (a > b) { + (a, b) = (b, a); + } + + result.Min[i] += a; + result.Max[i] += b; + } + } + Vector3 size = box.Max - box.Min; List vertices = new List() { box.Min, @@ -113,6 +135,28 @@ public bool Intersects(BoundingBox3 box, float epsilon = 0) { return compoundBox.Contains(box.Max, epsilon); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private float Square(float x) { + return x * x; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IntersectsSphere(Vector3 center, float radius, float epsilon = 0) { + // by Jim Arvo, in "Graphics Gems" + + float dmin = 0; + + for (int i = 0; i < 3; ++i) { + if (center[i] < Min[i]) { + dmin += Square(center[i] - Min[i]); + } else if (center[i] > Max[i]) { + dmin += Square(center[i] - Max[i]); + } + } + + return dmin <= Square(radius); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsNearlyEqual(BoundingBox3 box, float epsilon = 1e-5f) { return Min.IsNearlyEqual(box.Min, epsilon) && Max.IsNearlyEqual(box.Max, epsilon); diff --git a/Maple2.Tools/VectorMath/Transform.cs b/Maple2.Tools/VectorMath/Transform.cs index 8362e5b6c..21584d999 100644 --- a/Maple2.Tools/VectorMath/Transform.cs +++ b/Maple2.Tools/VectorMath/Transform.cs @@ -89,9 +89,9 @@ public float Scale { float scale = Scale; // Normalize each axis before scaling to prevent floating point drift. - RightAxis = Vector3.Normalize(RightAxis) * (value / scale); - UpAxis = Vector3.Normalize(UpAxis) * (value / scale); - FrontAxis = Vector3.Normalize(FrontAxis) * (value / scale); + RightAxis = Vector3.Normalize(RightAxis) * value; + UpAxis = Vector3.Normalize(UpAxis) * value; + FrontAxis = Vector3.Normalize(FrontAxis) * value; } } From bebdc3833aedd1b09aec59b7aefcd7b4f1ef0ff3 Mon Sep 17 00:00:00 2001 From: mettaursp Date: Sun, 29 Sep 2024 20:34:18 -0700 Subject: [PATCH 07/11] formatting --- Maple2.File.Ingest/Helpers/NifParserHelper.cs | 4 +- .../Game/Field/FieldAccelerationStructure.cs | 18 +++--- .../Graphics/Scene/Camera.cs | 4 +- Maple2.Server.Game/Commands/DebugCommand.cs | 60 +++++++------------ Maple2.Tools/Extensions/StringExtension.cs | 2 +- 5 files changed, 34 insertions(+), 54 deletions(-) diff --git a/Maple2.File.Ingest/Helpers/NifParserHelper.cs b/Maple2.File.Ingest/Helpers/NifParserHelper.cs index cd3d37d67..6870ef9bf 100644 --- a/Maple2.File.Ingest/Helpers/NifParserHelper.cs +++ b/Maple2.File.Ingest/Helpers/NifParserHelper.cs @@ -37,11 +37,11 @@ private static void ParseNifDocument(uint llid, NifDocument document) { if (ex.InnerException.Message.StartsWith("[/library/triggerslibrary/gamebryodata/generic")) { return; } - + if (ex.InnerException.Message.StartsWith("[/model/tool/shadersphere.nif]:")) { return; } - + if (ex.InnerException.Message.StartsWith("[/model/tool/triggerproxy_")) { return; } diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs index 116d13270..aad9c52e3 100644 --- a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -102,7 +102,7 @@ public void QueryCells(Vector3 min, Vector3 max, Action callback) { Vector3S minIndex = PointToCell(min) - MinIndex; Vector3S maxIndex = PointToCell(max) - MinIndex; - for (short x = short.Max(0, minIndex.X); x < short.Min((short)(maxIndex.X + 1), GridSize.X); ++x) { + for (short x = short.Max(0, minIndex.X); x < short.Min((short) (maxIndex.X + 1), GridSize.X); ++x) { for (short y = short.Max(0, minIndex.Y); y < short.Min((short) (maxIndex.Y + 1), GridSize.Y); ++y) { for (short z = short.Max(0, minIndex.Z); z < short.Min((short) (maxIndex.Z + 1), GridSize.Z); ++z) { (byte count, int startIndex) = GetCellInfo(cellGrid[x, y, z]); @@ -300,7 +300,7 @@ private void GenerateSpawnLocations(Dictionary> grid Vector3S minCubeIndex = PointToCell(minPosition); Vector3S maxCubeIndex = PointToCell(maxPosition); - for (short x = minCubeIndex.X; x <= maxCubeIndex.X; x++) { + for (short x = minCubeIndex.X; x <= maxCubeIndex.X; x++) { for (short y = minCubeIndex.Y; y <= maxCubeIndex.Y; y++) { for (short z = minCubeIndex.Z; z <= maxCubeIndex.Z; z++) { Vector3S coord = new Vector3S(x, y, z); @@ -598,7 +598,7 @@ public void WriteTo(IByteWriter writer) { writer.Write(GridSize); writer.Write(MinIndex); writer.Write(vibrateEntities.Count); - + for (short x = 0; x < GridSize.X; x++) { for (short y = 0; y < GridSize.Y; y++) { for (short z = 0; z < GridSize.Z; z++) { @@ -624,7 +624,7 @@ public void WriteTo(IByteWriter writer) { } if (writer is ByteWriter byteWriter) { - GridBytesWritten = (ulong)byteWriter.Length; + GridBytesWritten = (ulong) byteWriter.Length; } writer.WriteInt(alignedEntities.Count); @@ -646,7 +646,7 @@ public void WriteTo(IByteWriter writer) { } } -#endregion + #endregion #region Serialization @@ -655,7 +655,7 @@ private Vector3S GetWorldGridIndex(Vector3 position) { int y = (int) Math.Round(position.Y) / 150; int z = (int) Math.Round(position.Z) / 150; - return new Vector3S((short)x, (short)y, (short)z); + return new Vector3S((short) x, (short) y, (short) z); } private bool IsGridAligned(Vector3 position) { @@ -731,7 +731,7 @@ public void WriteTo(FieldEntity entity, IByteWriter writer) { writer.Write(entity.Bounds.Max); } - switch(entity) { + switch (entity) { case FieldVibrateEntity vibrateEntity: writer.Write(vibrateEntity.VibrateIndex); break; @@ -753,7 +753,7 @@ public void WriteTo(FieldEntity entity, IByteWriter writer) { break; case FieldCellEntities cell: writer.WriteInt(cell.Entities.Count); - foreach(FieldEntity childEntity in cell.Entities) { + foreach (FieldEntity childEntity in cell.Entities) { WriteTo(childEntity, writer); } break; @@ -800,7 +800,7 @@ public void ReadFrom(IByteReader reader) { if (cell.count == 0) { // use list start index as empty count for byte streams - z += (short)(cell.startIndex - 1); + z += (short) (cell.startIndex - 1); continue; } diff --git a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs index bfe387b09..ffd0d4f44 100644 --- a/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs +++ b/Maple2.Server.DebugGame/Graphics/Scene/Camera.cs @@ -9,8 +9,8 @@ public class Camera { public float AspectRatio { get; private set; } public float NearPlane { get; private set; } - public float FarPlane { get; private set; } - public float FieldOfView { get; private set; } + public float FarPlane { get; private set; } + public float FieldOfView { get; private set; } public void SetProperties(float fieldOfView, float aspectRatio, float nearPlane, float farPlane) { diff --git a/Maple2.Server.Game/Commands/DebugCommand.cs b/Maple2.Server.Game/Commands/DebugCommand.cs index 457b63a11..39b78befe 100644 --- a/Maple2.Server.Game/Commands/DebugCommand.cs +++ b/Maple2.Server.Game/Commands/DebugCommand.cs @@ -145,22 +145,18 @@ private void Handle(InvocationContext ctx, string packet) { } } - private class DebugQueryCommand : Command - { - public DebugQueryCommand(GameSession session, MapDataStorage mapDataStorage) : base("query", "Tests entity spatial queries.") - { + private class DebugQueryCommand : Command { + public DebugQueryCommand(GameSession session, MapDataStorage mapDataStorage) : base("query", "Tests entity spatial queries.") { AddCommand(new DebugQuerySpawnCommand(session, mapDataStorage)); AddCommand(new DebugQueryFluidCommand(session, mapDataStorage)); AddCommand(new DebugQueryVibrateCommand(session, mapDataStorage)); } - private class DebugQuerySpawnCommand : Command - { + private class DebugQuerySpawnCommand : Command { private readonly GameSession session; private readonly MapDataStorage mapDataStorage; - public DebugQuerySpawnCommand(GameSession session, MapDataStorage mapDataStorage) : base("spawns", "Searches for nearby valid mob spawn points.") - { + public DebugQuerySpawnCommand(GameSession session, MapDataStorage mapDataStorage) : base("spawns", "Searches for nearby valid mob spawn points.") { this.session = session; this.mapDataStorage = mapDataStorage; @@ -171,15 +167,12 @@ private class DebugQuerySpawnCommand : Command this.SetHandler(Handle, radius); } - private void Handle(InvocationContext ctx, float radius) - { - if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) - { + private void Handle(InvocationContext ctx, float radius) { + if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) { return; } - if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) - { + if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) { return; } @@ -188,8 +181,7 @@ private void Handle(InvocationContext ctx, float radius) ctx.Console.Out.WriteLine($"Player at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); - mapData.QuerySpawns(session.Player.Position, radius, (spawn) => - { + mapData.QuerySpawns(session.Player.Position, radius, (spawn) => { Vector3 center = spawn.Position; Vector3S cell = FieldAccelerationStructure.PointToCell(center); @@ -198,13 +190,11 @@ private void Handle(InvocationContext ctx, float radius) } } - private class DebugQueryFluidCommand : Command - { + private class DebugQueryFluidCommand : Command { private readonly GameSession session; private readonly MapDataStorage mapDataStorage; - public DebugQueryFluidCommand(GameSession session, MapDataStorage mapDataStorage) : base("fluids", "Searches for nearby fluids.") - { + public DebugQueryFluidCommand(GameSession session, MapDataStorage mapDataStorage) : base("fluids", "Searches for nearby fluids.") { this.session = session; this.mapDataStorage = mapDataStorage; @@ -219,15 +209,12 @@ private class DebugQueryFluidCommand : Command this.SetHandler(Handle, x, y, z); } - private void Handle(InvocationContext ctx, float x, float y, float z) - { - if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) - { + private void Handle(InvocationContext ctx, float x, float y, float z) { + if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) { return; } - if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) - { + if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) { return; } @@ -236,8 +223,7 @@ private void Handle(InvocationContext ctx, float x, float y, float z) ctx.Console.Out.WriteLine($"Player at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); - mapData.QueryFluidsCenter(session.Player.Position, 2 * new Vector3(x, y, z), (fluid) => - { + mapData.QueryFluidsCenter(session.Player.Position, 2 * new Vector3(x, y, z), (fluid) => { Vector3 center = fluid.Position; Vector3S cell = FieldAccelerationStructure.PointToCell(center); string fluidType = !fluid.IsSurface ? "Deep fluid" : fluid.IsShallow ? "Shallow fluid" : "Fluid"; @@ -247,13 +233,11 @@ private void Handle(InvocationContext ctx, float x, float y, float z) } } - private class DebugQueryVibrateCommand : Command - { + private class DebugQueryVibrateCommand : Command { private readonly GameSession session; private readonly MapDataStorage mapDataStorage; - public DebugQueryVibrateCommand(GameSession session, MapDataStorage mapDataStorage) : base("vibrate", "Searches for nearby vibrate objects.") - { + public DebugQueryVibrateCommand(GameSession session, MapDataStorage mapDataStorage) : base("vibrate", "Searches for nearby vibrate objects.") { this.session = session; this.mapDataStorage = mapDataStorage; @@ -268,15 +252,12 @@ private class DebugQueryVibrateCommand : Command this.SetHandler(Handle, x, y, z); } - private void Handle(InvocationContext ctx, float x, float y, float z) - { - if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) - { + private void Handle(InvocationContext ctx, float x, float y, float z) { + if (!session.Field.MapMetadata.TryGet(session.Field.MapId, out MapMetadata? map)) { return; } - if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) - { + if (!mapDataStorage.TryGet(map.XBlock, out FieldAccelerationStructure? mapData)) { return; } @@ -285,8 +266,7 @@ private void Handle(InvocationContext ctx, float x, float y, float z) ctx.Console.Out.WriteLine($"Player at {center.X} {center.Y} {center.Z} in cell {cell.X} {cell.Y} {cell.Z}"); - mapData.QueryVibrateObjectsCenter(session.Player.Position, 2 * new Vector3(x, y, z), (vibrate) => - { + mapData.QueryVibrateObjectsCenter(session.Player.Position, 2 * new Vector3(x, y, z), (vibrate) => { Vector3 center = vibrate.Position; Vector3S cell = FieldAccelerationStructure.PointToCell(center); diff --git a/Maple2.Tools/Extensions/StringExtension.cs b/Maple2.Tools/Extensions/StringExtension.cs index bdf47074c..188f17200 100644 --- a/Maple2.Tools/Extensions/StringExtension.cs +++ b/Maple2.Tools/Extensions/StringExtension.cs @@ -10,7 +10,7 @@ public static string ColorBlue(this string input) { public static string ColorGreen(this string input) { return input.Pastel("#aced66"); } - + public static string ColorPurple(this string input) { return input.Pastel("#ff00d7"); } From 499c3f7bffcd1a3ef76ee593b6ff16e4924daa3f Mon Sep 17 00:00:00 2001 From: mettaursp Date: Sun, 29 Sep 2024 21:04:07 -0700 Subject: [PATCH 08/11] fix compilation issue --- Maple2.File.Ingest/Mapper/MapDataMapper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 95514c6e7..215009460 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -329,9 +329,9 @@ protected override IEnumerable Map() { mapXStats.AddValue((ulong) mapData.GridSize.X); mapYStats.AddValue((ulong) mapData.GridSize.Y); mapZStats.AddValue((ulong) mapData.GridSize.Z); - alignedStats.AddValue((ulong) mapData.alignedEntities.Count); - alignedTrimmedStats.AddValue((ulong) mapData.alignedEntities.Count); - unalignedStats.AddValue((ulong) mapData.unalignedEntities.Count); + alignedStats.AddValue((ulong) mapData.AlignedEntities.Length); + alignedTrimmedStats.AddValue((ulong) mapData.AlignedTrimmedEntities.Length); + unalignedStats.AddValue((ulong) mapData.UnalignedEntities.Length); } return new MapDataMetadata(xblock, data); From 3a5b3ed4b1ac3dffd173d78493cfd02ccf060b22 Mon Sep 17 00:00:00 2001 From: mettaursp Date: Sun, 29 Sep 2024 21:55:36 -0700 Subject: [PATCH 09/11] small fixes --- Maple2.File.Ingest/Mapper/MapDataMapper.cs | 2 -- .../Game/Field/FieldAccelerationStructure.cs | 18 +++++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 215009460..7cd52f219 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -13,9 +13,7 @@ using Maple2.PacketLib.Tools; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; -using Pastel; using System.IO.Compression; -using System.Linq; using System.Numerics; namespace Maple2.File.Ingest.Mapper; diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs index aad9c52e3..bf5382778 100644 --- a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -1,15 +1,11 @@ -using DotRecast.Detour.Dynamic.Colliders; -using Maple2.Model.Common; +using Maple2.Model.Common; using Maple2.Model.Metadata.FieldEntities; using Maple2.PacketLib.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; -using System.Collections.ObjectModel; -using System.Net; using System.Numerics; using System.Runtime.InteropServices; -using static Maple2.Model.Metadata.WorldMapTable; namespace Maple2.Model.Game.Field; @@ -172,7 +168,7 @@ public void QueryFluids(BoundingBox3 box, Action callback) { QueryFluids(box.Min, box.Max, callback); } - public List QueryFluidsList(BoundingBox3 box, Action callback) { + public List QueryFluidsList(BoundingBox3 box) { List fluids = new(); QueryFluids(box, fluids.Add); @@ -188,7 +184,7 @@ public void QueryFluids(Vector3 min, Vector3 max, Action callb }); } - public List QueryFluidsList(Vector3 min, Vector3 max, Action callback) { + public List QueryFluidsList(Vector3 min, Vector3 max) { List fluids = new(); QueryFluids(min, max, fluids.Add); @@ -200,7 +196,7 @@ public void QueryFluidsCenter(Vector3 center, Vector3 size, Action QueryFluidsCenterList(Vector3 center, Vector3 size, Action callback) { + public List QueryFluidsCenterList(Vector3 center, Vector3 size) { List fluids = new(); QueryFluidsCenter(center, size, fluids.Add); @@ -212,7 +208,7 @@ public void QueryVibrateObjects(BoundingBox3 box, Action cal QueryVibrateObjects(box.Min, box.Max, callback); } - public List QueryVibrateObjectsList(BoundingBox3 box, Action callback) { + public List QueryVibrateObjectsList(BoundingBox3 box) { List vibrateObjects = new(); QueryVibrateObjects(box, vibrateObjects.Add); @@ -228,7 +224,7 @@ public void QueryVibrateObjects(Vector3 min, Vector3 max, Action QueryVibrateObjectsList(Vector3 min, Vector3 max, Action callback) { + public List QueryVibrateObjectsList(Vector3 min, Vector3 max) { List vibrateObjects = new(); QueryVibrateObjects(min, max, vibrateObjects.Add); @@ -240,7 +236,7 @@ public void QueryVibrateObjectsCenter(Vector3 center, Vector3 size, Action QueryVibrateObjectsCenterList(Vector3 center, Vector3 size, Action callback) { + public List QueryVibrateObjectsCenterList(Vector3 center, Vector3 size) { List vibrateObjects = new(); QueryVibrateObjectsCenter(center, size, vibrateObjects.Add); From 29f64f879064dd0b8c3ccbaa92405168ba804d4c Mon Sep 17 00:00:00 2001 From: mettaursp Date: Mon, 30 Sep 2024 18:34:05 -0700 Subject: [PATCH 10/11] feedback changes --- .../Storage/Metadata/MapDataStorage.cs | 2 +- Maple2.File.Ingest/Mapper/MapDataMapper.cs | 29 ++++--- Maple2.File.Ingest/Mapper/MapEntityMapper.cs | 2 +- Maple2.File.Ingest/Program.cs | 4 +- Maple2.Model/Common/Vector.cs | 2 +- .../Game/Field/FieldAccelerationStructure.cs | 75 +++++++++---------- .../{MapEntity => FieldEntity}/FieldEntity.cs | 2 +- .../Graphics/Resources/Mesh.cs | 2 + .../Model/Field/Entity/FieldMobSpawn.cs | 9 +-- Maple2.Tools/VectorMath/BoundingBox3.cs | 9 ++- 10 files changed, 68 insertions(+), 68 deletions(-) rename Maple2.Model/Metadata/{MapEntity => FieldEntity}/FieldEntity.cs (97%) diff --git a/Maple2.Database/Storage/Metadata/MapDataStorage.cs b/Maple2.Database/Storage/Metadata/MapDataStorage.cs index dccec2cb3..743b82be7 100644 --- a/Maple2.Database/Storage/Metadata/MapDataStorage.cs +++ b/Maple2.Database/Storage/Metadata/MapDataStorage.cs @@ -37,7 +37,7 @@ public bool TryGet(string xblock, [NotNullWhen(true)] out FieldAccelerationStruc ByteReader reader = new ByteReader(output.ToArray()); - mapData = reader.ReadClass(); + mapData = reader.ReadClassWithNew(); Cache.AddReplace(xblock, mapData); } diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 7cd52f219..316ddb692 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -9,7 +9,7 @@ using Maple2.Model.Common; using Maple2.Model.Game.Field; using Maple2.Model.Metadata; -using Maple2.Model.Metadata.FieldEntities; +using Maple2.Model.Metadata.FieldEntity; using Maple2.PacketLib.Tools; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; @@ -19,6 +19,9 @@ namespace Maple2.File.Ingest.Mapper; public class MapDataMapper : TypeMapper { + public const float BLOCK_SIZE = (float) Constant.BlockSize; + public const float HALF_BLOCK = 0.5f * BLOCK_SIZE; + private readonly HashSet xBlocks; private readonly XBlockParser parser; @@ -34,13 +37,13 @@ public class MapDataMapper : TypeMapper { private readonly HashSet invalidLlids = new(); private readonly HashSet missingLlids = new(); - public MapDataMapper(MetadataContext db, M2dReader exportedReader, XBlockParser parser) { + public MapDataMapper(MetadataContext db, XBlockParser parser) { xBlocks = db.MapMetadata.Select(metadata => metadata.XBlock).ToHashSet(); this.parser = parser; } - public class StatsTracker { + private class StatsTracker { public ulong MinValue = ulong.MaxValue; public ulong MaxValue; @@ -63,7 +66,7 @@ public void AddValue(ulong value) { } } - private FieldAccelerationStructure ParseMapEntities(string xblock, IEnumerable entities) { + private FieldAccelerationStructure ParseMapEntities(IEnumerable entities) { Dictionary> gridAlignedEntities = new Dictionary>(); List unalignedEntities = new List(); Vector3S minIndex = new Vector3S(short.MaxValue, short.MaxValue, short.MaxValue); @@ -84,9 +87,9 @@ private FieldAccelerationStructure ParseMapEntities(string xblock, IEnumerable Map() { return new MapDataMetadata(xblock, GetEmptyMap()); } - FieldAccelerationStructure mapData = ParseMapEntities(xblock, map.entities); + FieldAccelerationStructure mapData = ParseMapEntities(map.entities); ByteWriter writer = new ByteWriter(); diff --git a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs index c3ca701dd..8ecdd8d57 100644 --- a/Maple2.File.Ingest/Mapper/MapEntityMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapEntityMapper.cs @@ -15,7 +15,7 @@ public class MapEntityMapper : TypeMapper { private readonly HashSet xBlocks; private readonly XBlockParser parser; - public MapEntityMapper(MetadataContext db, M2dReader exportedReader, XBlockParser parser) { + public MapEntityMapper(MetadataContext db, XBlockParser parser) { xBlocks = db.MapMetadata.Select(metadata => metadata.XBlock).ToHashSet(); this.parser = parser; diff --git a/Maple2.File.Ingest/Program.cs b/Maple2.File.Ingest/Program.cs index 7c2a5bbf5..c3a26f63b 100644 --- a/Maple2.File.Ingest/Program.cs +++ b/Maple2.File.Ingest/Program.cs @@ -144,9 +144,9 @@ XBlockParser parser = new XBlockParser(exportedReader, index); -UpdateDatabase(metadataContext, new MapEntityMapper(metadataContext, exportedReader, parser)); +UpdateDatabase(metadataContext, new MapEntityMapper(metadataContext, parser)); -MapDataMapper mapDataMapper = new MapDataMapper(metadataContext, exportedReader, parser); +MapDataMapper mapDataMapper = new MapDataMapper(metadataContext, parser); UpdateDatabase(metadataContext, mapDataMapper); diff --git a/Maple2.Model/Common/Vector.cs b/Maple2.Model/Common/Vector.cs index 4f8f20aeb..bcf63a5a8 100644 --- a/Maple2.Model/Common/Vector.cs +++ b/Maple2.Model/Common/Vector.cs @@ -40,7 +40,7 @@ public readonly record struct Vector3S(short X, short Y, short Z) { // This offset is used to correct rounding errors due to floating point arithmetic. private const float OFFSET = 0.001f; - public Vector3 Vector3 { get => new Vector3(X, Y, Z); } + public Vector3 Vector3 => new Vector3(X, Y, Z); public static implicit operator Vector3S(Vector3 vector) { return new Vector3S( diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs index bf5382778..06ac22df4 100644 --- a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -1,14 +1,17 @@ using Maple2.Model.Common; -using Maple2.Model.Metadata.FieldEntities; +using Maple2.Model.Metadata; +using Maple2.Model.Metadata.FieldEntity; using Maple2.PacketLib.Tools; using Maple2.Tools; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; +using Microsoft.VisualBasic; using System.Numerics; using System.Runtime.InteropServices; namespace Maple2.Model.Game.Field; +[Flags] internal enum FieldEntityMembers : byte { None = 0x0, Id = 0x1, @@ -59,36 +62,33 @@ internal enum FieldEntityMembers : byte { */ public class FieldAccelerationStructure : IByteSerializable, IByteDeserializable { public const int AXIS_TRIM_ENTITY_COUNT = 10; + public const float BLOCK_SIZE = (float) Constant.BlockSize; + public const float HALF_BLOCK = 0.5f * BLOCK_SIZE; public Vector3S GridSize { get; private set; } = new Vector3S(); public Vector3S MinIndex { get; private set; } = new Vector3S(); public Vector3S MaxIndex { get; private set; } = new Vector3S(); - public ReadOnlySpan AlignedEntities { get => CollectionsMarshal.AsSpan(alignedEntities); } - public ReadOnlySpan AlignedTrimmedEntities { get => CollectionsMarshal.AsSpan(alignedTrimmedEntities); } - public ReadOnlySpan UnalignedEntities { get => CollectionsMarshal.AsSpan(unalignedEntities); } + public ReadOnlySpan AlignedEntities => CollectionsMarshal.AsSpan(alignedEntities); + public ReadOnlySpan AlignedTrimmedEntities => CollectionsMarshal.AsSpan(alignedTrimmedEntities); + public ReadOnlySpan UnalignedEntities => CollectionsMarshal.AsSpan(unalignedEntities); // Make a list of vibrate objects on the field with the same size & order as this list // Then in queries use field.VibrateObjects[vibrateEntity.VibrateIndex] to retrieve the right one - public ReadOnlySpan VibrateEntities { get => CollectionsMarshal.AsSpan(vibrateEntities); } + public ReadOnlySpan VibrateEntities => CollectionsMarshal.AsSpan(vibrateEntities); - private List alignedEntities; - private List alignedTrimmedEntities; - private List unalignedEntities; // TODO: add AABB tree implementation for querying unaligned objects - private List vibrateEntities; - private int[,,] cellGrid; + private List alignedEntities = new(); + private List alignedTrimmedEntities = new(); + private List unalignedEntities = new(); // TODO: add AABB tree implementation for querying unaligned objects + private List vibrateEntities = new(); + private int[,,] cellGrid = new int[0, 0, 0]; public ulong GridBytesWritten { get; private set; } = 0; public FieldAccelerationStructure() { - alignedEntities = new(); - alignedTrimmedEntities = new(); - unalignedEntities = new(); - vibrateEntities = new(); - cellGrid = new int[0, 0, 0]; } public static Vector3S PointToCell(Vector3 point) { - point *= (1 / 150.0f); + point *= (1 / BLOCK_SIZE); return new Vector3S((short) Math.Floor(point.X + 0.5f), (short) Math.Floor(point.Y + 0.5f), (short) Math.Floor(point.Z)); } @@ -291,8 +291,8 @@ private void GenerateSpawnLocations(Dictionary> grid } foreach (FieldEntity entity in unalignedEntities) { - Vector3 minPosition = (1 / 150.0f) * entity.Bounds.Min; - Vector3 maxPosition = (1 / 150.0f) * entity.Bounds.Max; + Vector3 minPosition = (1 / BLOCK_SIZE) * entity.Bounds.Min; + Vector3 maxPosition = (1 / BLOCK_SIZE) * entity.Bounds.Max; Vector3S minCubeIndex = PointToCell(minPosition); Vector3S maxCubeIndex = PointToCell(maxPosition); @@ -332,8 +332,8 @@ private void GenerateSpawnLocations(Dictionary> grid gridAlignedEntities.Add(coord, entities); } - Vector3 cellPosition = 150.0f * coord.Vector3; - BoundingBox3 bounds = new BoundingBox3(cellPosition - new Vector3(75, 75, 0), cellPosition + new Vector3(75, 75, 150)); + Vector3 cellPosition = BLOCK_SIZE * coord.Vector3; + BoundingBox3 bounds = new BoundingBox3(cellPosition - new Vector3(HALF_BLOCK, HALF_BLOCK, 0), cellPosition + new Vector3(HALF_BLOCK, HALF_BLOCK, BLOCK_SIZE)); entities.Add(new FieldSpawnTile( Id: new FieldEntityId(0, 0), @@ -473,7 +473,7 @@ public void TrimGridSize(Dictionary> gridAlignedEnti if (isTrimmed && entityList.Count > 0) { trimmedCount += entityList.Count; - Vector3 cellPosition = 150.0f * coord.Vector3; + Vector3 cellPosition = BLOCK_SIZE * coord.Vector3; BoundingBox3 bounds = entityList.First().Bounds; foreach (FieldEntity entity in entityList) { @@ -647,19 +647,19 @@ public void WriteTo(IByteWriter writer) { #region Serialization private Vector3S GetWorldGridIndex(Vector3 position) { - int x = (int) Math.Round(position.X) / 150; - int y = (int) Math.Round(position.Y) / 150; - int z = (int) Math.Round(position.Z) / 150; + int x = (int) Math.Round(position.X) / Constant.BlockSize; + int y = (int) Math.Round(position.Y) / Constant.BlockSize; + int z = (int) Math.Round(position.Z) / Constant.BlockSize; return new Vector3S((short) x, (short) y, (short) z); } private bool IsGridAligned(Vector3 position) { - int x = (int) Math.Round(position.X) / 150; - int y = (int) Math.Round(position.Y) / 150; - int z = (int) Math.Round(position.Z) / 150; + int x = (int) Math.Round(position.X) / Constant.BlockSize; + int y = (int) Math.Round(position.Y) / Constant.BlockSize; + int z = (int) Math.Round(position.Z) / Constant.BlockSize; - return position.IsNearlyEqual(150 * new Vector3(x, y, z), 0.1f); + return position.IsNearlyEqual(BLOCK_SIZE * new Vector3(x, y, z), 0.1f); } private bool IsCellBounds(Vector3 position, BoundingBox3 bounds) { @@ -667,8 +667,8 @@ private bool IsCellBounds(Vector3 position, BoundingBox3 bounds) { return false; } - bool isMinOnCell = bounds.Min.IsNearlyEqual(position - new Vector3(75, 75, 0), 0.1f); - bool isMaxOnCell = bounds.Max.IsNearlyEqual(position + new Vector3(75, 75, 150), 0.1f); + bool isMinOnCell = bounds.Min.IsNearlyEqual(position - new Vector3(HALF_BLOCK, HALF_BLOCK, 0), 0.1f); + bool isMaxOnCell = bounds.Max.IsNearlyEqual(position + new Vector3(HALF_BLOCK, HALF_BLOCK, BLOCK_SIZE), 0.1f); return isMinOnCell && isMaxOnCell; } @@ -764,13 +764,6 @@ public void ReadFrom(IByteReader reader) { MaxIndex = MinIndex + GridSize - new Vector3S(1, 1, 1); cellGrid = new int[GridSize.X, GridSize.Y, GridSize.Z]; - if (alignedEntities is null || unalignedEntities is null || alignedTrimmedEntities is null || vibrateEntities is null) { - alignedEntities = new(); - alignedTrimmedEntities = new(); - unalignedEntities = new(); - vibrateEntities = new(); - } - alignedEntities.Clear(); alignedTrimmedEntities.Clear(); unalignedEntities.Clear(); @@ -849,7 +842,7 @@ public FieldEntity ReadEntity(IByteReader reader) { if ((memberFlags & FieldEntityMembers.Position) != 0) { position = reader.Read(); } else { - position = 150 * reader.Read().Vector3; + position = BLOCK_SIZE * reader.Read().Vector3; } if ((memberFlags & FieldEntityMembers.Rotation) != 0) { @@ -866,8 +859,8 @@ public FieldEntity ReadEntity(IByteReader reader) { max: reader.Read()); } else { bounds = new BoundingBox3( - min: position - new Vector3(75, 75, 0), - max: position + new Vector3(75, 75, 150)); + min: position - new Vector3(HALF_BLOCK, HALF_BLOCK, 0), + max: position + new Vector3(HALF_BLOCK, HALF_BLOCK, BLOCK_SIZE)); } switch (type) { @@ -924,7 +917,7 @@ public FieldEntity ReadEntity(IByteReader reader) { IsSurface: reader.Read()); case FieldEntityType.Cell: int childCount = reader.ReadInt(); - List children = new List(); + var children = new List(); for (int i = 0; i < childCount; ++i) { children.Add(ReadEntity(reader)); } diff --git a/Maple2.Model/Metadata/MapEntity/FieldEntity.cs b/Maple2.Model/Metadata/FieldEntity/FieldEntity.cs similarity index 97% rename from Maple2.Model/Metadata/MapEntity/FieldEntity.cs rename to Maple2.Model/Metadata/FieldEntity/FieldEntity.cs index 3a21a13a3..94aba85e8 100644 --- a/Maple2.Model/Metadata/MapEntity/FieldEntity.cs +++ b/Maple2.Model/Metadata/FieldEntity/FieldEntity.cs @@ -1,7 +1,7 @@ using Maple2.Tools.VectorMath; using System.Numerics; -namespace Maple2.Model.Metadata.FieldEntities; +namespace Maple2.Model.Metadata.FieldEntity; public enum FieldEntityType : byte { Unknown, diff --git a/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs b/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs index 83b0fc8e9..24116edfb 100644 --- a/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs +++ b/Maple2.Server.DebugGame/Graphics/Resources/Mesh.cs @@ -60,6 +60,7 @@ public void UploadData(Ms2MeshData meshData) { indexCount = (uint) meshData.IndexBuffer.Length; } +#pragma warning disable CS8500 private unsafe void UploadBuffer(ReadOnlySpan data, ref ComPtr buffer, BindFlag flags) { var bufferDescription = new BufferDesc { ByteWidth = (uint) (data.Length * sizeof(BufferType)), @@ -77,6 +78,7 @@ private unsafe void UploadBuffer(ReadOnlySpan data, ref buffer = bufferHandle; } } +#pragma warning restore CS8500 public void CleanUp() { if (indexCount == 0) { diff --git a/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs b/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs index 9c2b94ea5..8511b25d9 100644 --- a/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs +++ b/Maple2.Server.Game/Model/Field/Entity/FieldMobSpawn.cs @@ -1,9 +1,8 @@ using System.Numerics; -using System.Reflection.Metadata.Ecma335; using Maple2.Model.Game; using Maple2.Model.Game.Field; using Maple2.Model.Metadata; -using Maple2.Model.Metadata.FieldEntities; +using Maple2.Model.Metadata.FieldEntity; using Maple2.Server.Game.Manager.Field; using Maple2.Server.Game.Packets; using Maple2.Tools; @@ -69,15 +68,15 @@ public void Despawn(int objectId) { private List GetRandomSpawns(int count) { List spawnsPicked = new(); - Vector3[] spawnsRemaining = new Vector3[validSpawns.Count]; + var spawnsRemaining = new Vector3[validSpawns.Count]; validSpawns.CopyTo(spawnsRemaining); - int selectSpawns = int.Min(count, validSpawns.Count); + int selectSpawns = Math.Min(count, validSpawns.Count); int remainder = count - selectSpawns; for (int i = 0; i < selectSpawns; ++i) { - int picked = Random.Shared.Next(0, spawnsRemaining.Length - i); + int picked = Random.Shared.Next(spawnsRemaining.Length - i); spawnsPicked.Add(spawnsRemaining[picked]); spawnsRemaining[picked] = spawnsRemaining[selectSpawns - i - 1]; // remove picked from list by replacing with last in list diff --git a/Maple2.Tools/VectorMath/BoundingBox3.cs b/Maple2.Tools/VectorMath/BoundingBox3.cs index a6fc86f90..039f0fe06 100644 --- a/Maple2.Tools/VectorMath/BoundingBox3.cs +++ b/Maple2.Tools/VectorMath/BoundingBox3.cs @@ -1,6 +1,7 @@ using Maple2.Tools.Extensions; using System.Collections; using System.Collections.Generic; +using System.IO; using System.Numerics; using System.Runtime.CompilerServices; @@ -10,8 +11,8 @@ public struct BoundingBox3 { public Vector3 Min; public Vector3 Max; - public Vector3 Size { get => Max - Min; } - public Vector3 Center { get => 0.5f * (Max + Min); } + public Vector3 Size => Max - Min; + public Vector3 Center => 0.5f * (Max + Min); [MethodImpl(MethodImplOptions.AggressiveInlining)] public BoundingBox3(Vector3 min = new Vector3()) { @@ -49,7 +50,7 @@ public BoundingBox3 Fatten(float amount) { public static BoundingBox3 Transform(BoundingBox3 box, Matrix4x4 matrix) { Vector3 translation = matrix.Translation; - BoundingBox3 result = new BoundingBox3(translation); + var result = new BoundingBox3(translation); for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { @@ -106,7 +107,7 @@ public static BoundingBox3 TransformAndTest(BoundingBox3 box, Matrix4x4 matrix) BoundingBox3 result2 = Compute(vertices); if (!result.IsNearlyEqual(result2, 1e-2f)) { - throw new System.Exception("possibly wrong axis"); + throw new InvalidDataException("possibly wrong axis"); } return result; From 57d6c4c9d0b62047ae64e37f8f9254b271d8699e Mon Sep 17 00:00:00 2001 From: mettaursp Date: Wed, 2 Oct 2024 14:54:02 -0700 Subject: [PATCH 11/11] feedback changes --- .../Storage/Metadata/MapDataStorage.cs | 2 +- Maple2.File.Ingest/Mapper/MapDataMapper.cs | 27 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Maple2.Database/Storage/Metadata/MapDataStorage.cs b/Maple2.Database/Storage/Metadata/MapDataStorage.cs index 743b82be7..b0d399cdc 100644 --- a/Maple2.Database/Storage/Metadata/MapDataStorage.cs +++ b/Maple2.Database/Storage/Metadata/MapDataStorage.cs @@ -9,7 +9,7 @@ namespace Maple2.Database.Storage.Metadata; public class MapDataStorage(MetadataContext context) : MetadataStorage(context, CACHE_SIZE) { - private const int CACHE_SIZE = 500; // ~500 total items + private const int CACHE_SIZE = 1500; // ~1.1k total Maps public bool TryGet(string xblock, [NotNullWhen(true)] out FieldAccelerationStructure? mapData) { if (Cache.TryGet(xblock, out mapData)) { diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 316ddb692..21811d94e 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -342,20 +342,17 @@ protected override IEnumerable Map() { } public void ReportStats() { - string blue = "\u001b[38;2;0;215;255m"; - string white = "\u001b[0m"; - - Console.WriteLine($"Total maps parsed:{blue} {mapByteStats.Entries} {white}"); - Console.WriteLine($"Total bytes:{blue} {mapByteStats.TotalValue} {white}"); - Console.WriteLine($"Average map bytes:{blue} {mapByteStats.AvgValue} {white}"); - Console.WriteLine($"Largest map bytes:{blue} {mapByteStats.MaxValue} {white}"); - Console.WriteLine($"Largest map dimensions:{blue} < {mapXStats.MaxValue}, {mapYStats.MaxValue}, {mapZStats.MaxValue} > {white}"); - Console.WriteLine($"Average map dimensions:{blue} < {mapXStats.AvgValue}, {mapYStats.AvgValue}, {mapZStats.AvgValue} > {white}"); - Console.WriteLine($"Largest aligned entities:{blue} {alignedStats.MaxValue} {white}"); - Console.WriteLine($"Average aligned entities:{blue} {alignedStats.AvgValue} {white}"); - Console.WriteLine($"Largest trimmed aligned entities:{blue} {alignedTrimmedStats.MaxValue} {white}"); - Console.WriteLine($"Average trimmed aligned entities:{blue} {alignedTrimmedStats.AvgValue} {white}"); - Console.WriteLine($"Largest unaligned entities:{blue} {unalignedStats.MaxValue} {white}"); - Console.WriteLine($"Average unaligned entities:{blue} {unalignedStats.AvgValue} {white}"); + Console.WriteLine($"Total maps parsed: {mapByteStats.Entries.ToString().ColorBlue()}"); + Console.WriteLine($"Total bytes: {mapByteStats.TotalValue.ToString().ColorBlue()} "); + Console.WriteLine($"Average map bytes: {mapByteStats.AvgValue.ToString().ColorBlue()} "); + Console.WriteLine($"Largest map bytes: {mapByteStats.MaxValue.ToString().ColorBlue()} "); + Console.WriteLine($"Largest map dimensions: {$"< {mapXStats.MaxValue}, {mapYStats.MaxValue}, {mapZStats.MaxValue} >".ColorBlue()} "); + Console.WriteLine($"Average map dimensions: {$"< {mapXStats.AvgValue}, {mapYStats.AvgValue}, {mapZStats.AvgValue} >".ColorBlue()} "); + Console.WriteLine($"Largest aligned entities: {alignedStats.MaxValue.ToString().ColorBlue()} "); + Console.WriteLine($"Average aligned entities: {alignedStats.AvgValue.ToString().ColorBlue()} "); + Console.WriteLine($"Largest trimmed aligned entities: {alignedTrimmedStats.MaxValue.ToString().ColorBlue()} "); + Console.WriteLine($"Average trimmed aligned entities: {alignedTrimmedStats.AvgValue.ToString().ColorBlue()} "); + Console.WriteLine($"Largest unaligned entities: {unalignedStats.MaxValue.ToString().ColorBlue()} "); + Console.WriteLine($"Average unaligned entities: {unalignedStats.AvgValue.ToString().ColorBlue()} "); } }