From 6188c2e751de17dc5d220d285aeaa2291d922f8d Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Sat, 31 Dec 2016 00:55:00 +0000 Subject: [PATCH] Add support for dynamic collision detection This commit implements collision detection using the map files loaded from the cache, and adds support for modifying the collision matrices at runtime when the game world is updated. All checks to see if a tile is reachable should now be done via. World#traversable, instead of Region#traversable, as the World object can handle checking tiles across multiple regions. These are done for the WalkingQueue and Pathfinder implementations. --- .../main/org/apollo/cache/map/MapObject.java | 14 + .../game/fs/decoder/GameObjectDecoder.java | 286 ----------------- .../game/fs/decoder/WorldMapDecoder.java | 101 ++++++ .../game/fs/decoder/WorldObjectsDecoder.java | 78 +++++ .../main/org/apollo/game/model/Direction.java | 150 +++++++++ .../main/org/apollo/game/model/Position.java | 16 +- .../src/main/org/apollo/game/model/World.java | 50 ++- .../org/apollo/game/model/area/Region.java | 9 + .../model/area/collision/CollisionFlag.java | 86 +++++- .../area/collision/CollisionManager.java | 212 +++++++++++++ .../model/area/collision/CollisionMatrix.java | 61 ++-- .../model/area/collision/CollisionUpdate.java | 248 +++++++++++++++ .../area/collision/CollisionUpdateType.java | 16 + .../GameObjectCollisionUpdateListener.java | 49 +++ .../game/model/entity/WalkingQueue.java | 45 ++- .../path/AStarPathfindingAlgorithm.java | 285 ++++++++--------- .../entity/path/PathfindingAlgorithm.java | 18 +- .../path/SimplePathfindingAlgorithm.java | 287 +++++++++--------- .../game/scheduling/impl/NpcMovementTask.java | 8 +- 19 files changed, 1380 insertions(+), 639 deletions(-) delete mode 100644 game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java create mode 100644 game/src/main/org/apollo/game/fs/decoder/WorldMapDecoder.java create mode 100644 game/src/main/org/apollo/game/fs/decoder/WorldObjectsDecoder.java create mode 100644 game/src/main/org/apollo/game/model/area/collision/CollisionManager.java create mode 100644 game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java create mode 100644 game/src/main/org/apollo/game/model/area/collision/CollisionUpdateType.java create mode 100644 game/src/main/org/apollo/game/model/area/collision/GameObjectCollisionUpdateListener.java diff --git a/cache/src/main/org/apollo/cache/map/MapObject.java b/cache/src/main/org/apollo/cache/map/MapObject.java index b2cf741d..72980add 100644 --- a/cache/src/main/org/apollo/cache/map/MapObject.java +++ b/cache/src/main/org/apollo/cache/map/MapObject.java @@ -40,6 +40,20 @@ public final class MapObject { this.orientation = orientation; } + /** + * Create a new {@code MapObject}. + * + * @param id The object ID of this map object. + * @param x The local X coordinate of this object. + * @param y The local Y coordinate of this object. + * @param height The height level of this object. + * @param type The type of this object. + * @param orientation The orientation of this object. + */ + public MapObject(int id, int x, int y, int height, int type, int orientation) { + this(id, (height & 0x3f) << 12 | (x & 0x3f) << 6 | (y & 0x3f), type, orientation); + } + /** * Get the object ID of this map object. * diff --git a/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java b/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java deleted file mode 100644 index 1998f12f..00000000 --- a/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java +++ /dev/null @@ -1,286 +0,0 @@ -package org.apollo.game.fs.decoder; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.apollo.cache.IndexedFileSystem; -import org.apollo.cache.decoder.ObjectDefinitionDecoder; -import org.apollo.cache.def.ObjectDefinition; -import org.apollo.cache.map.MapIndex; -import org.apollo.cache.map.MapIndexDecoder; -import org.apollo.game.io.player.PlayerSerializer; -import org.apollo.game.model.Position; -import org.apollo.game.model.World; -import org.apollo.game.model.area.Region; -import org.apollo.game.model.area.RegionRepository; -import org.apollo.game.model.area.collision.CollisionMatrix; -import org.apollo.game.model.entity.obj.GameObject; -import org.apollo.game.model.entity.obj.ObjectType; -import org.apollo.game.model.entity.obj.StaticGameObject; -import org.apollo.util.BufferUtil; -import org.apollo.util.CompressionUtil; - -/** - * Parses static object definitions, which include map tiles and landscapes. - * - * @author Ryley - * @author Major - */ -public final class GameObjectDecoder implements Runnable { - - /** - * A bit flag that indicates that the tile at the current Position is blocked. - */ - private static final int BLOCKED_TILE = 1; - - /** - * A bit flag that indicates that the tile at the current Position is a bridge tile. - */ - private static final int BRIDGE_TILE = 2; - - /** - * The {@link IndexedFileSystem}. - */ - private final IndexedFileSystem fs; - - /** - * A {@link List} of decoded GameObjects. - */ - private final List objects = new ArrayList<>(); - - /** - * The RegionRepository. - */ - private final RegionRepository regions; - - /** - * The World to place the objects in. - */ - private final World world; - - /** - * The most-recently used Region. - */ - private Region previous; - - /** - * Creates the GameObjectDecoder. - * - * @param fs The {@link IndexedFileSystem}. - * @param world The {@link World} to place the objects in. - */ - public GameObjectDecoder(IndexedFileSystem fs, World world) { - this.fs = fs; - this.world = world; - regions = world.getRegionRepository(); - previous = regions.fromPosition(PlayerSerializer.TUTORIAL_ISLAND_SPAWN); // dummy, so 'previous' is never null. - } - - @Override - public void run() { - ObjectDefinitionDecoder decoder = new ObjectDefinitionDecoder(fs); - decoder.run(); - - MapIndexDecoder mapIndexDecoder = new MapIndexDecoder(fs); - mapIndexDecoder.run(); - - try { - Map indices = MapIndex.getIndices(); - - for (MapIndex definition : indices.values()) { - int packed = definition.getPackedCoordinates(); - int x = (packed >> 8 & 0xFF) * (Region.SIZE * Region.SIZE); - int y = (packed & 0xFF) * (Region.SIZE * Region.SIZE); - - ByteBuffer objects = fs.getFile(4, definition.getObjectFile()); - ByteBuffer decompressed = ByteBuffer.wrap(CompressionUtil.degzip(objects)); - decodeObjects(decompressed, x, y); - - ByteBuffer terrain = fs.getFile(4, definition.getMapFile()); - decompressed = ByteBuffer.wrap(CompressionUtil.degzip(terrain)); - decodeTerrain(decompressed, x, y); - } - } catch (IOException e) { - throw new UncheckedIOException("Error decoding StaticGameObjects.", e); - } - - objects.forEach(object -> regions.fromPosition(object.getPosition()).addEntity(object, false)); - } - - /** - * Blocks tiles covered by a GameObject, if applicable. - * - * @param object The {@link GameObject}. - * @param position The position of the GameObject. - */ - private void block(GameObject object, Position position) { - ObjectDefinition definition = ObjectDefinition.lookup(object.getId()); - int type = object.getType(); - - int x = position.getX(), y = position.getY(), height = position.getHeight(); - - if (!previous.contains(position)) { - previous = regions.fromPosition(position); - } - - CollisionMatrix matrix = previous.getMatrix(height); - if (unwalkable(definition, type)) { - int width = definition.getWidth(), length = definition.getLength(); - - for (int dx = 0; dx < width; dx++) { - for (int dy = 0; dy < length; dy++) { - int localX = x % Region.SIZE + dx, localY = y % Region.SIZE + dy; - - if (localX > 7 || localY > 7) { - int nextLocalX = localX > 7 ? x + localX - 7 : x + localX; - int nextLocalY = localY > 7 ? y + localY - 7 : y - localY; - Region next = regions.fromPosition(new Position(nextLocalX, nextLocalY)); - - int nextX = nextLocalX % Region.SIZE + dx; - int nextY = nextLocalY % Region.SIZE + dy; - - if (nextX > 7) { - nextX -= 7; - } - - if (nextY > 7) { - nextY -= 7; - } - - next.getMatrix(height).block(nextX, nextY); - continue; - } - - matrix.block(localX, localY); - } - } - } - } - - /** - * Decodes the attributes of a terrain file, blocking the tile if necessary. - * - * @param attributes The terrain attributes. - * @param x The x coordinate of the tile the attributes belong to. - * @param y The y coordinate of the tile the attributes belong to. - * @param height The level level of the tile the attributes belong to. - */ - private void decodeAttributes(int attributes, int x, int y, int height) { - boolean block = false; - if ((attributes & BLOCKED_TILE) != 0) { - block = true; - } - - if ((attributes & BRIDGE_TILE) != 0 && height >0) { - block = true; - height--; - } - - if (block) { - int localX = x % Region.SIZE, localY = y % Region.SIZE; - Position position = new Position(x, y, height); - - if (!previous.contains(position)) { - previous = regions.fromPosition(position); - } - - previous.getMatrix(height).block(localX, localY); - } - } - - /** - * Decodes object data stored in the specified {@link ByteBuffer}. - * - * @param buffer The ByteBuffer to decode data from. - * @param x The x coordinate of the top left tile of the map file. - * @param y The y coordinate of the top left tile of the map file. - */ - private void decodeObjects(ByteBuffer buffer, int x, int y) { - int id = -1; - int idOffset = BufferUtil.readSmart(buffer); - - while (idOffset != 0) { - id += idOffset; - - int packed = 0; - int positionOffset = BufferUtil.readSmart(buffer); - - while (positionOffset != 0) { - packed += positionOffset - 1; - - int localY = packed & 0x3F; - int localX = packed >> 6 & 0x3F; - int height = packed >> 12 & 0x3; - - int attributes = buffer.get() & 0xFF; - int type = attributes >> 2; - int orientation = attributes & 0x3; - Position position = new Position(x + localX, y + localY, height); - - GameObject object = new StaticGameObject(world, id, position, type, orientation); - objects.add(object); - - block(object, position); - positionOffset = BufferUtil.readSmart(buffer); - } - - idOffset = BufferUtil.readSmart(buffer); - } - } - - /** - * Decodes terrain data stored in the specified {@link ByteBuffer}. - * - * @param buffer The ByteBuffer. - * @param x The x coordinate of the top left tile of the map file. - * @param y The y coordinate of the top left tile of the map file. - */ - private void decodeTerrain(ByteBuffer buffer, int x, int y) { - for (int height = 0; height < Position.HEIGHT_LEVELS; height++) { - for (int localX = 0; localX < Region.SIZE * Region.SIZE; localX++) { - for (int localY = 0; localY < Region.SIZE * Region.SIZE; localY++) { - int attributes = 0; - - while (true) { - int attributeId = buffer.get() & 0xFF; - - if (attributeId == 0) { - decodeAttributes(attributes, x + localX, y + localY, height); - break; - } else if (attributeId == 1) { - buffer.get(); - decodeAttributes(attributes, x + localX, y + localY, height); - break; - } else if (attributeId <= 49) { - buffer.get(); - } else if (attributeId <= 81) { - attributes = attributeId - 49; - } - } - } - } - } - } - - /** - * Returns whether or not an object with the specified {@link ObjectDefinition} and {@code type} should result in - * the tile(s) it is located on being blocked. - * - * @param definition The {@link ObjectDefinition} of the object. - * @param type The type of the object. - * @return {@code true} iff the tile(s) the object is on should be blocked. - */ - private boolean unwalkable(ObjectDefinition definition, int type) { - // TODO figure out the other ObjectTypes and get rid of all the getValue() calls - return (type == ObjectType.FLOOR_DECORATION.getValue() && definition.isInteractive()) || - (type >= ObjectType.LENGTHWISE_WALL.getValue() && type <= ObjectType.RECTANGULAR_CORNER.getValue()) || - (type > ObjectType.DIAGONAL_INTERACTABLE.getValue() && type < ObjectType.FLOOR_DECORATION.getValue()) || - (type == ObjectType.INTERACTABLE.getValue() && definition.isSolid()) || - type == ObjectType.DIAGONAL_WALL.getValue(); - } - -} \ No newline at end of file diff --git a/game/src/main/org/apollo/game/fs/decoder/WorldMapDecoder.java b/game/src/main/org/apollo/game/fs/decoder/WorldMapDecoder.java new file mode 100644 index 00000000..1f6fac93 --- /dev/null +++ b/game/src/main/org/apollo/game/fs/decoder/WorldMapDecoder.java @@ -0,0 +1,101 @@ +package org.apollo.game.fs.decoder; + +import org.apollo.cache.IndexedFileSystem; +import org.apollo.cache.map.MapConstants; +import org.apollo.cache.map.MapFile; +import org.apollo.cache.map.MapFileDecoder; +import org.apollo.cache.map.MapIndex; +import org.apollo.cache.map.MapPlane; +import org.apollo.cache.map.Tile; +import org.apollo.game.model.Position; +import org.apollo.game.model.area.collision.CollisionManager; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Map; + +/** + * A decoder which loads {@link MapFile}s and notifies the {@link CollisionManager} of tiles which are blocked, + * or on a bridge. + */ +public final class WorldMapDecoder implements Runnable { + + /** + * A bit flag that indicates that the tile at the current Position is blocked. + */ + private static final int BLOCKED_TILE = 0x1; + + /** + * A bit flag that indicates that the tile at the current Position is a bridge tile. + */ + private static final int BRIDGE_TILE = 0x2; + + /** + * The {@link IndexedFileSystem}. + */ + private IndexedFileSystem fs; + + /** + * The {@link CollisionManager} to notify of bridged / blocked tiles. + */ + private CollisionManager collisionManager; + + /** + * Create a new {@link WorldMapDecoder}. + * + * @param fs The {@link IndexedFileSystem} to load {@link MapFile}s. from. + * @param collisionManager The {@link CollisionManager} to register tiles with. + */ + public WorldMapDecoder(IndexedFileSystem fs, CollisionManager collisionManager) { + this.fs = fs; + this.collisionManager = collisionManager; + } + + /** + * Decode all {@link MapFile}s and notify the {@link CollisionManager} of any tiles that are + * flagged as blocked or on a bridge. + */ + @Override + public void run() { + Map mapIndices = MapIndex.getIndices(); + + try { + for (MapIndex index : mapIndices.values()) { + MapFileDecoder decoder = MapFileDecoder.create(fs, index); + MapFile mapFile = decoder.decode(); + MapPlane[] mapPlanes = mapFile.getPlanes(); + + int mapX = index.getX(), mapY = index.getY(); + for (MapPlane plane : mapPlanes) { + markTiles(mapX, mapY, plane); + } + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Mark any tiles in the given {@link MapPlane} as blocked or bridged in the {@link CollisionManager}. + * + * @param mapX The X coordinate of the map file. + * @param mapY The Y coordinate of the map file. + * @param plane The {@link MapPlane} to load tiles from. + */ + private void markTiles(int mapX, int mapY, MapPlane plane) { + for (int x = 0; x < MapConstants.MAP_WIDTH; x++) { + for (int y = 0; y < MapConstants.MAP_WIDTH; y++) { + Tile tile = plane.getTile(x, y); + Position position = new Position(mapX + x, mapY + y, plane.getLevel()); + + if ((tile.getAttributes() & BLOCKED_TILE) == BLOCKED_TILE) { + collisionManager.markBlocked(position); + } + + if ((tile.getAttributes() & BRIDGE_TILE) == BRIDGE_TILE) { + collisionManager.markBridged(position); + } + } + } + } +} diff --git a/game/src/main/org/apollo/game/fs/decoder/WorldObjectsDecoder.java b/game/src/main/org/apollo/game/fs/decoder/WorldObjectsDecoder.java new file mode 100644 index 00000000..2b8d01eb --- /dev/null +++ b/game/src/main/org/apollo/game/fs/decoder/WorldObjectsDecoder.java @@ -0,0 +1,78 @@ +package org.apollo.game.fs.decoder; + +import org.apollo.cache.IndexedFileSystem; +import org.apollo.cache.map.MapIndex; +import org.apollo.cache.map.MapObject; +import org.apollo.cache.map.MapObjectsDecoder; +import org.apollo.game.model.Position; +import org.apollo.game.model.World; +import org.apollo.game.model.area.Region; +import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.entity.obj.StaticGameObject; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Map; + +/** + * A decoder which decodes {@link MapObject}s and registers them with the game world. + */ +public final class WorldObjectsDecoder implements Runnable { + /** + * The IndexedFileSystem. + */ + private final IndexedFileSystem fs; + + /** + * The {@link RegionRepository} to lookup {@link Region}s from. + */ + private final RegionRepository regionRepository; + + /** + * The {@link World} to register {@link StaticGameObject}s with. + */ + private final World world; + + /** + * Create a new {@link WorldObjectsDecoder}. + * + * @param fs The {@link IndexedFileSystem} to load object files from. + * @param world The {@link World} to register objects with. + * @param regionRepository The {@link RegionRepository} to lookup {@link Region}s from. + */ + public WorldObjectsDecoder(IndexedFileSystem fs, World world, RegionRepository regionRepository) { + this.fs = fs; + this.world = world; + this.regionRepository = regionRepository; + } + + /** + * Decode the {@code MapObject}s from the cache and register them with the world. + */ + @Override + public void run() { + Map mapIndices = MapIndex.getIndices(); + + try { + for (MapIndex index : mapIndices.values()) { + MapObjectsDecoder decoder = MapObjectsDecoder.create(fs, index); + List objects = decoder.decode(); + + int mapX = index.getX(), mapY = index.getY(); + + for (MapObject object : objects) { + Position position = new Position(mapX + object.getLocalX(), mapY + object.getLocalY(), + object.getHeight()); + + StaticGameObject gameObject = new StaticGameObject(world, object.getId(), position, + object.getType(), object.getOrientation()); + + regionRepository.fromPosition(position).addEntity(gameObject, false); + } + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } +} diff --git a/game/src/main/org/apollo/game/model/Direction.java b/game/src/main/org/apollo/game/model/Direction.java index 85e49902..dc03c558 100644 --- a/game/src/main/org/apollo/game/model/Direction.java +++ b/game/src/main/org/apollo/game/model/Direction.java @@ -57,6 +57,23 @@ public enum Direction { */ public static final Direction[] EMPTY_DIRECTION_ARRAY = new Direction[0]; + /** + * An array of directions without any diagonal directions. + */ + public final static Direction[] NESW = { NORTH, EAST, SOUTH, WEST }; + + /** + * An array of directions without any diagonal directions, and one step counter-clockwise, as used by + * the clients collision mapping. + */ + public final static Direction[] WNES = { WEST, NORTH, EAST, SOUTH }; + + /** + * An array of diagonal directions, and one step counter-clockwise, as used by the clients collision + * mapping. + */ + public final static Direction[] WNES_DIAGONAL = { NORTH_WEST, NORTH_EAST, SOUTH_EAST, SOUTH_WEST}; + /** * Gets the Direction between the two {@link Position}s.. * @@ -68,6 +85,17 @@ public enum Direction { int deltaX = next.getX() - current.getX(); int deltaY = next.getY() - current.getY(); + return fromDeltas(deltaX, deltaY); + } + + /** + * Creates a direction from the differences between X and Y. + * + * @param deltaX The difference between two X coordinates. + * @param deltaY The difference between two Y coordinates. + * @return The direction. + */ + public static Direction fromDeltas(int deltaX, int deltaY) { if (deltaY == 1) { if (deltaX == 1) { return NORTH_EAST; @@ -97,6 +125,27 @@ public enum Direction { throw new IllegalArgumentException("Difference between Positions must be [-1, 1]."); } + /** + * Get the 2 directions which make up a diagonal direction (i.e., NORTH and EAST for NORTH_EAST). + * + * @param direction The direction to get the components for. + * @return The components for the given direction. + */ + public static Direction[] diagonalComponents(Direction direction) { + switch (direction) { + case NORTH_EAST: + return new Direction[] { NORTH, EAST }; + case NORTH_WEST: + return new Direction[] { NORTH, WEST }; + case SOUTH_EAST: + return new Direction[] { SOUTH, EAST }; + case SOUTH_WEST: + return new Direction[] { SOUTH, WEST }; + } + + throw new IllegalArgumentException("Must provide a diagonal direction."); + } + /** * The direction as an integer. */ @@ -111,6 +160,83 @@ public enum Direction { this.intValue = intValue; } + /** + * Gets the opposite direction of the this direction. + * + * @return The opposite direction. + */ + public Direction opposite() { + switch (this) { + case NORTH: + return SOUTH; + case SOUTH: + return NORTH; + case EAST: + return WEST; + case WEST: + return EAST; + case NORTH_WEST: + return SOUTH_EAST; + case NORTH_EAST: + return SOUTH_WEST; + case SOUTH_EAST: + return NORTH_WEST; + case SOUTH_WEST: + return NORTH_EAST; + } + + return NONE; + } + + /** + * Gets the X delta from a {@link Position} of (0, 0). + * + * @return The delta of X from (0, 0). + */ + public int deltaX() { + switch (this) { + case SOUTH_EAST: + case NORTH_EAST: + case EAST: + return 1; + case SOUTH_WEST: + case NORTH_WEST: + case WEST: + return -1; + } + + return 0; + } + + /** + * Gets the Y delta from a {@link Position} of (0, 0). + * + * @return The delta of Y from (0, 0). + */ + public int deltaY() { + switch (this) { + case NORTH_WEST: + case NORTH_EAST: + case NORTH: + return 1; + case SOUTH_WEST: + case SOUTH_EAST: + case SOUTH: + return -1; + } + + return 0; + } + + /** + * Check if this direction is a diagonal direction. + * + * @return {@code true} if this direction is a diagonal direction, {@code false} otherwise. + */ + public boolean isDiagonal() { + return this == SOUTH_EAST || this == SOUTH_WEST || this == NORTH_EAST || this == NORTH_WEST; + } + /** * Gets the direction as an integer which the client can understand. * @@ -120,4 +246,28 @@ public enum Direction { return intValue; } + /** + * Gets the direction as an integer as used orientation in the client maps (WNES as opposed to NESW). + * + * @return The direction as an integer. + */ + public int toOrientationInteger() { + switch(this) { + case WEST: + case NORTH_WEST: + return 0; + case NORTH: + case NORTH_EAST: + return 1; + case EAST: + case SOUTH_EAST: + return 2; + case SOUTH: + case SOUTH_WEST: + return 3; + default: + throw new IllegalStateException("Only a valid direction can have an orientation value"); + } + + } } \ No newline at end of file diff --git a/game/src/main/org/apollo/game/model/Position.java b/game/src/main/org/apollo/game/model/Position.java index 45f3c9fc..e68241a0 100644 --- a/game/src/main/org/apollo/game/model/Position.java +++ b/game/src/main/org/apollo/game/model/Position.java @@ -224,9 +224,19 @@ public final class Position { return deltaX <= distance && deltaY <= distance && getHeight() == other.getHeight(); } - @Override - public String toString() { - return MoreObjects.toStringHelper(this).add("x", getX()).add("y", getY()).add("height", getHeight()).add("region", getRegionCoordinates()).toString(); + /** + * Creates a new position {@code num} steps from this position in the given direction. + * + * @param num The number of steps to make. + * @param direction The direction to make steps in. + * @return A new {@code Position} that is {@code num} steps in {@code direction} ahead of this one. + */ + public Position step(int num, Direction direction) { + return new Position(getX() + (num * direction.deltaX()), getY() + (num * direction.deltaY()), getHeight()); } + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("x", getX()).add("y", getY()).add("height", getHeight()).add("map", getRegionCoordinates()).toString(); + } } \ No newline at end of file diff --git a/game/src/main/org/apollo/game/model/World.java b/game/src/main/org/apollo/game/model/World.java index 09ca1962..0b10e0cf 100644 --- a/game/src/main/org/apollo/game/model/World.java +++ b/game/src/main/org/apollo/game/model/World.java @@ -1,10 +1,6 @@ package org.apollo.game.model; -import java.util.ArrayDeque; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Queue; +import java.util.*; import java.util.logging.Logger; import com.google.common.base.Preconditions; @@ -12,12 +8,17 @@ import org.apollo.Service; import org.apollo.cache.IndexedFileSystem; import org.apollo.cache.decoder.ItemDefinitionDecoder; import org.apollo.cache.decoder.NpcDefinitionDecoder; +import org.apollo.cache.decoder.ObjectDefinitionDecoder; +import org.apollo.cache.map.MapIndexDecoder; import org.apollo.game.command.CommandDispatcher; -import org.apollo.game.fs.decoder.GameObjectDecoder; import org.apollo.game.fs.decoder.SynchronousDecoder; +import org.apollo.game.fs.decoder.WorldMapDecoder; +import org.apollo.game.fs.decoder.WorldObjectsDecoder; import org.apollo.game.io.EquipmentDefinitionParser; import org.apollo.game.model.area.Region; import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionManager; +import org.apollo.game.model.area.collision.GameObjectCollisionUpdateListener; import org.apollo.game.model.entity.Entity; import org.apollo.game.model.entity.EntityType; import org.apollo.game.model.entity.MobRepository; @@ -110,6 +111,11 @@ public final class World { */ private final RegionRepository regions = RegionRepository.immutable(); + /** + * This world's {@link CollisionManager}. + */ + private final CollisionManager collisionManager = new CollisionManager(regions); + /** * The scheduler. */ @@ -130,6 +136,13 @@ public final class World { */ private int releaseNumber; + /** + * Gets the collision manager. + * + * @return The collision manager + */ + public CollisionManager getCollisionManager() { return collisionManager; } + /** * Gets the command dispatcher. * @@ -207,13 +220,28 @@ public final class World { public void init(int release, IndexedFileSystem fs, PluginManager manager) throws Exception { releaseNumber = release; - SynchronousDecoder decoder = new SynchronousDecoder(new ItemDefinitionDecoder(fs), - new NpcDefinitionDecoder(fs), new GameObjectDecoder(fs, this), - EquipmentDefinitionParser.fromFile("data/equipment-" + release + "" + ".dat")); + SynchronousDecoder firstStageDecoder = new SynchronousDecoder( + new NpcDefinitionDecoder(fs), + new ItemDefinitionDecoder(fs), + new ObjectDefinitionDecoder(fs), + new MapIndexDecoder(fs), + EquipmentDefinitionParser.fromFile("data/equipment-" + release + "" + ".dat") + ); - decoder.block(); + firstStageDecoder.block(); - npcMovement = new NpcMovementTask(regions); // Must be exactly here because of ordering issues. + SynchronousDecoder secondStageDecoder = new SynchronousDecoder( + new WorldObjectsDecoder(fs, this, regions), + new WorldMapDecoder(fs, collisionManager) + ); + + secondStageDecoder.block(); + + // Build collision matrices for the first time + collisionManager.build(false); + regions.addRegionListener(new GameObjectCollisionUpdateListener(collisionManager)); + + npcMovement = new NpcMovementTask(collisionManager); // Must be exactly here because of ordering issues. scheduler.schedule(npcMovement); manager.start(); diff --git a/game/src/main/org/apollo/game/model/area/Region.java b/game/src/main/org/apollo/game/model/area/Region.java index 9b2f4a8c..39b54af7 100644 --- a/game/src/main/org/apollo/game/model/area/Region.java +++ b/game/src/main/org/apollo/game/model/area/Region.java @@ -300,6 +300,15 @@ public final class Region { return matrices[height]; } + /** + * Gets all {@link CollisionMatrix}'s in this {@code Region}. + * + * @return The collision matrices of this region. + */ + public CollisionMatrix[] getMatrices() { + return matrices; + } + /** * Gets the {@link Set} of {@link RegionUpdateMessage}s that have occurred in the last pulse. This method can * only be called once per pulse. diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionFlag.java b/game/src/main/org/apollo/game/model/area/collision/CollisionFlag.java index 33b19bfe..b4a813b7 100644 --- a/game/src/main/org/apollo/game/model/area/collision/CollisionFlag.java +++ b/game/src/main/org/apollo/game/model/area/collision/CollisionFlag.java @@ -9,45 +9,85 @@ import org.apollo.game.model.entity.EntityType; */ public enum CollisionFlag { + /** + * The walk north west flag. + */ + MOB_NORTH_WEST(1), + /** * The walk north flag. */ - MOB_NORTH(0), + MOB_NORTH(2), + + /** + * The walk north east flag. + */ + MOB_NORTH_EAST(3), /** * The walk east flag. */ - MOB_EAST(1), + MOB_EAST(4), + + /** + * The walk south east flag. + */ + MOB_SOUTH_EAST(5), /** * The walk south flag. */ - MOB_SOUTH(2), + MOB_SOUTH(6), + + /** + * The walk south west flag. + */ + MOB_SOUTH_WEST(7), /** * The walk west flag. */ - MOB_WEST(3), + MOB_WEST(8), + + /** + * The projectile north west flag. + */ + PROJECTILE_NORTH_WEST(9), /** * The projectile north flag. */ - PROJECTILE_NORTH(4), + PROJECTILE_NORTH(10), + + /** + * The projectile north east flag. + */ + PROJECTILE_NORTH_EAST(11), /** * The projectile east flag. */ - PROJECTILE_EAST(5), + PROJECTILE_EAST(12), + + /** + * The projectile south east flag. + */ + PROJECTILE_SOUTH_EAST(13), /** * The projectile south flag. */ - PROJECTILE_SOUTH(6), + PROJECTILE_SOUTH(14), + + /** + * The projectile south west flag. + */ + PROJECTILE_SOUTH_WEST(15), /** * The projectile west flag. */ - PROJECTILE_WEST(7); + PROJECTILE_WEST(16); /** * Returns an array of CollisionFlags that indicate if the specified {@link EntityType} can be positioned on a tile. @@ -65,7 +105,16 @@ public enum CollisionFlag { * @return The array of CollisionFlags. */ public static CollisionFlag[] mobs() { - return new CollisionFlag[] { MOB_NORTH, MOB_EAST, MOB_SOUTH, MOB_WEST }; + return new CollisionFlag[] { + MOB_NORTH_WEST, + MOB_NORTH, + MOB_NORTH_EAST, + MOB_WEST, + MOB_EAST, + MOB_SOUTH_WEST, + MOB_SOUTH, + MOB_SOUTH_EAST + }; } /** @@ -74,7 +123,16 @@ public enum CollisionFlag { * @return The array of CollisionFlags. */ public static CollisionFlag[] projectiles() { - return new CollisionFlag[] { PROJECTILE_NORTH, PROJECTILE_EAST, PROJECTILE_SOUTH, PROJECTILE_WEST }; + return new CollisionFlag[] { + PROJECTILE_NORTH_WEST, + PROJECTILE_NORTH, + PROJECTILE_NORTH_EAST, + PROJECTILE_WEST, + PROJECTILE_EAST, + PROJECTILE_SOUTH_WEST, + PROJECTILE_SOUTH, + PROJECTILE_SOUTH_EAST + }; } /** @@ -92,12 +150,12 @@ public enum CollisionFlag { } /** - * Gets this CollisionFlag, as a {@code byte}. + * Gets this CollisionFlag, as a {@code short}. * - * @return The value, as a {@code byte}. + * @return The value, as a {@code short}. */ - public byte asByte() { - return (byte) (1 << bit); + public short asShort() { + return (short) (1 << bit); } /** diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java b/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java new file mode 100644 index 00000000..8b4d8ccc --- /dev/null +++ b/game/src/main/org/apollo/game/model/area/collision/CollisionManager.java @@ -0,0 +1,212 @@ +package org.apollo.game.model.area.collision; + +import org.apollo.game.model.Direction; +import org.apollo.game.model.Position; +import org.apollo.game.model.area.Region; +import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionUpdate.DirectionFlag; +import org.apollo.game.model.entity.EntityType; +import org.apollo.game.model.entity.obj.GameObject; + +import java.util.*; + +/** + * Manages applying {@link CollisionUpdate}s to the respective {@link CollisionMatrix} instances, and keeping + * a record of collision state (i.e., which tiles are bridged). + */ +public final class CollisionManager { + /** + * A comparator which sorts {@link Position}s by their X coordinate, then Y, then height. + */ + private static final Comparator POSITION_COMPARATOR = + Comparator.comparingInt(Position::getX).thenComparingInt(Position::getY).thenComparingInt(Position::getHeight); + + /** + * A {@code SortedSet} of positions where the tile is part of a bridged structure. + */ + private final SortedSet bridgeTiles = new TreeSet<>(POSITION_COMPARATOR); + + /** + * A {@code SortedSet} of positions where the tile is completely blocked. + */ + private final SortedSet blockedTiles = new TreeSet<>(POSITION_COMPARATOR); + + /** + * The {@link RegionRepository} containing {@link Region}s, used to lookup {@link CollisionMatrix}'s. + */ + private final RegionRepository regionRepository; + + /** + * Creates a new {@code CollisionManager}. + * + * @param regionRepository The {@link RegionRepository} to lookup {@link Region} and {@link CollisionMatrix} instances + * from. + */ + public CollisionManager(RegionRepository regionRepository) { + this.regionRepository = regionRepository; + } + + /** + * Apples the first initial {@link CollisionUpdate} to the {@link CollisionMatrix}es for all objects and tiles loaded from + * the cache. + * + * @param rebuilding A flag indicating whether {@link CollisionMatrix}es are being rebuilt, or built for the first time. + */ + public void build(boolean rebuilding) { + if (rebuilding) { + for (Region region : regionRepository.getRegions()) { + for (CollisionMatrix matrix : region.getMatrices()) { + matrix.reset(); + } + } + } + + CollisionUpdate.Builder tileUpdateBuilder = new CollisionUpdate.Builder(); + tileUpdateBuilder.type(CollisionUpdateType.ADDING); + + for (Position tile : blockedTiles) { + int x = tile.getX(), y = tile.getY(); + int height = tile.getHeight(); + + if (bridgeTiles.contains(new Position(x, y, 1))) { + height--; + } + + if (height >= 0) { + tileUpdateBuilder.tile(new Position(x, y, height), false, Direction.NESW); + } + } + + CollisionUpdate tileUpdate = tileUpdateBuilder.build(); + apply(tileUpdate); + + for (Region region : regionRepository.getRegions()) { + CollisionUpdate.Builder regionObjectsUpdateBuilder = new CollisionUpdate.Builder(); + regionObjectsUpdateBuilder.type(CollisionUpdateType.ADDING); + + region.getEntities(EntityType.STATIC_OBJECT, EntityType.DYNAMIC_OBJECT) + .forEach(entity -> regionObjectsUpdateBuilder.object((GameObject) entity)); + + CollisionUpdate regionObjectsUpdate = regionObjectsUpdateBuilder.build(); + apply(regionObjectsUpdate); + } + } + + /** + * Apply a {@link CollisionUpdate} to the game world. + * + * @param update The update to apply. + */ + public void apply(CollisionUpdate update) { + Region prev = null; + + CollisionUpdateType type = update.getType(); + Map> flags = update.getFlags().asMap(); + + for (Map.Entry> flag : flags.entrySet()) { + Position position = flag.getKey(); + Collection directionFlags = flag.getValue(); + + int height = position.getHeight(); + if (bridgeTiles.contains(new Position(position.getX(), position.getY(), 1))) { + if (--height < 0) { + continue; + } + } + + if (prev == null || !prev.contains(position)) { + prev = regionRepository.fromPosition(position); + } + + int localX = position.getX() % Region.SIZE, localY = position.getY() % Region.SIZE; + + CollisionMatrix matrix = prev.getMatrix(height); + CollisionFlag[] mobs = CollisionFlag.mobs(); + CollisionFlag[] projectiles = CollisionFlag.projectiles(); + + for (DirectionFlag directionFlag : directionFlags) { + Direction direction = directionFlag.getDirection(); + if (direction == Direction.NONE) { + continue; + } + + int orientation = direction.toInteger(); + if (directionFlag.isImpenetrable()) { + flag(type, matrix, localX, localY, projectiles[orientation]); + } + + flag(type, matrix, localX, localY, mobs[orientation]); + } + } + } + + /** + * Apply a {@link CollisionUpdate} flag to a {@link CollisionMatrix}. + * + * @param type The type of update to apply. + * @param matrix The matrix the update is being applied to. + * @param localX The local X position of the tile the flag represents. + * @param localY The local Y position of the tile the flag represents. + * @param flag The flag to update. + */ + private void flag(CollisionUpdateType type, CollisionMatrix matrix, int localX, int localY, CollisionFlag flag) { + if (type == CollisionUpdateType.ADDING) { + matrix.flag(localX, localY, flag); + } else { + matrix.clear(localX, localY, flag); + } + } + + /** + * Marks a tile as completely untraversable from all directions. + * + * @param position The {@link Position} of the tile. + */ + public void markBlocked(Position position) { + blockedTiles.add(position); + } + + /** + * Marks a tile as part of a bridge. + * + * @param position The {@link Position} of the tile. + */ + public void markBridged(Position position) { + bridgeTiles.add(position); + } + + /** + * Checks if the given {@link EntityType} can traverse to the next tile from {@code position} in the given + * {@code direction}. + * + * @param position The current position of the entity. + * @param type The type of the entity. + * @param direction The direction the entity is travelling. + * @return {@code true} if next tile is traversable, {@code false} otherwise. + */ + public boolean traversable(Position position, EntityType type, Direction direction) { + Position next = position.step(1, direction); + Region region = regionRepository.fromPosition(next); + + if (!region.traversable(next, type, direction)) { + return false; + } + + if (direction.isDiagonal()) { + for (Direction component : Direction.diagonalComponents(direction)) { + next = position.step(1, component); + + if (!region.contains(next)) { + region = regionRepository.fromPosition(next); + } + + if (!region.traversable(next, type, component)) { + return false; + } + } + } + + return true; + } + +} diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionMatrix.java b/game/src/main/org/apollo/game/model/area/collision/CollisionMatrix.java index d08a528b..86d755f6 100644 --- a/game/src/main/org/apollo/game/model/area/collision/CollisionMatrix.java +++ b/game/src/main/org/apollo/game/model/area/collision/CollisionMatrix.java @@ -18,12 +18,17 @@ public final class CollisionMatrix { /** * Indicates that all types of traversal are allowed. */ - private static final byte ALL_ALLOWED = 0b0000_0000; + private static final short ALL_ALLOWED = 0b00000000_00000000; /** * Indicates that no types of traversal are allowed. */ - private static final byte ALL_BLOCKED = (byte) 0b1111_1111; + private static final short ALL_BLOCKED = (short) 0b11111111_11111111; + + /** + * Indicates that projectiles may traverse this tile, but mobs may not. + */ + private static final short ALL_MOBS_BLOCKED = (short) 0b11111111_00000000; /** * Creates an array of CollisionMatrix objects, all of the specified width and length. @@ -45,9 +50,9 @@ public final class CollisionMatrix { private final int length; /** - * The collision matrix, as a {@code byte} array. + * The collision matrix, as a {@code short} array. */ - private final byte[] matrix; + private final short[] matrix; /** * The width of the matrix. @@ -63,7 +68,7 @@ public final class CollisionMatrix { public CollisionMatrix(int width, int length) { this.width = width; this.length = length; - matrix = new byte[width * length]; + matrix = new short[width * length]; } /** @@ -123,7 +128,7 @@ public final class CollisionMatrix { * @param y The y coordinate. */ public void block(int x, int y) { - set(x, y, ALL_BLOCKED); + block(x, y, true); } /** @@ -135,7 +140,18 @@ public final class CollisionMatrix { * @param flag The CollisionFlag. */ public void clear(int x, int y, CollisionFlag flag) { - set(x, y, (byte) (matrix[indexOf(x, y)] & ~flag.asByte())); + set(x, y, (short) (matrix[indexOf(x, y)] & ~flag.asShort())); + } + + /** + * Adds an additional {@link CollisionFlag} for the specified coordinate pair. + * + * @param x The x coordinate. + * @param y The y coordinate. + * @param flag The CollisionFlag. + */ + public void flag(int x, int y, CollisionFlag flag) { + matrix[indexOf(x, y)] |= flag.asShort(); } /** @@ -147,7 +163,7 @@ public final class CollisionMatrix { * @return {@code true} iff the CollisionFlag is set. */ public boolean flagged(int x, int y, CollisionFlag flag) { - return (get(x, y) & flag.asByte()) != 0; + return (get(x, y) & flag.asShort()) != 0; } /** @@ -158,7 +174,7 @@ public final class CollisionMatrix { * @return The value. */ public int get(int x, int y) { - return matrix[indexOf(x, y)] & 0xFF; + return matrix[indexOf(x, y)] & 0xFFFF; } /** @@ -171,6 +187,17 @@ public final class CollisionMatrix { set(x, y, ALL_ALLOWED); } + /** + * Resets all cells in this matrix. + */ + public void reset() { + for (int x = 0; x < width; x++) { + for (int y = 0; y < width; y++) { + reset(x, y); + } + } + } + /** * Sets (i.e. sets to {@code true}) the value of the specified {@link CollisionFlag} for the specified coordinate * pair. @@ -180,13 +207,13 @@ public final class CollisionMatrix { * @param flag The CollisionFlag. */ public void set(int x, int y, CollisionFlag flag) { - set(x, y, flag.asByte()); + set(x, y, flag.asShort()); } @Override public String toString() { return MoreObjects.toStringHelper(this).add("width", width).add("length", length) - .add("matrix", Arrays.toString(matrix)).toString(); + .add("matrix", Arrays.toString(matrix)).toString(); } /** @@ -201,23 +228,23 @@ public final class CollisionMatrix { */ public boolean untraversable(int x, int y, EntityType entity, Direction direction) { CollisionFlag[] flags = CollisionFlag.forType(entity); - int north = 0, east = 1, south = 2, west = 3; + int northwest = 0, north = 1, northeast = 2, west = 3, east = 4, southwest = 5, south = 6, southeast = 7; switch (direction) { case NORTH_WEST: - return flagged(x, y, flags[south]) || flagged(x, y, flags[east]); + return flagged(x, y, flags[southeast]) || flagged(x, y, flags[south]) || flagged(x, y, flags[east]); case NORTH: return flagged(x, y, flags[south]); case NORTH_EAST: - return flagged(x, y, flags[south]) || flagged(x, y, flags[west]); + return flagged(x, y, flags[southwest]) || flagged(x, y, flags[south]) || flagged(x, y, flags[west]); case EAST: return flagged(x, y, flags[west]); case SOUTH_EAST: - return flagged(x, y, flags[north]) || flagged(x, y, flags[west]); + return flagged(x, y, flags[northwest]) || flagged(x, y, flags[north]) || flagged(x, y, flags[west]); case SOUTH: return flagged(x, y, flags[north]); case SOUTH_WEST: - return flagged(x, y, flags[north]) || flagged(x, y, flags[east]); + return flagged(x, y, flags[northeast]) || flagged(x, y, flags[north]) || flagged(x, y, flags[east]); case WEST: return flagged(x, y, flags[east]); default: @@ -246,7 +273,7 @@ public final class CollisionMatrix { * @param y The y coordinate. * @param value The value. */ - private void set(int x, int y, byte value) { + private void set(int x, int y, short value) { matrix[indexOf(x, y)] = value; } diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java b/game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java new file mode 100644 index 00000000..21225f00 --- /dev/null +++ b/game/src/main/org/apollo/game/model/area/collision/CollisionUpdate.java @@ -0,0 +1,248 @@ +package org.apollo.game.model.area.collision; + +import com.google.common.base.Preconditions; +import com.google.common.collect.*; +import org.apollo.cache.def.ObjectDefinition; +import org.apollo.game.model.Direction; +import org.apollo.game.model.Position; +import org.apollo.game.model.entity.obj.GameObject; +import org.apollo.game.model.entity.obj.ObjectType; + +import java.util.stream.Stream; + +/** + * A global update to the collision matrices. + */ +public final class CollisionUpdate { + /** + * The type of this update. + */ + private final CollisionUpdateType type; + + /** + * A mapping of {@link Position}s to a set of their {@link DirectionFlag}s. + */ + private final Multimap flags; + + public CollisionUpdate(CollisionUpdateType type, Multimap flags) { + this.type = type; + this.flags = flags; + } + + /** + * Get the type of this update (ADDING, or REMOVING). + * + * @return The type of this update. + */ + public CollisionUpdateType getType() { + return type; + } + + /** + * Get the mapping of tiles -> flags contained in this update. + * + * @return The flags contained in this update. + */ + public Multimap getFlags() { + return flags; + } + + /** + * A directional flag in a {@code CollisionUpdate}. Consists of a {@code direction} and a flag indicating whether + * that tile is impenetrable as well as untraversable. + */ + public static final class DirectionFlag { + private final boolean impenetrable; + private final Direction direction; + + public DirectionFlag(boolean impenetrable, Direction direction) { + this.impenetrable = impenetrable; + this.direction = direction; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DirectionFlag that = (DirectionFlag) o; + + if (impenetrable != that.impenetrable) return false; + return direction == that.direction; + + } + + @Override + public int hashCode() { + int result = (impenetrable ? 1 : 0); + result = 31 * result + direction.hashCode(); + return result; + } + + /** + * Check if this flag represents an impenetrable direction. + * + * @return {@code true} if this flag represents an impenetrable direction, {@code false} otherwise. + */ + public boolean isImpenetrable() { + return impenetrable; + } + + /** + * Get the direction this flag represents. + * + * @return The direction this flag represents. + */ + public Direction getDirection() { + return direction; + } + } + + public static class Builder { + private final Multimap flags; + private CollisionUpdateType type; + + public Builder() { + this.flags = MultimapBuilder.hashKeys().hashSetValues().build(); + } + + /** + * Set the type of the {@link CollisionUpdate}. Can only be called once. + * + * @param type The type of collision update to use. + */ + public void type(CollisionUpdateType type) { + Preconditions.checkState(type != null, "update type has already been set"); + this.type = type; + } + + /** + * Sets the tile at the given {@code position} as untraversable in the given directions. + * + * @param position The world position of the tile. + * @param directions The directions that are untraversable from this tile. + */ + public void tile(Position position, boolean impenetrable, Direction... directions) { + if (directions.length == 0) { + return; + } + + Stream.of(directions).forEach(direction -> flags.put(position, new DirectionFlag(impenetrable, direction))); + } + + /** + * Flag a wall in the CollisionUpdate. When constructing a CollisionMatrix, the flags for a wall are represented + * as the tile the wall exists on and the tile one step in the facing direction. So for a wall facing south, + * the tile one step to the south be flagged as untraversable from the north. + * + * @param position The position of the wall. + * @param impenetrable If projectiles can pass through this wall. + * @param orientation The facing direction of this wall. + */ + public void wall(Position position, boolean impenetrable, Direction orientation) { + tile(position, impenetrable, orientation); + tile(position.step(1, orientation), impenetrable, orientation.opposite()); + } + + /** + * Flag a larger corner wall in the CollisionUpdate. A corner is represented by the 2 directions that it faces, + * and the 2 tiles in both directions. For example, when a tile is facing north its facing directions + * are north and east, so the position of the object will be untraversable from those directions. Additionally, + * the tile 1 step to the north, and 1 step to the east will be untraversable from the opposite directions of + * north and east respectively. + *

