Merge pull request #222 from apollo-rsps/bugfix/collision-detection

Add support for building and updating collision flags on the fly
This commit is contained in:
Gary Tierney
2016-12-31 05:26:56 +00:00
committed by GitHub
31 changed files with 2821 additions and 771 deletions
@@ -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<Integer, MapDefinition> decode(IndexedFileSystem fs) throws IOException {
Archive archive = fs.getArchive(0, VERSIONS_ARCHIVE_FILE_ID);
ArchiveEntry entry = archive.getEntry("map_index");
Map<Integer, MapDefinition> 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;
}
}
+62
View File
@@ -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() {
}
}
+48
View File
@@ -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();
}
}
+123
View File
@@ -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.
* <p>
* This constructor expects the {@link ByteBuffer} to <strong>not</strong> 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();
}
}
+125
View File
@@ -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<Integer, MapIndex> indices;
/**
* Initialises the class with the specified set of indices.
*/
public static void init(Map<Integer, MapIndex> indices) {
MapIndex.indices = Collections.unmodifiableMap(indices);
}
/**
* Gets the {@code Map} of {@link MapIndex} instances.
*
* @return The map of {@link MapIndex} instances.
*/
public static Map<Integer, MapIndex> 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;
}
}
@@ -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<Integer, MapIndex> decode() throws IOException {
Archive archive = fs.getArchive(0, VERSIONS_ARCHIVE_FILE_ID);
ArchiveEntry entry = archive.getEntry("map_index");
Map<Integer, MapIndex> 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);
}
}
}
+119
View File
@@ -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;
}
}
@@ -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<MapObject> decode() {
List<MapObject> 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;
}
}
+90
View File
@@ -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> 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.
* <p>
* 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<Tile> getTiles() {
return Arrays.stream(tiles).flatMap(Arrays::stream);
}
}
+295
View File
@@ -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;
}
}
+161
View File
@@ -0,0 +1,161 @@
package org.apollo.cache.map;
/*
* Copyright (c) 2012-2013 Jonathan Edgecombe <jonathanedgecombe@gmail.com>
* 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() {
}
}
@@ -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<GameObject> 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<Integer, MapDefinition> 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();
}
}
@@ -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<Integer, MapIndex> 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);
}
}
}
}
}
@@ -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<Integer, MapIndex> mapIndices = MapIndex.getIndices();
try {
for (MapIndex index : mapIndices.values()) {
MapObjectsDecoder decoder = MapObjectsDecoder.create(fs, index);
List<MapObject> 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);
}
}
}
@@ -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");
}
}
}
@@ -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();
}
}
+39 -11
View File
@@ -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();
@@ -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 <strong>once</strong> per pulse.
@@ -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<RegionCoordinates, Region> regions = new HashMap<>();
/**
* A list of default {@link RegionListener}s which will be added to {@link Region}s upon creation.
*/
private final List<RegionListener> 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.
*
@@ -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);
}
/**
@@ -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> 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<Position> bridgeTiles = new TreeSet<>(POSITION_COMPARATOR);
/**
* A {@code SortedSet} of positions where the tile is completely blocked.
*/
private final SortedSet<Position> 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<Position, Collection<DirectionFlag>> flags = update.getFlags().asMap();
for (Map.Entry<Position, Collection<DirectionFlag>> flag : flags.entrySet()) {
Position position = flag.getKey();
Collection<DirectionFlag> 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;
}
}
@@ -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;
}
@@ -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<Position, DirectionFlag> flags;
public CollisionUpdate(CollisionUpdateType type, Multimap<Position, DirectionFlag> 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<Position, DirectionFlag> 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<Position, DirectionFlag> 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.
* <p>
* 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;
}
}
@@ -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
}
@@ -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);
}
}
@@ -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);
}
}
}
}
}
@@ -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.
* <p>
* 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.
* <p>
* 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<Position> find(Position origin, Position target) {
Map<Position, Node> nodes = new HashMap<>();
Node start = new Node(origin), end = new Node(target);
nodes.put(origin, start);
nodes.put(target, end);
Set<Node> open = new HashSet<>();
Queue<Node> 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<Position> 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<Node> open, Queue<Node> 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<Node> 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.
* <p>
* 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.
* <p>
* 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<Position> find(Position origin, Position target) {
Map<Position, Node> nodes = new HashMap<>();
Node start = new Node(origin), end = new Node(target);
nodes.put(origin, start);
nodes.put(target, end);
Set<Node> open = new HashSet<>();
Queue<Node> 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<Position> 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<Node> open, Queue<Node> 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<Node> nodes) {
Node node = nodes.peek();
while (!node.isOpen()) {
nodes.poll();
node = nodes.peek();
}
return node;
}
}
@@ -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;
}
}
@@ -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<Position[]> boundaries = Optional.empty();
@Override
public Deque<Position> find(Position origin, Position target) {
int approximation = (int) (origin.getLongestDelta(target) * 1.5);
Deque<Position> 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<Position> 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}.
* <p>
* This method:
* <ul>
* <li>Adds positions horizontally until we are either horizontally aligned with the target, or the next step is not
* traversable.
* <li>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.
* </ul>
*
* @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<Position> addHorizontal(Position start, Position target, Deque<Position> 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}.
* <p>
* This method:
* <ul>
* <li>Adds positions vertically until we are either vertically aligned with the target, or the next step is not
* traversable.
* <li>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.
* </ul>
*
* @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<Position> addVertical(Position start, Position target, Deque<Position> 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<Position[]> boundaries = Optional.empty();
@Override
public Deque<Position> find(Position origin, Position target) {
int approximation = (int) (origin.getLongestDelta(target) * 1.5);
Deque<Position> 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<Position> 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}.
* <p/>
* This method:
* <ul>
* <li>Adds positions horizontally until we are either horizontally aligned with the target, or the next step is not
* traversable.
* <li>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.
* </ul>
*
* @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<Position> addHorizontal(Position start, Position target, Deque<Position> 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}.
* <p/>
* This method:
* <ul>
* <li>Adds positions vertically until we are either vertically aligned with the target, or the next step is not
* traversable.
* <li>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.
* </ul>
*
* @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<Position> addVertical(Position start, Position target, Deque<Position> 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;
}
}
@@ -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);
}
/**
@@ -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:
* <pre>
* (0,2) |---------|
* | |
* | |
* | xxx |
* (0,1) |---------|
* | xxx |
* | |
* | |
* (0,0) |---------|
* | |
* | |
* | |
* |---------|
* </pre>
* <p>
* 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:
* <p>
* <pre>
* (0,2) |---------|---------|
* | | |
* | | |
* | |xxx |
* (0,1) |---------|---------|
* | xxx| |
* | | |
* | | |
* (0,0) |---------|---------|
* (1,0) (2,0) (3,0)
* </pre>
* <p>
* 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:
* <pre>
* (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)
* </pre>
* <p>
* 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);
}
}
}