From 6a4625cb32a00e53537fa96e0262efd8420c717a Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Fri, 30 Dec 2016 23:42:48 +0000 Subject: [PATCH 1/5] Refactor map loading code Refactors the map file loading code by importing existing code from the Vicis cache editor project. Adds classes for decoding objects and tiles for a given map file index. Only the new MapIndex decoder code is used when loading GameObjects at the moment , which will later be updated. --- .../apollo/cache/decoder/MapFileDecoder.java | 135 -------- .../org/apollo/cache/map/MapConstants.java | 62 ++++ .../main/org/apollo/cache/map/MapFile.java | 48 +++ .../org/apollo/cache/map/MapFileDecoder.java | 123 ++++++++ .../main/org/apollo/cache/map/MapIndex.java | 125 ++++++++ .../org/apollo/cache/map/MapIndexDecoder.java | 70 +++++ .../main/org/apollo/cache/map/MapObject.java | 105 +++++++ .../apollo/cache/map/MapObjectsDecoder.java | 83 +++++ .../main/org/apollo/cache/map/MapPlane.java | 90 ++++++ cache/src/main/org/apollo/cache/map/Tile.java | 295 ++++++++++++++++++ .../main/org/apollo/cache/map/TileUtils.java | 161 ++++++++++ .../game/fs/decoder/GameObjectDecoder.java | 15 +- 12 files changed, 1171 insertions(+), 141 deletions(-) delete mode 100644 cache/src/main/org/apollo/cache/decoder/MapFileDecoder.java create mode 100644 cache/src/main/org/apollo/cache/map/MapConstants.java create mode 100644 cache/src/main/org/apollo/cache/map/MapFile.java create mode 100644 cache/src/main/org/apollo/cache/map/MapFileDecoder.java create mode 100644 cache/src/main/org/apollo/cache/map/MapIndex.java create mode 100644 cache/src/main/org/apollo/cache/map/MapIndexDecoder.java create mode 100644 cache/src/main/org/apollo/cache/map/MapObject.java create mode 100644 cache/src/main/org/apollo/cache/map/MapObjectsDecoder.java create mode 100644 cache/src/main/org/apollo/cache/map/MapPlane.java create mode 100644 cache/src/main/org/apollo/cache/map/Tile.java create mode 100644 cache/src/main/org/apollo/cache/map/TileUtils.java diff --git a/cache/src/main/org/apollo/cache/decoder/MapFileDecoder.java b/cache/src/main/org/apollo/cache/decoder/MapFileDecoder.java deleted file mode 100644 index 6b3d6425..00000000 --- a/cache/src/main/org/apollo/cache/decoder/MapFileDecoder.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.apollo.cache.decoder; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.HashMap; -import java.util.Map; - -import org.apollo.cache.IndexedFileSystem; -import org.apollo.cache.archive.Archive; -import org.apollo.cache.archive.ArchiveEntry; - -/** - * Decodes {@link MapDefinition}s from the {@link IndexedFileSystem}. - * - * @author Ryley - * @author Major - */ -public final class MapFileDecoder { - - /** - * A definition for a region. - */ - public static final class MapDefinition { - - /** - * Indicates whether or not this map is members-only. - */ - private final boolean members; - - /** - * The object file id. - */ - private final int objects; - - /** - * The packed coordinates. - */ - private final int packedCoordinates; - - /** - * The terrain file id. - */ - private final int terrain; - - /** - * Creates the {@link MapDefinition}. - * - * @param packedCoordinates The packed coordinates. - * @param terrain The terrain file id. - * @param objects The object file id. - * @param members Indicates whether or not this map is members-only. - */ - public MapDefinition(int packedCoordinates, int terrain, int objects, boolean members) { - this.packedCoordinates = packedCoordinates; - this.terrain = terrain; - this.objects = objects; - this.members = members; - } - - /** - * Gets the id of the file containing the object data. - * - * @return The file id. - */ - public int getObjectFile() { - return objects; - } - - /** - * Gets the packed coordinates. - * - * @return The packed coordinates. - */ - public int getPackedCoordinates() { - return packedCoordinates; - } - - /** - * Gets the id of the file containing the terrain data. - * - * @return The file id. - */ - public int getTerrainFile() { - return terrain; - } - - /** - * Returns whether or not this MapDefinition is for a members-only area of the world. - * - * @return {@code true} if this MapDefinition is for a members-only area, {@code false} if not. - */ - public boolean isMembersOnly() { - return members; - } - - } - - /** - * The width (and length) of a map file, in tiles. - */ - public static final int MAP_FILE_WIDTH = 64; - - /** - * The file id of the versions archive. - */ - private static final int VERSIONS_ARCHIVE_FILE_ID = 5; - - /** - * Decodes {@link MapDefinition}s from the specified {@link IndexedFileSystem}. - * - * @param fs The IndexedFileSystem. - * @return A {@link Map} of packed coordinates to their MapDefinitions. - * @throws IOException If there is an error reading or decoding the Archive. - */ - public static Map decode(IndexedFileSystem fs) throws IOException { - Archive archive = fs.getArchive(0, VERSIONS_ARCHIVE_FILE_ID); - ArchiveEntry entry = archive.getEntry("map_index"); - Map definitions = new HashMap<>(); - - ByteBuffer buffer = entry.getBuffer(); - int count = buffer.capacity() / (3 * Short.BYTES + Byte.BYTES); - - for (int times = 0; times < count; times++) { - int id = buffer.getShort() & 0xFFFF; - int terrain = buffer.getShort() & 0xFFFF; - int objects = buffer.getShort() & 0xFFFF; - boolean members = buffer.get() == 1; - - definitions.put(id, new MapDefinition(id, terrain, objects, members)); - } - - return definitions; - } - -} \ No newline at end of file diff --git a/cache/src/main/org/apollo/cache/map/MapConstants.java b/cache/src/main/org/apollo/cache/map/MapConstants.java new file mode 100644 index 00000000..63492978 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapConstants.java @@ -0,0 +1,62 @@ +package org.apollo.cache.map; + +/** + * Contains {@link MapFile}-related constants. + * + * @author Major + */ +public final class MapConstants { + + /** + * The index containing the map files. + */ + public static final int MAP_INDEX = 4; + + /** + * The width (and length) of a {@link MapFile} in {@link Tile}s. + */ + public static final int MAP_WIDTH = 64; + + /** + * The amount of planes in a MapFile. + */ + public static final int MAP_PLANES = 4; + + /** + * The multiplicand for height values. + */ + static final int HEIGHT_MULTIPLICAND = 8; + + /** + * The lowest type value that will result in the decoding of a Tile being continued. + */ + static final int LOWEST_CONTINUED_TYPE = 2; + + /** + * The minimum type that specifies the Tile attributes. + */ + static final int MINIMUM_ATTRIBUTES_TYPE = 81; + + /** + * The minimum type that specifies the Tile underlay id. + */ + static final int MINIMUM_OVERLAY_TYPE = 49; + + /** + * The amount of possible overlay orientations. + */ + static final int ORIENTATION_COUNT = 4; + + /** + * The height difference between two planes. + */ + static final int PLANE_HEIGHT_DIFFERENCE = 240; + + /** + * Sole private constructor to prevent instantiation. + */ + private MapConstants() { + + } + +} \ No newline at end of file diff --git a/cache/src/main/org/apollo/cache/map/MapFile.java b/cache/src/main/org/apollo/cache/map/MapFile.java new file mode 100644 index 00000000..b1e6d646 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapFile.java @@ -0,0 +1,48 @@ +package org.apollo.cache.map; + +import com.google.common.base.Preconditions; + +/** + * A 3-dimensional 64x64 area of the map. + * + * @author Major + */ +public final class MapFile { + + /** + * The array of MapPlanes. + */ + private final MapPlane[] planes; + + /** + * Creates the MapFile. + * + * @param planes The {@link MapPlane}s. + */ + public MapFile(MapPlane[] planes) { + this.planes = planes.clone(); + } + + /** + * Gets the {@link MapPlane} with the specified level. + * + * @param plane The plane. + * @return The MapPlane. + * @throws ArrayIndexOutOfBoundsException If {@code plane} is out of bounds. + */ + public MapPlane getPlane(int plane) { + int length = planes.length; + Preconditions.checkElementIndex(plane, length, "Plane index out of bounds, must be [0, " + length + ")."); + return planes[plane]; + } + + /** + * Gets all of the {@link MapPlane}s in this MapFile. + * + * @return The MapPlanes. + */ + public MapPlane[] getPlanes() { + return planes.clone(); + } + +} \ No newline at end of file diff --git a/cache/src/main/org/apollo/cache/map/MapFileDecoder.java b/cache/src/main/org/apollo/cache/map/MapFileDecoder.java new file mode 100644 index 00000000..e85ae5e2 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapFileDecoder.java @@ -0,0 +1,123 @@ +package org.apollo.cache.map; + +import org.apollo.cache.IndexedFileSystem; +import org.apollo.util.CompressionUtil; + +import java.nio.ByteBuffer; +import java.io.IOException; + +/** + * A decoder for the terrain data stored in {@link MapFile}s. + * + * @author Major + */ +public class MapFileDecoder { + /** + * Creates a MapFileDecoder for the specified map file. + * + * @param fs The {@link IndexedFileSystem} to get the file from. + * @param index The {@link MapIndex} to get the file index from. + * @return The MapFileDecoder. + * @throws IOException If there is an error reading or decompressing the file. + */ + public static MapFileDecoder create(IndexedFileSystem fs, MapIndex index) throws IOException { + ByteBuffer compressed = fs.getFile(MapConstants.MAP_INDEX, index.getMapFile()); + ByteBuffer decompressed = ByteBuffer.wrap(CompressionUtil.degzip(compressed)); + + return new MapFileDecoder(decompressed); + } + + /** + * The DataBuffer containing the MapFile data. + */ + private final ByteBuffer buffer; + + /** + * Creates the MapIndexDecoder. + *

+ * This constructor expects the {@link ByteBuffer} to not be compressed. + * + * @param buffer The DataBuffer containing the MapFile data. + */ + public MapFileDecoder(ByteBuffer buffer) { + this.buffer = buffer.asReadOnlyBuffer(); + } + + /** + * Decodes the data into a {@link MapFile}. + * + * @return The MapFile. + */ + public MapFile decode() { + MapPlane[] planes = new MapPlane[MapConstants.MAP_PLANES]; + + for (int level = 0; level < MapConstants.MAP_PLANES; level++) { + planes[level] = decodePlane(planes, level); + } + + return new MapFile(planes); + } + + /** + * Decodes a {@link MapPlane} with the specified level. + * + * @param planes The previously-decoded {@link MapPlane}s, for calculating the height of the tiles. + * @param level The level. + * @return The MapPlane. + */ + private MapPlane decodePlane(MapPlane[] planes, int level) { + Tile[][] tiles = new Tile[MapConstants.MAP_WIDTH][MapConstants.MAP_WIDTH]; + + for (int x = 0; x < MapConstants.MAP_WIDTH; x++) { + for (int z = 0; z < MapConstants.MAP_WIDTH; z++) { + tiles[x][z] = decodeTile(planes, level, x, z); + } + } + + return new MapPlane(level, tiles); + } + + /** + * Decodes the data into a {@link Tile}. + * + * @param planes The previously-decoded {@link MapPlane}s, for calculating the height of the Tile. + * @param level The level the Tile is on. + * @param x The x coordinate of the Tile. + * @param z The z coordinate of the Tile. + * @return The MapFile. + */ + private Tile decodeTile(MapPlane[] planes, int level, int x, int z) { + Tile.Builder builder = Tile.builder(x, z, level); + + int type; + do { + type = buffer.get() & 0xFF; + + if (type == 0) { + if (level == 0) { + builder.setHeight(TileUtils.calculateHeight(x, z)); + } else { + Tile below = planes[level - 1].getTile(x, z); + builder.setHeight(below.getHeight() + MapConstants.PLANE_HEIGHT_DIFFERENCE); + } + } else if (type == 1) { + int height = buffer.get(); + int below = (level == 0) ? 0 : planes[level - 1].getTile(x, z).getHeight(); + + builder.setHeight((height == 1 ? 0 : height) * MapConstants.HEIGHT_MULTIPLICAND + below); + } else if (type <= MapConstants.MINIMUM_OVERLAY_TYPE) { + builder.setOverlay(buffer.get()); + builder.setOverlayType((type - MapConstants.LOWEST_CONTINUED_TYPE) + / MapConstants.ORIENTATION_COUNT); + builder.setOverlayOrientation(type - MapConstants.LOWEST_CONTINUED_TYPE + % MapConstants.ORIENTATION_COUNT); + } else if (type <= MapConstants.MINIMUM_ATTRIBUTES_TYPE) { + builder.setAttributes(type - MapConstants.MINIMUM_OVERLAY_TYPE); + } else { + builder.setUnderlay(type - MapConstants.MINIMUM_ATTRIBUTES_TYPE); + } + } while (type >= MapConstants.LOWEST_CONTINUED_TYPE); + + return builder.build(); + } +} diff --git a/cache/src/main/org/apollo/cache/map/MapIndex.java b/cache/src/main/org/apollo/cache/map/MapIndex.java new file mode 100644 index 00000000..9b707c85 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapIndex.java @@ -0,0 +1,125 @@ +package org.apollo.cache.map; + +import org.apollo.cache.def.ItemDefinition; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * A definition for a map. + */ +public final class MapIndex { + + /** + * Indicates whether or not this map is members-only. + */ + private final boolean members; + + /** + * The object file id. + */ + private final int objects; + + /** + * The packed coordinates. + */ + private final int packedCoordinates; + + /** + * The terrain file id. + */ + private final int terrain; + + /** + * A mapping of region ids to {@link MapIndex}es. + */ + private static Map indices; + + /** + * Initialises the class with the specified set of indices. + */ + public static void init(Map indices) { + MapIndex.indices = Collections.unmodifiableMap(indices); + } + + /** + * Gets the {@code Map} of {@link MapIndex} instances. + * + * @return The map of {@link MapIndex} instances. + */ + public static Map getIndices() { + return indices; + } + + /** + * Creates the {@link MapIndex}. + * + * @param packedCoordinates The packed coordinates. + * @param terrain The terrain file id. + * @param objects The object file id. + * @param members Indicates whether or not this map is members-only. + */ + public MapIndex(int packedCoordinates, int terrain, int objects, boolean members) { + this.packedCoordinates = packedCoordinates; + this.terrain = terrain; + this.objects = objects; + this.members = members; + } + + /** + * Gets the id of the file containing the object data. + * + * @return The file id. + */ + public int getObjectFile() { + return objects; + } + + /** + * Gets the packed coordinates. + * + * @return The packed coordinates. + */ + public int getPackedCoordinates() { + return packedCoordinates; + } + + /** + * Gets the id of the file containing the terrain data. + * + * @return The file id. + */ + public int getMapFile() { + return terrain; + } + + /** + * Gets the X coordinate of this map. + * + * @return The X coordinate of this map. + */ + public int getX() { + return (packedCoordinates >> 8 & 0xFF) * MapConstants.MAP_WIDTH; + } + + /** + * Gets the Y coordinate of this map. + * + * @return The y coordinate of this map. + */ + public int getY() { + return (packedCoordinates & 0xFF) * MapConstants.MAP_WIDTH; + + } + + /** + * Returns whether or not this MapIndex is for a members-only area of the world. + * + * @return {@code true} if this MapIndex is for a members-only area, {@code false} if not. + */ + public boolean isMembersOnly() { + return members; + } + +} diff --git a/cache/src/main/org/apollo/cache/map/MapIndexDecoder.java b/cache/src/main/org/apollo/cache/map/MapIndexDecoder.java new file mode 100644 index 00000000..7bca2add --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapIndexDecoder.java @@ -0,0 +1,70 @@ +package org.apollo.cache.map; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import org.apollo.cache.IndexedFileSystem; +import org.apollo.cache.archive.Archive; +import org.apollo.cache.archive.ArchiveEntry; +import org.apollo.cache.map.MapIndex; + +/** + * Decodes {@link MapIndex}s from the {@link IndexedFileSystem}. + * + * @author Ryley + * @author Major + */ +public final class MapIndexDecoder implements Runnable { + + /** + * The file id of the versions archive. + */ + private static final int VERSIONS_ARCHIVE_FILE_ID = 5; + + /** + * The IndexedFileSystem. + */ + private final IndexedFileSystem fs; + + public MapIndexDecoder(IndexedFileSystem fs) { + this.fs = fs; + } + + /** + * Decodes {@link MapIndex}s from the specified {@link IndexedFileSystem}. + * + * @return A {@link Map} of packed coordinates to their MapDefinitions. + * @throws IOException If there is an error reading or decoding the Archive. + */ + public Map decode() throws IOException { + Archive archive = fs.getArchive(0, VERSIONS_ARCHIVE_FILE_ID); + ArchiveEntry entry = archive.getEntry("map_index"); + Map definitions = new HashMap<>(); + + ByteBuffer buffer = entry.getBuffer(); + int count = buffer.capacity() / (3 * Short.BYTES + Byte.BYTES); + + for (int times = 0; times < count; times++) { + int id = buffer.getShort() & 0xFFFF; + int terrain = buffer.getShort() & 0xFFFF; + int objects = buffer.getShort() & 0xFFFF; + boolean members = buffer.get() == 1; + + definitions.put(id, new MapIndex(id, terrain, objects, members)); + } + + return definitions; + } + + @Override + public void run() { + try { + MapIndex.init(decode()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} \ No newline at end of file diff --git a/cache/src/main/org/apollo/cache/map/MapObject.java b/cache/src/main/org/apollo/cache/map/MapObject.java new file mode 100644 index 00000000..b2cf741d --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapObject.java @@ -0,0 +1,105 @@ +package org.apollo.cache.map; + +/** + * Represents a static world object in a map file. + */ +public final class MapObject { + + /** + * The object definition id of this {@code MapObject}. + */ + private final int id; + + /** + * The packed coordinates (local XY and height) for this object. + */ + private int packedCoordinates; + + /** + * The type of this object. + */ + private final int type; + + /** + * The orientation of this object. + */ + private final int orientation; + + /** + * Creates a new {@code MapObject}. + * + * @param id The object ID of this map object. + * @param packedCoordinates A packed integer containing the coordinates of this map object. + * @param type The type of object. + * @param orientation The object facing direction. + */ + public MapObject(int id, int packedCoordinates, int type, int orientation) { + this.id = id; + this.packedCoordinates = packedCoordinates; + this.type = type; + this.orientation = orientation; + } + + /** + * Get the object ID of this map object. + * + * @return The object ID for {@link org.apollo.cache.def.ObjectDefinition} lookups. + */ + public int getId() { + return id; + } + + /** + * Get the plane this map object exists on. + * + * @return The plane this map object is on. + */ + public int getHeight() { + return packedCoordinates >> 12 & 0x3; + } + + /** + * Get the X coordinate of this object relative to the map position. + * + * @return The local X coordinate. + */ + public int getLocalX() { + return packedCoordinates >> 6 & 0x3F; + } + + /** + * Get the Y coordinate of this object relative to the map position. + * + * @return The local Y coordinate. + */ + public int getLocalY() { + return packedCoordinates & 0x3F; + } + + /** + * Get the integer representation of this objects orientation (0 indexed, starting West-North-East-South). + * + * @return The orientation of this object. + */ + public int getOrientation() { + return orientation; + } + + /** + * Get a packed integer containing the x/y coordinates and height for this object. + * + * @return The packed coordinates. + */ + public int getPackedCoordinates() { + return packedCoordinates; + } + + /** + * Get the type of this object. + * + * @return The type of this object. + */ + public int getType() { + return type; + } +} diff --git a/cache/src/main/org/apollo/cache/map/MapObjectsDecoder.java b/cache/src/main/org/apollo/cache/map/MapObjectsDecoder.java new file mode 100644 index 00000000..351ea7d7 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapObjectsDecoder.java @@ -0,0 +1,83 @@ +package org.apollo.cache.map; + +import org.apollo.cache.IndexedFileSystem; +import org.apollo.cache.map.MapIndex; +import org.apollo.cache.map.MapConstants; +import org.apollo.cache.map.MapObject; +import org.apollo.util.BufferUtil; +import org.apollo.util.CompressionUtil; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A decoder for reading the map objects for a given map. + * + * @author Major + */ +public final class MapObjectsDecoder { + /** + * Creates a MapObjectsDecoder for the specified map file. + * + * @param fs The {@link IndexedFileSystem} to get the file from. + * @param index The map index to decode objects for. + * @return The MapObjectsDecoder. + * @throws IOException If there is an error reading or decompressing the file. + */ + public static MapObjectsDecoder create(IndexedFileSystem fs, MapIndex index) throws IOException { + ByteBuffer compressed = fs.getFile(MapConstants.MAP_INDEX, index.getObjectFile()); + ByteBuffer decompressed = ByteBuffer.wrap(CompressionUtil.degzip(compressed)); + + return new MapObjectsDecoder(decompressed); + } + + /** + * The buffer to decode {@link MapObject}s from. + */ + private final ByteBuffer buffer; + + /** + * Create a new {@link MapObjectsDecoder} from the given buffer and map coordinates. + * + * @param buffer The decompressed object file buffer. + */ + public MapObjectsDecoder(ByteBuffer buffer) { + this.buffer = buffer.asReadOnlyBuffer(); + } + + /** + * Decodes the data in the {@code buffer} to a list of {@link MapObject}s. + * + * @return A list of decoded {@link MapObject}s. + */ + public List decode() { + List objects = new ArrayList<>(); + + 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 attributes = buffer.get() & 0xFF; + int type = attributes >> 2; + int orientation = attributes & 0x3; + objects.add(new MapObject(id, packed, type, orientation)); + + positionOffset = BufferUtil.readSmart(buffer); + } + + idOffset = BufferUtil.readSmart(buffer); + } + + return objects; + } +} diff --git a/cache/src/main/org/apollo/cache/map/MapPlane.java b/cache/src/main/org/apollo/cache/map/MapPlane.java new file mode 100644 index 00000000..1dfc3a01 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapPlane.java @@ -0,0 +1,90 @@ +package org.apollo.cache.map; + +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * A plane of a map, which is a distinct height level. + * + * @author Major + */ +public final class MapPlane { + + /** + * Returns a shallow copy of the specified 2-dimensional array. + * + * @param array The array to copy. Must not be {@code null}. + * @return The copy. + */ + private static T[][] clone(T[][] array) { + T[][] copy = array.clone(); + for (int index = 0; index < copy.length; index++) { + copy[index] = array[index].clone(); + } + + return copy; + } + + /** + * The level of this MapPlane. + */ + private final int level; + + /** + * The 2-dimensional array of Tiles. + */ + private final Tile[][] tiles; + + /** + * Creates the MapPlane. + * + * @param level The level of the MapPlane. + * @param tiles The 2D array of {@link Tile}s. Must not be {@code null}. Must be square. + */ + public MapPlane(int level, Tile[][] tiles) { + this.level = level; + this.tiles = clone(tiles); + } + + /** + * Gets the level of this MapPlane. + * + * @return The level. + */ + public int getLevel() { + return level; + } + + /** + * Gets the amount of tiles in this MapPlane. + * + * @return The amount of tiles. + */ + public int getSize() { + return tiles.length * tiles[0].length; + } + + /** + * Gets the {@link Tile} at the specified (x, z) coordinate. + * + * @param x The x coordinate. + * @param z The z coordinate. + * @return The Tile. + */ + public Tile getTile(int x, int z) { + return tiles[x][z]; + } + + /** + * Gets the {@link Tile}s in this MapPlane. + *

+ * This method returns the Tiles according on a column-based ordering: for a 2x2 tile set, the order will be + * {@code (0, 0), (0, 1), (1, 0), (1, 1)}. + * + * @return The Tiles. + */ + public Stream getTiles() { + return Arrays.stream(tiles).flatMap(Arrays::stream); + } + +} \ No newline at end of file diff --git a/cache/src/main/org/apollo/cache/map/Tile.java b/cache/src/main/org/apollo/cache/map/Tile.java new file mode 100644 index 00000000..e19d8133 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/Tile.java @@ -0,0 +1,295 @@ +package org.apollo.cache.map; + +/** + * A single tile on the map. + * + * @author Major + */ +public final class Tile { + + /** + * A builder class for a Tile. + */ + public static final class Builder { + + /** + * The attributes of the Tile. + */ + private int attributes; + + /** + * The height of the Tile. + */ + private int height; + + /** + * The overlay id of the Tile. + */ + private int overlay; + + /** + * The overlay orientation of the Tile. + */ + private int overlayOrientation; + + /** + * The overlay type of the Tile. + */ + private int overlayType; + + /** + * The x coordinate of the Tile. + */ + private int x; + + /** + * The y coordinate of the Tile. + */ + private int y; + + /** + * The underlay id of the Tile. + */ + private int underlay; + + /** + * Creates the Builder. + * + * @param x The x position of the Tile. + * @param y The y position of the Tile. + * @param height The height level of the Tile. + */ + public Builder(int x, int y, int height) { + this.x = x; + this.y = y; + this.height = height; + } + + /** + * Builds the contents of this Builder into a Tile. + * + * @return The Tile. + */ + public Tile build() { + return new Tile(x, y, attributes, height, overlay, overlayType, overlayOrientation, underlay); + } + + /** + * Sets the attributes of the Tile. + * + * @param attributes The attributes. + */ + public void setAttributes(int attributes) { + this.attributes = attributes; + } + + /** + * Sets the height of the Tile. + * + * @param height The height. + */ + public void setHeight(int height) { + this.height = height; + } + + /** + * Sets the overlay id of the Tile. + * + * @param overlay The overlay id. + */ + public void setOverlay(int overlay) { + this.overlay = overlay; + } + + /** + * Sets the overlay orientation of the Tile. + * + * @param orientation The overlay orientation. + */ + public void setOverlayOrientation(int orientation) { + this.overlayOrientation = orientation; + } + + /** + * Sets the overlay type of the Tile. + * + * @param type The overlay type. + */ + public void setOverlayType(int type) { + this.overlayType = type; + } + + /** + * Sets the position of the Tile. + * + * @param x The x coordinate of the Tile. + * @param y the y coordinate of the Tile + * @param height The height level of the Tile. + */ + public void setPosition(int x, int y, int height) { + this.x = x; + this.y = y; + this.height = height; + } + + /** + * Sets the underlay id of the Tile. + * + * @param underlay The underlay. + */ + public void setUnderlay(int underlay) { + this.underlay = underlay; + } + + } + + /** + * Creates a {@link Builder} for a Tile. + * + * @param x The x coordinate of the Tile. + * @param y the y coordinate of the Tile. + * @param height The height level of the Tile. + * @return The Builder. + */ + public static Builder builder(int x, int y, int height) { + return new Builder(x, y, height); + } + + /** + * The attributes of this Tile. + */ + private final int attributes; + + /** + * The height of this Tile. + */ + private final int height; + + /** + * The overlay id of this Tile. + */ + private final int overlay; + + /** + * The overlay orientation of this Tile. + */ + private final int overlayOrientation; + + /** + * The overlay type of this Tile. + */ + private final int overlayType; + + /** + * The x coordinate of this Tile. + */ + private final int x; + + /** + * The y coordinate of this Tile. + */ + private final int y; + + /** + * The underlay id of this Tile. + */ + private final int underlay; + + /** + * Creates the Tile. + * + * @param x The x coordinate of the Tile. + * @param y The y coordinate of the Tile. + * @param attributes The attributes. + * @param height The height. + * @param overlay The overlay id. + * @param overlayType The overlay type. + * @param overlayOrientation The overlay orientation. + * @param underlay The underlay id. + */ + public Tile(int x, int y, int attributes, int height, int overlay, int overlayType, int overlayOrientation, + int underlay) { + this.x = x; + this.y = y; + this.attributes = attributes; + this.height = height; + this.overlay = overlay; + this.overlayType = overlayType; + this.overlayOrientation = overlayOrientation; + this.underlay = underlay; + } + + /** + * Gets the attributes of this Tile. + * + * @return The attributes. + */ + public int getAttributes() { + return attributes; + } + + /** + * Gets the height of this Tile. + * + * @return The height. + */ + public int getHeight() { + return height; + } + + /** + * Gets the overlay id of this Tile. + * + * @return The overlay id. + */ + public int getOverlay() { + return overlay; + } + + /** + * Gets the overlay orientation of this Tile. + * + * @return The overlay orientation. + */ + public int getOverlayOrientation() { + return overlayOrientation; + } + + /** + * Gets the overlay type of this Tile. + * + * @return The overlay types. + */ + public int getOverlayType() { + return overlayType; + } + + /** + * Gets the underlay id of this Tile. + * + * @return The underlay id. + */ + public int getUnderlay() { + return underlay; + } + + + /** + * Gets the x coordinate of this Tile. + * + * @return The x coordinate. + */ + public int getX() { + return x; + } + + /** + * Gets the y coordinate of this Tile. + * + * @return The y coordinate. + */ + public int getY() { + return y; + } + +} diff --git a/cache/src/main/org/apollo/cache/map/TileUtils.java b/cache/src/main/org/apollo/cache/map/TileUtils.java new file mode 100644 index 00000000..977e9562 --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/TileUtils.java @@ -0,0 +1,161 @@ +package org.apollo.cache.map; + +/* + * Copyright (c) 2012-2013 Jonathan Edgecombe + * Copyright (c) 2015 Major + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * Contains tile-related utility methods. + * + * @author Johnny + * @author Major + */ +public final class TileUtils { + + /** + * The x coordinate offset, used for computing the Tile height. + */ + static final int TILE_HEIGHT_X_OFFSET = 0xe3b7b; + + /** + * The z coordinate offset, used for computing the Tile height. + */ + static final int TILE_HEIGHT_Z_OFFSET = 0x87cce; + + /** + * The cosine table used for interpolation. + */ + private static final int[] COSINE = new int[2048]; + + static { + for (int index = 0; index < COSINE.length; index++) { + COSINE[index] = (int) (65536 * Math.cos(2 * Math.PI * index / COSINE.length)); + } + } + + /** + * Calculates the height offset for the specified coordinate pair. + * + * @param x The x coordinate of the Tile. + * @param z The z coordinate of the Tile. + * @return The height offset. + */ + public static int calculateHeight(int x, int z) { + int regionSize = 8; + int regionOffset = 6; + int offset = regionOffset * regionSize; + + int baseX = x - offset; + int baseZ = z - offset; + + return computeHeight(x + TILE_HEIGHT_X_OFFSET - baseX, z + TILE_HEIGHT_Z_OFFSET - baseZ) + * MapConstants.HEIGHT_MULTIPLICAND; + } + + /** + * Gets the height offset for the specified coordinate pair. + * + * @param x The offset-x coordinate of the tile. + * @param z The offset-z coordinate of the tile. + * @return The tile height offset. + */ + private static int computeHeight(int x, int z) { + int total = interpolatedNoise(x + 45365, z + 91923, 4) - 128; + + total += (interpolatedNoise(x + 10294, z + 37821, 2) - 128) / 2; + total += (interpolatedNoise(x, z, 1) - 128) / 4; + + total = (int) Math.max(total * 0.3 + 35, 10); + return Math.min(total, 60); + } + + /** + * Interpolates two smooth noise values. + * + * @param a The first smooth noise value. + * @param b The second smooth noise value. + * @param theta The angle. + * @param reciprocal The frequency reciprocal. + * @return The interpolated value. + */ + private static int interpolate(int a, int b, int theta, int reciprocal) { + int cosine = 65536 - COSINE[theta * COSINE.length / (2 * reciprocal)] / 2; + return (a * (65536 - cosine)) / 65536 + (b * cosine) / 65536; + } + + /** + * Gets interpolated noise for the specified coordinate pair, using the specified frequency reciprocal. + * + * @param x The x coordinate. + * @param z The z coordinate. + * @param reciprocal The frequency reciprocal. + * @return The interpolated noise. + */ + private static int interpolatedNoise(int x, int z, int reciprocal) { + int xt = x % reciprocal; + int zt = z % reciprocal; + + x /= reciprocal; + z /= reciprocal; + + int c = smoothNoise(x, z); + int e = smoothNoise(x + 1, z); + int ce = interpolate(c, e, xt, reciprocal); + + int n = smoothNoise(x, z + 1); + int ne = smoothNoise(x + 1, z + 1); + int u = interpolate(n, ne, xt, reciprocal); + + return interpolate(ce, u, zt, reciprocal); + } + + /** + * Computes noise for the specified coordinate pair. + * + * @param x The x coordinate. + * @param z The z coordinate. + * @return The noise. + */ + private static int noise(int x, int z) { + int n = x + z * 57; + n = (n << 13) ^ n; + n = (n * (n * n * 15731 + 789221) + 1376312589) & Integer.MAX_VALUE; + return (n >> 19) & 0xff; + } + + /** + * Computes smooth noise for the specified coordinate pair. + * + * @param x The x coordinate. + * @param z The z coordinate. + * @return The smooth noise. + */ + private static int smoothNoise(int x, int z) { + int corners = noise(x - 1, z - 1) + noise(x + 1, z - 1) + noise(x - 1, z + 1) + noise(x + 1, z + 1); + int sides = noise(x - 1, z) + noise(x + 1, z) + noise(x, z - 1) + noise(x, z + 1); + int center = noise(x, z); + + return corners / 16 + sides / 8 + center / 4; + } + + /** + * Sole private constructor to prevent instantiation. + */ + private TileUtils() { + + } + +} \ No newline at end of file diff --git a/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java b/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java index 68b7af03..1998f12f 100644 --- a/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java +++ b/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java @@ -8,10 +8,10 @@ import java.util.List; import java.util.Map; import org.apollo.cache.IndexedFileSystem; -import org.apollo.cache.decoder.MapFileDecoder; -import org.apollo.cache.decoder.MapFileDecoder.MapDefinition; 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; @@ -85,10 +85,13 @@ public final class GameObjectDecoder implements Runnable { ObjectDefinitionDecoder decoder = new ObjectDefinitionDecoder(fs); decoder.run(); - try { - Map definitions = MapFileDecoder.decode(fs); + MapIndexDecoder mapIndexDecoder = new MapIndexDecoder(fs); + mapIndexDecoder.run(); - for (MapDefinition definition : definitions.values()) { + 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); @@ -97,7 +100,7 @@ public final class GameObjectDecoder implements Runnable { ByteBuffer decompressed = ByteBuffer.wrap(CompressionUtil.degzip(objects)); decodeObjects(decompressed, x, y); - ByteBuffer terrain = fs.getFile(4, definition.getTerrainFile()); + ByteBuffer terrain = fs.getFile(4, definition.getMapFile()); decompressed = ByteBuffer.wrap(CompressionUtil.degzip(terrain)); decodeTerrain(decompressed, x, y); } From 376d36871ad4e6f0a4a6616cf80f38c0575b51ac Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Fri, 30 Dec 2016 23:47:03 +0000 Subject: [PATCH 2/5] Allow registering region listeners globally Adds the ability to register region listeners with the RegionRepository, which will register the listener with any existing regions and cache it for any newly created regions later. --- .../org/apollo/game/model/area/Region.java | 4 ++++ .../game/model/area/RegionRepository.java | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+) 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 1e66b211..9b2f4a8c 100644 --- a/game/src/main/org/apollo/game/model/area/Region.java +++ b/game/src/main/org/apollo/game/model/area/Region.java @@ -163,6 +163,10 @@ public final class Region { addEntity(entity, true); } + public void addListener(RegionListener listener) { + listeners.add(listener); + } + /** * Checks if this Region contains the specified Entity. * diff --git a/game/src/main/org/apollo/game/model/area/RegionRepository.java b/game/src/main/org/apollo/game/model/area/RegionRepository.java index 693ce8cd..902ffdbf 100644 --- a/game/src/main/org/apollo/game/model/area/RegionRepository.java +++ b/game/src/main/org/apollo/game/model/area/RegionRepository.java @@ -1,5 +1,6 @@ package org.apollo.game.model.area; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,6 +49,11 @@ public final class RegionRepository { */ private final Map regions = new HashMap<>(); + /** + * A list of default {@link RegionListener}s which will be added to {@link Region}s upon creation. + */ + private final List defaultRegionListeners = new ArrayList<>(); + /** * Creates a new RegionRepository. * @@ -71,9 +77,24 @@ public final class RegionRepository { throw new UnsupportedOperationException("Cannot add a Region with the same coordinates as an existing Region."); } + defaultRegionListeners.forEach(region::addListener); regions.put(region.getCoordinates(), region); } + /** + * Adds a {@link RegionListener} to be registered as a default listener with all newly created {@link Region}s and + * associated with any existing instances. + * + * @param listener The listener to add. + */ + public void addRegionListener(RegionListener listener) { + for (Region region : regions.values()) { + region.addListener(listener); + } + + defaultRegionListeners.add(listener); + } + /** * Indicates whether the supplied value (i.e. the {@link Region}) has a mapping. * From 0672fa2ea01c80ad834b8f5766dc9c7dbbd5ed98 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Fri, 30 Dec 2016 23:49:17 +0000 Subject: [PATCH 3/5] Fix bitwise negation in CollisionMatrix#clear() Adds the missing bitwise AND to the clear() method in CollisionMatrix, so any flags besides the one given are retained. --- .../game/model/area/collision/CollisionMatrix.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 23c058fe..d08a528b 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 @@ -104,6 +104,18 @@ public final class CollisionMatrix { return false; } + /** + * Completely blocks the tile at the specified coordinate pair, while optionally allowing projectiles + * to pass through. + * + * @param x The x coordinate. + * @param y The y coordinate. + * @param impenetrable If projectiles should be permitted to traverse this tile. + */ + public void block(int x, int y, boolean impenetrable) { + set(x, y, impenetrable ? ALL_BLOCKED : ALL_MOBS_BLOCKED); + } + /** * Completely blocks the tile at the specified coordinate pair. * @@ -123,7 +135,7 @@ public final class CollisionMatrix { * @param flag The CollisionFlag. */ public void clear(int x, int y, CollisionFlag flag) { - set(x, y, (byte) ~flag.asByte()); + set(x, y, (byte) (matrix[indexOf(x, y)] & ~flag.asByte())); } /** From 6188c2e751de17dc5d220d285aeaa2291d922f8d Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Sat, 31 Dec 2016 00:55:00 +0000 Subject: [PATCH 4/5] 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); } /** From 916f6c547d24c74e6907e8f70fd2d9e68390cbab Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Sat, 31 Dec 2016 05:19:49 +0000 Subject: [PATCH 5/5] Add tests around CollisionManager functionality Adds tests for the CollisionManager which makes sure that collision matrices are correctly built for the following object types: * Interactables * Walls * Corners Missing from these tests are tests for blocked / bridged tiles, and large corner walls, which can be added later. Additionally, the test cases include ASCII illustrations of the collision grid and how it is built / checked for a particular object or movement. --- .../area/collision/CollisionManagerTests.java | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 game/src/test/org/apollo/game/model/area/collision/CollisionManagerTests.java diff --git a/game/src/test/org/apollo/game/model/area/collision/CollisionManagerTests.java b/game/src/test/org/apollo/game/model/area/collision/CollisionManagerTests.java new file mode 100644 index 00000000..d0f52897 --- /dev/null +++ b/game/src/test/org/apollo/game/model/area/collision/CollisionManagerTests.java @@ -0,0 +1,242 @@ +package org.apollo.game.model.area.collision; + +import org.apollo.cache.def.ObjectDefinition; +import org.apollo.cache.map.MapObject; +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.entity.EntityType; +import org.apollo.game.model.entity.obj.ObjectType; +import org.apollo.game.model.entity.obj.StaticGameObject; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + + +/** + * Tests for the {@link CollisionManager}. + */ +public final class CollisionManagerTests { + + /** + * The id of a simple wall object. + */ + private static final int WALL = 0; + + /** + * The id of a square 2x2 solid object. + */ + private static final int SQUARE_OBJECT = 1; + + /** + * Setup some simple object definitions to use in collision tests. + */ + @BeforeClass + public static void setupObjectDefinitions() { + ObjectDefinition wall = new ObjectDefinition(WALL); + + ObjectDefinition squareObject = new ObjectDefinition(SQUARE_OBJECT); + squareObject.setLength(2); + squareObject.setWidth(2); + + ObjectDefinition.init(new ObjectDefinition[]{ + wall, squareObject + }); + } + + /** + * Tests that a wall is untraversable from the front and back, for the facing direction and opposite facing direction + * respectively. For a simple grid, with a wall on 0,1 we end up with a collision matrix looking like: + *
+	 * (0,2) |---------|
+	 *       |         |
+	 *       |         |
+	 *       |   xxx   |
+	 * (0,1) |---------|
+	 *       |   xxx   |
+	 *       |         |
+	 *       |         |
+	 * (0,0) |---------|
+	 *       |         |
+	 *       |         |
+	 *       |         |
+	 *       |---------|
+	 * 
+ *

+ * Where {@code xxx} denotes that you cannot walk into that tile from that direction (in this case you south on + * (0,2) and north on (0,1)). For the grid above you can't walk north into {@code (0, 2)} because {@code (0, 2)} is + * untraversable from the south, and as expectred you can't walk south into {@code (0,1)} because ({@code (0, 1)} is + * untraversable from the north. + */ + @Test + public void wall() { + Position front = new Position(0, 1, 0); + Position back = front.step(1, Direction.NORTH); + + CollisionManager collisionManager = createCollisionManager( + createMapObject(WALL, front, ObjectType.LENGTHWISE_WALL, Direction.NORTH) + ); + + assertUntraversable(collisionManager, front, Direction.NORTH); + assertUntraversable(collisionManager, back, Direction.SOUTH); + assertUntraversable(collisionManager, front, Direction.NORTH_EAST); + assertUntraversable(collisionManager, back, Direction.SOUTH_EAST); + } + + /** + * Tests that a corner wall is untraversable from the sides that it blocks. Corners are much like walls, with + * the only difference being their orientation. Instead of {@link Direction#WNES}, they have diagonal directions. + * With a corner wall at (0, 1) facing to the north-east we end up with a grid looking like this: + *

+ *

+	 * (0,2) |---------|---------|
+	 *       |         |         |
+	 *       |         |         |
+	 *       |         |xxx      |
+	 * (0,1) |---------|---------|
+	 *       |      xxx|         |
+	 *       |         |         |
+	 *       |         |         |
+	 * (0,0) |---------|---------|
+	 * 	   (1,0)     (2,0)     (3,0)
+	 * 
+ *

+ * Where you can walk north and east through the tile the corner occupies, as well as south and west through the + * adjacent tile, but not north-east or south-west. + */ + @Test + public void cornerWall() { + Position front = new Position(0, 1, 0); + Position back = front.step(1, Direction.NORTH_EAST); + + CollisionManager collisionManager = createCollisionManager( + createMapObject(WALL, front, ObjectType.TRIANGULAR_CORNER, Direction.NORTH_EAST) + ); + + assertTraversable(collisionManager, front, Direction.NORTH, Direction.EAST); + assertUntraversable(collisionManager, front, Direction.NORTH_EAST); + assertTraversable(collisionManager, back, Direction.SOUTH, Direction.WEST); + assertUntraversable(collisionManager, back, Direction.SOUTH_WEST); + } + + /** + * Tests that the tiles occupied by a 2x2 square object are not traversable. When an interactable object is added + * to a collision update, all tiles spanning its with and length from its origin position will be marked as completely + * blocked off, which with a 2x2 object at (1,1) produces a grid like this: + *

+	 * (0,3) |---------|---------|---------|---------|
+	 *       |         |xxxxxxxxx|xxxxxxxxx|         |
+	 *       |         |x       x|x       x|         |
+	 *       |         |xxxxxxxxx|xxxxxxxxx|         |
+	 * (0,2) |---------|---------|---------|---------|
+	 *       |         |xxxxxxxxx|xxxxxxxxx|         |
+	 *       |         |x       x|x       x|         |
+	 *       |         |xxxxxxxxx|xxxxxxxxx|         |
+	 * (0,1) |---------|---------|---------|---------|
+	 *       |         |         |         |         |
+	 *       |         |         |         |         |
+	 *       |         |         |         |         |
+	 * (0,0) |---------|---------|---------|---------|
+	 * 	   (0,0)     (1,0)     (2,0)     (3,0)     (4,0)
+	 * 
+ *

+ * Where every tile that is occupied by the object is untraversable in every direction. + */ + @Test + public void object() { + Position origin = new Position(1, 1, 0); + + CollisionManager collisionManager = createCollisionManager( + createMapObject(SQUARE_OBJECT, origin, ObjectType.INTERACTABLE, Direction.NORTH) + ); + + Position west = origin.step(1, Direction.WEST); + Position east = origin.step(2, Direction.EAST); + Position northEast = origin.step(2, Direction.NORTH).step(1, Direction.EAST); + Position southEast = origin.step(1, Direction.SOUTH_EAST); + + assertUntraversable(collisionManager, west, Direction.EAST); + assertUntraversable(collisionManager, east, Direction.WEST); + assertUntraversable(collisionManager, northEast, Direction.SOUTH_EAST, Direction.SOUTH_WEST); + assertUntraversable(collisionManager, southEast, Direction.NORTH_EAST, Direction.NORTH_WEST); + } + + /** + * Helper function for creating {@code org.apollo.cache} {@link MapObject}s using data structures from + * {@code org.apollo.game}. + * + * @param id The id of the object. Must be one of the {@link ObjectDefinition}s defined above. + * @param position The position of the object. + * @param type The object type. + * @param direction The orientation of the object. + * @return A new {@link MapObject}. + */ + private static MapObject createMapObject(int id, Position position, ObjectType type, Direction direction) { + return new MapObject(id, position.getX(), position.getY(), position.getHeight(), type.getValue(), + direction.toOrientationInteger()); + } + + /** + * Sets up dependencies for and creates a stub {@link CollisionManager}, then builds the collision matrices + * using the {@code objects} given. + * + * @param objects The objects to build collision matrices from. + * @return A new {@link CollisionManager} with a valid {@link RegionRepository} and every {@link CollisionMatrix} + * built. + */ + private static CollisionManager createCollisionManager(MapObject... objects) { + World world = new World(); + RegionRepository regions = world.getRegionRepository(); + CollisionManager collisionManager = world.getCollisionManager(); + + for (MapObject object : objects) { + // treat local coordinates as absolute for simplicity + int x = object.getLocalX(), y = object.getLocalY(); + int height = object.getHeight(); + + Position position = new Position(x, y, height); + Region region = regions.fromPosition(position); + region.addEntity(new StaticGameObject(world, object.getId(), position, object.getType(), + object.getOrientation()), false); + } + + collisionManager.build(false); + return collisionManager; + } + + /** + * Helper test assertion that a position is untraversable in a given direction. + * + * @param collisionManager The {@link CollisionManager} to check. + * @param position The {@link Position}. + * @param directions The {@link Direction}s to assert. + */ + private static void assertUntraversable(CollisionManager collisionManager, Position position, Direction... directions) { + for (Direction direction : directions) { + boolean traversable = collisionManager.traversable(position, EntityType.NPC, direction); + String message = String.format("Can walk %s from tile at (%d,%d), should not be able to", + direction.toString(), position.getX(), position.getY()); + + Assert.assertFalse(message, traversable); + } + } + + /** + * Helper test assertion that a position is traversable in a given direction. + * + * @param collisionManager The {@link CollisionManager} to check. + * @param position The {@link Position}. + * @param directions The {@link Direction}s to assert. + */ + private static void assertTraversable(CollisionManager collisionManager, Position position, Direction... directions) { + for (Direction direction : directions) { + boolean traversable = collisionManager.traversable(position, EntityType.NPC, direction); + String message = String.format("Cannot walk %s from tile at (%d,%d), should be able to", + direction.toString(), position.getX(), position.getY()); + + Assert.assertTrue(message, traversable); + } + } +}