mirror of
https://github.com/2006-Scape/apollo.git
synced 2026-07-03 00:38:21 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+49
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user