diff --git a/Maple2.File.Ingest/Mapper/MapDataMapper.cs b/Maple2.File.Ingest/Mapper/MapDataMapper.cs index 21811d94e..c8cacf960 100644 --- a/Maple2.File.Ingest/Mapper/MapDataMapper.cs +++ b/Maple2.File.Ingest/Mapper/MapDataMapper.cs @@ -9,12 +9,12 @@ using Maple2.Model.Common; using Maple2.Model.Game.Field; using Maple2.Model.Metadata; -using Maple2.Model.Metadata.FieldEntity; using Maple2.PacketLib.Tools; using Maple2.Tools.Extensions; using Maple2.Tools.VectorMath; using System.IO.Compression; using System.Numerics; +using Maple2.Model.Metadata.FieldEntity; namespace Maple2.File.Ingest.Mapper; @@ -145,6 +145,21 @@ private FieldAccelerationStructure ParseMapEntities(IEnumerable enti break; } + if (entity is IMS2CubeProp cube && cube.CubeSalableGroup != 0) { + entityBounds.Min = -new Vector3(HALF_BLOCK, HALF_BLOCK, 0); + entityBounds.Max = new Vector3(HALF_BLOCK, HALF_BLOCK, BLOCK_SIZE); + fieldEntity = new FieldSellableTile( + Id: entityId, + Position: placeable.Position, + Rotation: placeable.Rotation, + Scale: placeable.Scale, + Bounds: entityBounds, + SellableGroup: cube.CubeSalableGroup + ); + + break; + } + bool isFluid = false; if (entity is IMS2MapProperties meshMapProperties) { diff --git a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs index 06ac22df4..fc99f6652 100644 --- a/Maple2.Model/Game/Field/FieldAccelerationStructure.cs +++ b/Maple2.Model/Game/Field/FieldAccelerationStructure.cs @@ -1,13 +1,12 @@ using Maple2.Model.Common; -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; +using Maple2.Model.Metadata; +using Maple2.Model.Metadata.FieldEntity; namespace Maple2.Model.Game.Field; @@ -19,21 +18,21 @@ internal enum FieldEntityMembers : byte { Rotation = 0x4, Scale = 0x8, Bounds = 0x10, - Llid = 0x20 + 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. @@ -54,7 +53,7 @@ internal enum FieldEntityMembers : byte { * - 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. * - Fluids: Captures all fluids within a bounding box. @@ -178,12 +177,24 @@ public List QueryFluidsList(BoundingBox3 box) { public void QueryFluids(Vector3 min, Vector3 max, Action callback) { QueryCells(min, max, (entity) => { - if (entity is FieldFluidEntity fluid && fluid.IsSurface && !fluid.IsShallow) { + if (entity is FieldFluidEntity { IsSurface: true, IsShallow: false } fluid) { callback(fluid); } }); } + public void QuerySellableTiles(Vector3 min, Vector3 max, Action callback) { + QueryCells(min, max, (entity) => { + if (entity is FieldSellableTile tile) { + callback(tile); + } + }); + } + + public void QuerySellableTilesCenter(Vector3 center, Vector3 size, Action callback) { + QuerySellableTiles(center - 0.5f * size, center + 0.5f * size, callback); + } + public List QueryFluidsList(Vector3 min, Vector3 max) { List fluids = new(); @@ -681,7 +692,8 @@ public void WriteTo(FieldEntity entity, IByteWriter writer) { FieldFluidEntity => FieldEntityType.Fluid, FieldMeshColliderEntity => FieldEntityType.MeshCollider, FieldCellEntities => FieldEntityType.Cell, - _ => FieldEntityType.Unknown + FieldSellableTile => FieldEntityType.SellableTile, + _ => FieldEntityType.Unknown, }; FieldEntityMembers memberFlags = FieldEntityMembers.None; @@ -753,6 +765,9 @@ public void WriteTo(FieldEntity entity, IByteWriter writer) { WriteTo(childEntity, writer); } break; + case FieldSellableTile sellableTile: + writer.Write(sellableTile.SellableGroup); + break; default: throw new InvalidDataException($"Writing unhandled field entity type: {entity.GetType().FullName}"); } @@ -928,6 +943,14 @@ public FieldEntity ReadEntity(IByteReader reader) { Scale: scale, Bounds: bounds, Entities: children); + case FieldEntityType.SellableTile: + return new FieldSellableTile( + Id: id, + Position: position, + Rotation: rotation, + Scale: scale, + Bounds: bounds, + SellableGroup: reader.Read()); default: throw new InvalidDataException($"Reading unhandled field entity type: {type}"); } diff --git a/Maple2.Model/Metadata/FieldEntity/FieldEntity.cs b/Maple2.Model/Metadata/FieldEntity/FieldEntity.cs index 94aba85e8..dcd800ae1 100644 --- a/Maple2.Model/Metadata/FieldEntity/FieldEntity.cs +++ b/Maple2.Model/Metadata/FieldEntity/FieldEntity.cs @@ -1,5 +1,5 @@ -using Maple2.Tools.VectorMath; -using System.Numerics; +using System.Numerics; +using Maple2.Tools.VectorMath; namespace Maple2.Model.Metadata.FieldEntity; @@ -10,7 +10,8 @@ public enum FieldEntityType : byte { BoxCollider, // for cube tiles (IsWhiteBox = false) & arbitrarily sized white boxes MeshCollider, Fluid, - Cell // for cells culled from the grid & demoted to AABB tree + SellableTile, + Cell, // for cells culled from the grid & demoted to AABB tree } public record FieldEntityId( @@ -83,3 +84,12 @@ public record FieldCellEntities( float Scale, BoundingBox3 Bounds, List Entities) : FieldEntity(Id, Position, Rotation, Scale, Bounds); + +public record FieldSellableTile( + FieldEntityId Id, + Vector3 Position, + Vector3 Rotation, + float Scale, + BoundingBox3 Bounds, + int SellableGroup +) : FieldEntity(Id, Position, Rotation, Scale, Bounds); diff --git a/Maple2.Server.Game/Commands/DebugCommand.cs b/Maple2.Server.Game/Commands/DebugCommand.cs index 39b78befe..5102a7866 100644 --- a/Maple2.Server.Game/Commands/DebugCommand.cs +++ b/Maple2.Server.Game/Commands/DebugCommand.cs @@ -150,6 +150,7 @@ public DebugQueryCommand(GameSession session, MapDataStorage mapDataStorage) : b AddCommand(new DebugQuerySpawnCommand(session, mapDataStorage)); AddCommand(new DebugQueryFluidCommand(session, mapDataStorage)); AddCommand(new DebugQueryVibrateCommand(session, mapDataStorage)); + AddCommand(new DebugQuerySellableTileCommand(session, mapDataStorage)); } private class DebugQuerySpawnCommand : Command { @@ -274,5 +275,48 @@ private void Handle(InvocationContext ctx, float x, float y, float z) { }); } } + + private class DebugQuerySellableTileCommand : Command { + private readonly GameSession session; + private readonly MapDataStorage mapDataStorage; + + public DebugQuerySellableTileCommand(GameSession session, MapDataStorage mapDataStorage) : base("sellable", "Searches for nearby sellable tiles.") { + 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.QuerySellableTilesCenter(session.Player.Position, 2 * new Vector3(x, y, z), (sellableTile) => { + Vector3 position = sellableTile.Position; + Vector3S toCell = FieldAccelerationStructure.PointToCell(position); + + + ctx.Console.Out.WriteLine($"SellableGroupId {sellableTile.SellableGroup} found at {position.X} {position.Y} {position.Z} in cell {toCell.X} {toCell.Y} {toCell.Z}"); + }); + } + } } }