+ * todo: "large corner wall", is that really what this is? + * + * @param position The position of the corner wall. + * @param impenetrable If projectiles can pass through this corner wall. + * @param orientation The direction of this corner wall + */ + public void largeCornerWall(Position position, boolean impenetrable, Direction orientation) { + Direction[] directions = Direction.diagonalComponents(orientation); + + tile(position, impenetrable, directions); + + for (Direction direction : directions) { + tile(position.step(1, direction), impenetrable, direction.opposite()); + } + } + + /** + * Flag a collision update for the given {@link GameObject}. + * + * @param object The object to update collision flags for. + */ + public void object(GameObject object) { + ObjectDefinition definition = object.getDefinition(); + Position position = object.getPosition(); + int type = object.getType(); + + if (!unwalkable(definition, type)) { + return; + } + + int x = position.getX(), y = position.getY(), height = position.getHeight(); + int width = definition.getWidth(), length = definition.getLength(); + boolean impenetrable = definition.isImpenetrable(); + int orientation = object.getOrientation(); + + // north / south for walls, north east / south west for corners + if (orientation == 1 || orientation == 3) { + width = definition.getLength(); + length = definition.getWidth(); + } + + if (type == ObjectType.FLOOR_DECORATION.getValue()) { + if (definition.isInteractive() && definition.isSolid()) { + tile(new Position(x, y, height), impenetrable, Direction.NESW); + } + } else if (type >= ObjectType.DIAGONAL_WALL.getValue() && type < ObjectType.FLOOR_DECORATION.getValue()) { + for (int dx = 0; dx < width; dx++) { + for (int dy = 0; dy < length; dy++) { + tile(new Position(x + dx, y + dy, height), impenetrable, Direction.NESW); + } + } + } else if (type == ObjectType.LENGTHWISE_WALL.getValue()) { + wall(position, impenetrable, Direction.WNES[orientation]); + } else if (type == ObjectType.TRIANGULAR_CORNER.getValue() + || type == ObjectType.RECTANGULAR_CORNER.getValue()) { + wall(position, impenetrable, Direction.WNES_DIAGONAL[orientation]); + } else if (type == ObjectType.WALL_CORNER.getValue()) { + largeCornerWall(position, impenetrable, Direction.WNES_DIAGONAL[orientation]); + } + } + + /** + * Create a new {@link CollisionUpdate}. + * + * @return A new CollisionUpdate with the flags in this builder. + */ + public CollisionUpdate build() { + Preconditions.checkNotNull(type, "update type must not be null"); + return new CollisionUpdate(type, Multimaps.unmodifiableMultimap(flags)); + } + } + + /** + * Returns whether or not an object with the specified {@link ObjectDefinition} and {@code type} should result in + * the tile(s) it is located on being blocked. + * + * @param definition The {@link ObjectDefinition} of the object. + * @param type The type of the object. + * @return {@code true} iff the tile(s) the object is on should be blocked. + */ + private static boolean unwalkable(ObjectDefinition definition, int type) { + boolean isSolidFloorDecoration = type == ObjectType.FLOOR_DECORATION.getValue() && definition.isInteractive(); + + boolean isWall = type >= ObjectType.LENGTHWISE_WALL.getValue() + && type <= ObjectType.RECTANGULAR_CORNER.getValue() || type == ObjectType.DIAGONAL_WALL.getValue(); + + boolean isRoof = type > ObjectType.DIAGONAL_INTERACTABLE.getValue() + && type < ObjectType.FLOOR_DECORATION.getValue(); + + boolean isSolidInteractable = (type == ObjectType.DIAGONAL_INTERACTABLE.getValue() + || type == ObjectType.INTERACTABLE.getValue()) && definition.isSolid(); + + return isWall || isRoof || isSolidInteractable || isSolidFloorDecoration; + } +} diff --git a/game/src/main/org/apollo/game/model/area/collision/CollisionUpdateType.java b/game/src/main/org/apollo/game/model/area/collision/CollisionUpdateType.java new file mode 100644 index 00000000..a7de9934 --- /dev/null +++ b/game/src/main/org/apollo/game/model/area/collision/CollisionUpdateType.java @@ -0,0 +1,16 @@ +package org.apollo.game.model.area.collision; + +/** + * An enum which represents the type of a {@link CollisionUpdate}. + */ +public enum CollisionUpdateType { + /** + * Indicates that a {@link CollisionUpdate} will be adding new flags to collision matrices. + */ + ADDING, + + /** + * Indicates that a {@link CollisionUpdate} will be clearing existing flags from collision matrices. + */ + REMOVING +} diff --git a/game/src/main/org/apollo/game/model/area/collision/GameObjectCollisionUpdateListener.java b/game/src/main/org/apollo/game/model/area/collision/GameObjectCollisionUpdateListener.java new file mode 100644 index 00000000..89c536cd --- /dev/null +++ b/game/src/main/org/apollo/game/model/area/collision/GameObjectCollisionUpdateListener.java @@ -0,0 +1,49 @@ +package org.apollo.game.model.area.collision; + +import org.apollo.game.model.area.EntityUpdateType; +import org.apollo.game.model.area.Region; +import org.apollo.game.model.area.RegionListener; +import org.apollo.game.model.entity.Entity; +import org.apollo.game.model.entity.EntityType; +import org.apollo.game.model.entity.obj.GameObject; + +/** + * A {@link RegionListener} which listens on object addition / removal events and applies + * the respective {@link CollisionUpdate}. + */ +public final class GameObjectCollisionUpdateListener implements RegionListener { + /** + * The {@link CollisionManager} to apply updates to. + */ + private CollisionManager collisionManager; + + /** + * Create a new {@link GameObjectCollisionUpdateListener}. + * + * @param collisionManager The {@link CollisionManager} that collision updates will be applied to. + */ + public GameObjectCollisionUpdateListener(CollisionManager collisionManager) { + this.collisionManager = collisionManager; + } + + @Override + public void execute(Region region, Entity entity, EntityUpdateType type) { + EntityType entityType = entity.getEntityType(); + + if (entityType != EntityType.STATIC_OBJECT && entityType != EntityType.DYNAMIC_OBJECT) { + return; + } + + CollisionUpdate.Builder objectUpdateBuilder = new CollisionUpdate.Builder(); + if (type == EntityUpdateType.ADD) { + objectUpdateBuilder.type(CollisionUpdateType.ADDING); + } else { + objectUpdateBuilder.type(CollisionUpdateType.REMOVING); + } + + objectUpdateBuilder.object((GameObject) entity); + + CollisionUpdate objectUpdate = objectUpdateBuilder.build(); + collisionManager.apply(objectUpdate); + } +} diff --git a/game/src/main/org/apollo/game/model/entity/WalkingQueue.java b/game/src/main/org/apollo/game/model/entity/WalkingQueue.java index 5dc0bb15..f2feeac7 100644 --- a/game/src/main/org/apollo/game/model/entity/WalkingQueue.java +++ b/game/src/main/org/apollo/game/model/entity/WalkingQueue.java @@ -1,13 +1,20 @@ package org.apollo.game.model.entity; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Queue; - import org.apollo.game.model.Direction; import org.apollo.game.model.Position; +import org.apollo.game.model.World; import org.apollo.game.model.area.Region; import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionFlag; +import org.apollo.game.model.area.collision.CollisionManager; +import org.apollo.game.model.area.collision.CollisionMatrix; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Deque; +import java.util.Queue; +import java.util.logging.Logger; +import java.util.stream.Collectors; /** * A queue of {@link Direction}s which a {@link Mob} will follow. @@ -126,19 +133,33 @@ public final class WalkingQueue { Direction firstDirection = Direction.NONE; Direction secondDirection = Direction.NONE; + World world = mob.getWorld(); + CollisionManager collisionManager = world.getCollisionManager(); Position next = points.poll(); if (next != null) { - previousPoints.add(next); firstDirection = Direction.between(position, next); - position = new Position(next.getX(), next.getY(), height); - if (running) { - next = points.poll(); - if (next != null) { - previousPoints.add(next); - secondDirection = Direction.between(position, next); - position = new Position(next.getX(), next.getY(), height); + if (!collisionManager.traversable(position, EntityType.NPC, firstDirection)) { + clear(); + firstDirection = Direction.NONE; + } else { + previousPoints.add(next); + position = new Position(next.getX(), next.getY(), height); + + if (running) { + next = points.poll(); + if (next != null) { + secondDirection = Direction.between(position, next); + + if (!collisionManager.traversable(position, EntityType.NPC, secondDirection)) { + clear(); + secondDirection = Direction.NONE; + } else { + previousPoints.add(next); + position = new Position(next.getX(), next.getY(), height); + } + } } } } diff --git a/game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java b/game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java index 6d69626a..0f2f1bc7 100644 --- a/game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java +++ b/game/src/main/org/apollo/game/model/entity/path/AStarPathfindingAlgorithm.java @@ -1,142 +1,145 @@ -package org.apollo.game.model.entity.path; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.PriorityQueue; -import java.util.Queue; -import java.util.Set; - -import org.apollo.game.model.Direction; -import org.apollo.game.model.Position; -import org.apollo.game.model.area.RegionRepository; - -/** - * A {@link PathfindingAlgorithm} that utilises the A* algorithm to find a solution. - *

- * This implementation utilises a {@link PriorityQueue} of open {@link Node}s, in addition to the usual {@link HashSet}. - * This allows for logarithmic-time finding of the cheapest element (as opposed to the linear time associated with - * iterating over the set), whilst still maintaining the constant time contains and remove of the set. - *

- * This implementation also avoids the linear-time removal from the queue by polling until the first open node is found - * when identifying the cheapest node. - * - * @author Major - */ -public final class AStarPathfindingAlgorithm extends PathfindingAlgorithm { - - /** - * The Heuristic used by this PathfindingAlgorithm. - */ - private final Heuristic heuristic; - - /** - * Creates the A* pathfinding algorithm with the specified {@link Heuristic}. - * - * @param repository The {@link RegionRepository}. - * @param heuristic The Heuristic. - */ - public AStarPathfindingAlgorithm(RegionRepository repository, Heuristic heuristic) { - super(repository); - this.heuristic = heuristic; - } - - @Override - public Deque find(Position origin, Position target) { - Map nodes = new HashMap<>(); - Node start = new Node(origin), end = new Node(target); - nodes.put(origin, start); - nodes.put(target, end); - - Set open = new HashSet<>(); - Queue sorted = new PriorityQueue<>(); - open.add(start); - sorted.add(start); - - do { - Node active = getCheapest(sorted); - Position position = active.getPosition(); - - if (position.equals(target)) { - break; - } - - open.remove(active); - active.close(); - - int x = position.getX(), y = position.getY(); - for (int nextX = x - 1; nextX <= x + 1; nextX++) { - for (int nextY = y - 1; nextY <= y + 1; nextY++) { - if (nextX == x && nextY == y) { - continue; - } - - Position adjacent = new Position(nextX, nextY); - Direction direction = Direction.between(adjacent, position); - if (traversable(adjacent, direction)) { - Node node = nodes.computeIfAbsent(adjacent, Node::new); - compare(active, node, open, sorted, heuristic); - } - } - } - } while (!open.isEmpty()); - - Deque shortest = new ArrayDeque<>(); - Node active = end; - - if (active.hasParent()) { - Position position = active.getPosition(); - - while (!origin.equals(position)) { - shortest.addFirst(position); - active = active.getParent(); // If the target has a parent then all of the others will. - position = active.getPosition(); - } - } - - return shortest; - } - - /** - * Compares the two specified {@link Node}s, adding the other node to the open {@link Set} if the estimation is - * cheaper than the current cost. - * - * @param active The active node. - * @param other The node to compare the active node against. - * @param open The set of open nodes. - * @param sorted The sorted {@link Queue} of nodes. - * @param heuristic The {@link Heuristic} used to estimate the cost of the node. - */ - private void compare(Node active, Node other, Set open, Queue sorted, Heuristic heuristic) { - int cost = active.getCost() + heuristic.estimate(active.getPosition(), other.getPosition()); - - if (other.getCost() > cost) { - open.remove(other); - other.close(); - } else if (other.isOpen() && !open.contains(other)) { - other.setCost(cost); - other.setParent(active); - open.add(other); - sorted.add(other); - } - } - - /** - * Gets the cheapest open {@link Node} from the {@link Queue}. - * - * @param nodes The queue of nodes. - * @return The cheapest node. - */ - private Node getCheapest(Queue nodes) { - Node node = nodes.peek(); - while (!node.isOpen()) { - nodes.poll(); - node = nodes.peek(); - } - - return node; - } - +package org.apollo.game.model.entity.path; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; + +import org.apollo.game.model.Direction; +import org.apollo.game.model.Position; +import org.apollo.game.model.World; +import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionManager; + +/** + * A {@link PathfindingAlgorithm} that utilises the A* algorithm to find a solution. + *

+ * This implementation utilises a {@link PriorityQueue} of open {@link Node}s, in addition to the usual {@link HashSet}. + * This allows for logarithmic-time finding of the cheapest element (as opposed to the linear time associated with + * iterating over the set), whilst still maintaining the constant time contains and remove of the set. + *

+ * This implementation also avoids the linear-time removal from the queue by polling until the first open node is found + * when identifying the cheapest node. + * + * @author Major + */ +public final class AStarPathfindingAlgorithm extends PathfindingAlgorithm { + + /** + * The Heuristic used by this PathfindingAlgorithm. + */ + private final Heuristic heuristic; + + /** + * Creates the A* pathfinding algorithm with the specified {@link Heuristic}. + * + * @param collisionManager The {@link CollisionManager} used to check if there is a collision + * between two {@link Position}s in a path. + * @param heuristic The Heuristic. + */ + public AStarPathfindingAlgorithm(CollisionManager collisionManager, Heuristic heuristic) { + super(collisionManager); + this.heuristic = heuristic; + } + + @Override + public Deque find(Position origin, Position target) { + Map nodes = new HashMap<>(); + Node start = new Node(origin), end = new Node(target); + nodes.put(origin, start); + nodes.put(target, end); + + Set open = new HashSet<>(); + Queue sorted = new PriorityQueue<>(); + open.add(start); + sorted.add(start); + + do { + Node active = getCheapest(sorted); + Position position = active.getPosition(); + + if (position.equals(target)) { + break; + } + + open.remove(active); + active.close(); + + int x = position.getX(), y = position.getY(); + for (int nextX = x - 1; nextX <= x + 1; nextX++) { + for (int nextY = y - 1; nextY <= y + 1; nextY++) { + if (nextX == x && nextY == y) { + continue; + } + + Position adjacent = new Position(nextX, nextY); + Direction direction = Direction.between(adjacent, position); + if (traversable(adjacent, direction)) { + Node node = nodes.computeIfAbsent(adjacent, Node::new); + compare(active, node, open, sorted, heuristic); + } + } + } + } while (!open.isEmpty()); + + Deque shortest = new ArrayDeque<>(); + Node active = end; + + if (active.hasParent()) { + Position position = active.getPosition(); + + while (!origin.equals(position)) { + shortest.addFirst(position); + active = active.getParent(); // If the target has a parent then all of the others will. + position = active.getPosition(); + } + } + + return shortest; + } + + /** + * Compares the two specified {@link Node}s, adding the other node to the open {@link Set} if the estimation is + * cheaper than the current cost. + * + * @param active The active node. + * @param other The node to compare the active node against. + * @param open The set of open nodes. + * @param sorted The sorted {@link Queue} of nodes. + * @param heuristic The {@link Heuristic} used to estimate the cost of the node. + */ + private void compare(Node active, Node other, Set open, Queue sorted, Heuristic heuristic) { + int cost = active.getCost() + heuristic.estimate(active.getPosition(), other.getPosition()); + + if (other.getCost() > cost) { + open.remove(other); + other.close(); + } else if (other.isOpen() && !open.contains(other)) { + other.setCost(cost); + other.setParent(active); + open.add(other); + sorted.add(other); + } + } + + /** + * Gets the cheapest open {@link Node} from the {@link Queue}. + * + * @param nodes The queue of nodes. + * @return The cheapest node. + */ + private Node getCheapest(Queue nodes) { + Node node = nodes.peek(); + while (!node.isOpen()) { + nodes.poll(); + node = nodes.peek(); + } + + return node; + } + } \ No newline at end of file diff --git a/game/src/main/org/apollo/game/model/entity/path/PathfindingAlgorithm.java b/game/src/main/org/apollo/game/model/entity/path/PathfindingAlgorithm.java index d96007d8..dddf426e 100644 --- a/game/src/main/org/apollo/game/model/entity/path/PathfindingAlgorithm.java +++ b/game/src/main/org/apollo/game/model/entity/path/PathfindingAlgorithm.java @@ -5,8 +5,10 @@ import java.util.Optional; import org.apollo.game.model.Direction; import org.apollo.game.model.Position; +import org.apollo.game.model.World; import org.apollo.game.model.area.Region; import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionManager; import org.apollo.game.model.entity.EntityType; import com.google.common.base.Preconditions; @@ -18,18 +20,16 @@ import com.google.common.base.Preconditions; */ abstract class PathfindingAlgorithm { - /** - * The RegionRepository. - */ - private final RegionRepository repository; + private final CollisionManager collisionManager; /** * Creates the PathfindingAlgorithm. * - * @param repository The {@link RegionRepository}. + * @param collisionManager The {@link CollisionManager} used to check if there is a collision + * between two {@link Position}s in a path. */ - public PathfindingAlgorithm(RegionRepository repository) { - this.repository = repository; + public PathfindingAlgorithm(CollisionManager collisionManager) { + this.collisionManager = collisionManager; } /** @@ -84,9 +84,7 @@ abstract class PathfindingAlgorithm { x--; } - Position next = new Position(x, y, height); - Region region = repository.get(next.getRegionCoordinates()); - if (region.traversable(next, EntityType.NPC, direction) && (positions.length == 0 || inside(next, positions))) { + if (collisionManager.traversable(current, EntityType.NPC, direction)) { return true; } } diff --git a/game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java b/game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java index caf97995..421315f8 100644 --- a/game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java +++ b/game/src/main/org/apollo/game/model/entity/path/SimplePathfindingAlgorithm.java @@ -1,143 +1,146 @@ -package org.apollo.game.model.entity.path; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Optional; - -import org.apollo.game.model.Direction; -import org.apollo.game.model.Position; -import org.apollo.game.model.area.RegionRepository; - -/** - * A very simple pathfinding algorithm that simply walks in the direction of the target until it either reaches it or is - * blocked. - * - * @author Major - */ -public final class SimplePathfindingAlgorithm extends PathfindingAlgorithm { - - /** - * Creates the SimplePathfindingAlgorithm. - * - * @param repository The {@link RegionRepository}. - */ - public SimplePathfindingAlgorithm(RegionRepository repository) { - super(repository); - } - - /** - * The Optional containing the boundary Positions. - */ - private Optional boundaries = Optional.empty(); - - @Override - public Deque find(Position origin, Position target) { - int approximation = (int) (origin.getLongestDelta(target) * 1.5); - Deque positions = new ArrayDeque<>(approximation); - - return addHorizontal(origin, target, positions); - } - - /** - * Finds a valid path from the origin {@link Position} to the target one. - * - * @param origin The origin Position. - * @param target The target Position. - * @param boundaries The boundary Positions, which are marking as untraversable. - * @return The {@link Deque} containing the Positions to go through. - */ - public Deque find(Position origin, Position target, Position[] boundaries) { - this.boundaries = Optional.of(boundaries); - return find(origin, target); - } - - /** - * Adds the necessary and possible horizontal {@link Position}s to the existing {@link Deque}. - *

- * This method: - *

    - *
  • Adds positions horizontally until we are either horizontally aligned with the target, or the next step is not - * traversable. - *
  • Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable: - * if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path. - *
- * - * @param start The current position. - * @param target The target position. - * @param positions The deque of positions. - * @return The deque of positions containing the path. - */ - private Deque addHorizontal(Position start, Position target, Deque positions) { - int x = start.getX(), y = start.getY(), height = start.getHeight(); - int dx = x - target.getX(), dy = y - target.getY(); - - if (dx > 0) { - Position current = start; - - while (traversable(current, boundaries, Direction.WEST) && dx-- > 0) { - current = new Position(--x, y, height); - positions.addLast(current); - } - } else if (dx < 0) { - Position current = start; - - while (traversable(current, boundaries, Direction.EAST) && dx++ < 0) { - current = new Position(++x, y, height); - positions.addLast(current); - } - } - - Position last = new Position(x, y, height); - if (!start.equals(last) && dy != 0 && traversable(last, boundaries, dy > 0 ? Direction.SOUTH : Direction.NORTH)) { - return addVertical(last, target, positions); - } - - return positions; - } - - /** - * Adds the necessary and possible vertical {@link Position}s to the existing {@link Deque}. - *

- * This method: - *

    - *
  • Adds positions vertically until we are either vertically aligned with the target, or the next step is not - * traversable. - *
  • Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable: - * if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path. - *
- * - * @param start The current position. - * @param target The target position. - * @param positions The deque of positions. - * @return The deque of positions containing the path. - */ - private Deque addVertical(Position start, Position target, Deque positions) { - int x = start.getX(), y = start.getY(), height = start.getHeight(); - int dy = y - target.getY(), dx = x - target.getX(); - - if (dy > 0) { - Position current = start; - - while (traversable(current, boundaries, Direction.SOUTH) && dy-- > 0) { - current = new Position(x, --y, height); - positions.addLast(current); - } - } else if (dy < 0) { - Position current = start; - - while (traversable(current, boundaries, Direction.NORTH) && dy++ < 0) { - current = new Position(x, ++y, height); - positions.addLast(current); - } - } - - Position last = new Position(x, y, height); - if (!last.equals(target) && dx != 0 - && traversable(last, boundaries, dx > 0 ? Direction.WEST : Direction.EAST)) { - return addHorizontal(last, target, positions); - } - - return positions; - } - +package org.apollo.game.model.entity.path; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Optional; + +import org.apollo.game.model.Direction; +import org.apollo.game.model.Position; +import org.apollo.game.model.World; +import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionManager; + +/** + * A very simple pathfinding algorithm that simply walks in the direction of the target until it either reaches it or is + * blocked. + * + * @author Major + */ +public final class SimplePathfindingAlgorithm extends PathfindingAlgorithm { + + /** + * Creates the SimplePathfindingAlgorithm. + * + * @param collisionManager The {@link CollisionManager} used to check if there is a collision + * between two {@link Position}s in a path. + */ + public SimplePathfindingAlgorithm(CollisionManager collisionManager) { + super(collisionManager); + } + + /** + * The Optional containing the boundary Positions. + */ + private Optional boundaries = Optional.empty(); + + @Override + public Deque find(Position origin, Position target) { + int approximation = (int) (origin.getLongestDelta(target) * 1.5); + Deque positions = new ArrayDeque<>(approximation); + + return addHorizontal(origin, target, positions); + } + + /** + * Finds a valid path from the origin {@link Position} to the target one. + * + * @param origin The origin Position. + * @param target The target Position. + * @param boundaries The boundary Positions, which are marking as untraversable. + * @return The {@link Deque} containing the Positions to go through. + */ + public Deque find(Position origin, Position target, Position[] boundaries) { + this.boundaries = Optional.of(boundaries); + return find(origin, target); + } + + /** + * Adds the necessary and possible horizontal {@link Position}s to the existing {@link Deque}. + *

+ * This method: + *

    + *
  • Adds positions horizontally until we are either horizontally aligned with the target, or the next step is not + * traversable. + *
  • Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable: + * if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path. + *
+ * + * @param start The current position. + * @param target The target position. + * @param positions The deque of positions. + * @return The deque of positions containing the path. + */ + private Deque addHorizontal(Position start, Position target, Deque positions) { + int x = start.getX(), y = start.getY(), height = start.getHeight(); + int dx = x - target.getX(), dy = y - target.getY(); + + if (dx > 0) { + Position current = start; + + while (traversable(current, boundaries, Direction.WEST) && dx-- > 0) { + current = new Position(--x, y, height); + positions.addLast(current); + } + } else if (dx < 0) { + Position current = start; + + while (traversable(current, boundaries, Direction.EAST) && dx++ < 0) { + current = new Position(++x, y, height); + positions.addLast(current); + } + } + + Position last = new Position(x, y, height); + if (!start.equals(last) && dy != 0 && traversable(last, boundaries, dy > 0 ? Direction.SOUTH : Direction.NORTH)) { + return addVertical(last, target, positions); + } + + return positions; + } + + /** + * Adds the necessary and possible vertical {@link Position}s to the existing {@link Deque}. + *

+ * This method: + *

    + *
  • Adds positions vertically until we are either vertically aligned with the target, or the next step is not + * traversable. + *
  • Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable: + * if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path. + *
+ * + * @param start The current position. + * @param target The target position. + * @param positions The deque of positions. + * @return The deque of positions containing the path. + */ + private Deque addVertical(Position start, Position target, Deque positions) { + int x = start.getX(), y = start.getY(), height = start.getHeight(); + int dy = y - target.getY(), dx = x - target.getX(); + + if (dy > 0) { + Position current = start; + + while (traversable(current, boundaries, Direction.SOUTH) && dy-- > 0) { + current = new Position(x, --y, height); + positions.addLast(current); + } + } else if (dy < 0) { + Position current = start; + + while (traversable(current, boundaries, Direction.NORTH) && dy++ < 0) { + current = new Position(x, ++y, height); + positions.addLast(current); + } + } + + Position last = new Position(x, y, height); + if (!last.equals(target) && dx != 0 + && traversable(last, boundaries, dx > 0 ? Direction.WEST : Direction.EAST)) { + return addHorizontal(last, target, positions); + } + + return positions; + } + } \ No newline at end of file diff --git a/game/src/main/org/apollo/game/scheduling/impl/NpcMovementTask.java b/game/src/main/org/apollo/game/scheduling/impl/NpcMovementTask.java index 36e99508..66342f31 100644 --- a/game/src/main/org/apollo/game/scheduling/impl/NpcMovementTask.java +++ b/game/src/main/org/apollo/game/scheduling/impl/NpcMovementTask.java @@ -7,7 +7,9 @@ import java.util.Queue; import java.util.Random; import org.apollo.game.model.Position; +import org.apollo.game.model.World; import org.apollo.game.model.area.RegionRepository; +import org.apollo.game.model.area.collision.CollisionManager; import org.apollo.game.model.entity.Npc; import org.apollo.game.model.entity.WalkingQueue; import org.apollo.game.model.entity.path.SimplePathfindingAlgorithm; @@ -50,11 +52,11 @@ public final class NpcMovementTask extends ScheduledTask { /** * Creates the NpcMovementTask. * - * @param repository The {@link RegionRepository}. + * @param collisionManager The {@link CollisionManager} used to check if an {@link Npc} movement is valid. */ - public NpcMovementTask(RegionRepository repository) { + public NpcMovementTask(CollisionManager collisionManager) { super(DELAY, false); - algorithm = new SimplePathfindingAlgorithm(repository); + algorithm = new SimplePathfindingAlgorithm(collisionManager); } /**