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..72980add --- /dev/null +++ b/cache/src/main/org/apollo/cache/map/MapObject.java @@ -0,0 +1,119 @@ +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; + } + + /** + * 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. + * + * @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 deleted file mode 100644 index 68b7af03..00000000 --- a/game/src/main/org/apollo/game/fs/decoder/GameObjectDecoder.java +++ /dev/null @@ -1,283 +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.MapFileDecoder; -import org.apollo.cache.decoder.MapFileDecoder.MapDefinition; -import org.apollo.cache.decoder.ObjectDefinitionDecoder; -import org.apollo.cache.def.ObjectDefinition; -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(); - - try { - Map definitions = MapFileDecoder.decode(fs); - - for (MapDefinition definition : definitions.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.getTerrainFile()); - 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 1e66b211..39b54af7 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. * @@ -296,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/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. * 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 23c058fe..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]; } /** @@ -104,6 +109,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. * @@ -111,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); } /** @@ -123,7 +140,18 @@ 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, (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(); } /** @@ -135,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; } /** @@ -146,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; } /** @@ -159,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. @@ -168,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(); } /** @@ -189,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: @@ -234,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: - *

- * - * @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); } /** 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); + } + } +}