added sources for Slick

Former-commit-id: 1647fa32ef6894bd7db44f741f07c2f4dcdf9054
Former-commit-id: 0e5810dcfbe1fd59b13e7cabe9f1e93c5542da2d
This commit is contained in:
Song Minjae
2016-12-30 23:29:12 +09:00
parent d1f01a203d
commit d3080ffb78
329 changed files with 58400 additions and 7 deletions

View File

@@ -0,0 +1,29 @@
package org.newdawn.slick.util;
import org.newdawn.slick.AppGameContainer;
import org.newdawn.slick.Game;
/**
* Utility class to wrap up starting a game in a single line
*
* @author kevin
*/
public class Bootstrap {
/**
* Start the game as an application
*
* @param game The game to be started
* @param width The width of the window
* @param height The height of the window
* @param fullscreen True if the window should be fullscreen
*/
public static void runAsApplication(Game game, int width, int height, boolean fullscreen) {
try {
AppGameContainer container = new AppGameContainer(game, width, height, fullscreen);
container.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,162 @@
package org.newdawn.slick.util;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.newdawn.slick.opengl.ImageIOImageData;
import org.newdawn.slick.opengl.InternalTextureLoader;
import org.newdawn.slick.opengl.Texture;
import org.newdawn.slick.opengl.TextureImpl;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
/**
* This is a utility class that allows you to convert a BufferedImage into a
* texture.
*
* @author James Chambers (Jimmy)
* @author Jeremy Adams (elias_naur)
* @author Kevin Glass (kevglass)
*/
public class BufferedImageUtil {
/**
* Load a texture
*
* @param resourceName
* The location of the resource to load
* @param resourceImage
* The BufferedImage we are converting
* @return The loaded texture
* @throws IOException
* Indicates a failure to access the resource
*/
public static Texture getTexture(String resourceName,
BufferedImage resourceImage) throws IOException {
Texture tex = getTexture(resourceName, resourceImage,
SGL.GL_TEXTURE_2D, // target
SGL.GL_RGBA8, // dest pixel format
SGL.GL_LINEAR, // min filter (unused)
SGL.GL_LINEAR);
return tex;
}
/**
* Load a texture
*
* @param resourceName
* The location of the resource to load
* @param resourceImage
* The BufferedImage we are converting
* @return The loaded texture
* @throws IOException
* Indicates a failure to access the resource
*/
public static Texture getTexture(String resourceName,
BufferedImage resourceImage, int filter) throws IOException {
Texture tex = getTexture(resourceName, resourceImage,
SGL.GL_TEXTURE_2D, // target
SGL.GL_RGBA8, // dest pixel format
filter, // min filter (unused)
filter);
return tex;
}
/**
* Load a texture into OpenGL from a BufferedImage
*
* @param resourceName
* The location of the resource to load
* @param resourceimage
* The BufferedImage we are converting
* @param target
* The GL target to load the texture against
* @param dstPixelFormat
* The pixel format of the screen
* @param minFilter
* The minimising filter
* @param magFilter
* The magnification filter
* @return The loaded texture
* @throws IOException
* Indicates a failure to access the resource
*/
public static Texture getTexture(String resourceName,
BufferedImage resourceimage, int target, int dstPixelFormat,
int minFilter, int magFilter) throws IOException {
ImageIOImageData data = new ImageIOImageData();int srcPixelFormat = 0;
// create the texture ID for this texture
int textureID = InternalTextureLoader.createTextureID();
TextureImpl texture = new TextureImpl(resourceName, target, textureID);
// Enable texturing
Renderer.get().glEnable(SGL.GL_TEXTURE_2D);
// bind this texture
Renderer.get().glBindTexture(target, textureID);
BufferedImage bufferedImage = resourceimage;
texture.setWidth(bufferedImage.getWidth());
texture.setHeight(bufferedImage.getHeight());
if (bufferedImage.getColorModel().hasAlpha()) {
srcPixelFormat = SGL.GL_RGBA;
} else {
srcPixelFormat = SGL.GL_RGB;
}
// convert that image into a byte buffer of texture data
ByteBuffer textureBuffer = data.imageToByteBuffer(bufferedImage, false, false, null);
texture.setTextureHeight(data.getTexHeight());
texture.setTextureWidth(data.getTexWidth());
texture.setAlpha(data.getDepth() == 32);
if (target == SGL.GL_TEXTURE_2D) {
Renderer.get().glTexParameteri(target, SGL.GL_TEXTURE_MIN_FILTER, minFilter);
Renderer.get().glTexParameteri(target, SGL.GL_TEXTURE_MAG_FILTER, magFilter);
if (Renderer.get().canTextureMirrorClamp()) {
Renderer.get().glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_S, SGL.GL_MIRROR_CLAMP_TO_EDGE_EXT);
Renderer.get().glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_T, SGL.GL_MIRROR_CLAMP_TO_EDGE_EXT);
} else {
Renderer.get().glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_S, SGL.GL_CLAMP);
Renderer.get().glTexParameteri(SGL.GL_TEXTURE_2D, SGL.GL_TEXTURE_WRAP_T, SGL.GL_CLAMP);
}
}
Renderer.get().glTexImage2D(target,
0,
dstPixelFormat,
texture.getTextureWidth(),
texture.getTextureHeight(),
0,
srcPixelFormat,
SGL.GL_UNSIGNED_BYTE,
textureBuffer);
return texture;
}
/**
* Implement of transform copy area for 1.4
*
* @param image The image to copy
* @param x The x position to copy to
* @param y The y position to copy to
* @param width The width of the image
* @param height The height of the image
* @param dx The transform on the x axis
* @param dy The transform on the y axis
*/
private static void copyArea(BufferedImage image, int x, int y, int width, int height, int dx, int dy) {
Graphics2D g = (Graphics2D) image.getGraphics();
g.drawImage(image.getSubimage(x, y, width, height),x+dx,y+dy,null);
}
}

View File

@@ -0,0 +1,28 @@
package org.newdawn.slick.util;
import java.io.InputStream;
import java.net.URL;
/**
* A resource location that searches the classpath
*
* @author kevin
*/
public class ClasspathLocation implements ResourceLocation {
/**
* @see org.newdawn.slick.util.ResourceLocation#getResource(java.lang.String)
*/
public URL getResource(String ref) {
String cpRef = ref.replace('\\', '/');
return ResourceLoader.class.getClassLoader().getResource(cpRef);
}
/**
* @see org.newdawn.slick.util.ResourceLocation#getResourceAsStream(java.lang.String)
*/
public InputStream getResourceAsStream(String ref) {
String cpRef = ref.replace('\\', '/');
return ResourceLoader.class.getClassLoader().getResourceAsStream(cpRef);
}
}

View File

@@ -0,0 +1,82 @@
package org.newdawn.slick.util;
import java.io.PrintStream;
import java.util.Date;
/**
* The default implementation that just spits the messages out to stdout
*
* @author kevin
*/
public class DefaultLogSystem implements LogSystem {
/** The output stream for dumping the log out on */
public static PrintStream out = System.out;
/**
* Log an error
*
* @param message The message describing the error
* @param e The exception causing the error
*/
public void error(String message, Throwable e) {
error(message);
error(e);
}
/**
* Log an error
*
* @param e The exception causing the error
*/
public void error(Throwable e) {
out.println(new Date()+" ERROR:" +e.getMessage());
e.printStackTrace(out);
}
/**
* Log an error
*
* @param message The message describing the error
*/
public void error(String message) {
out.println(new Date()+" ERROR:" +message);
}
/**
* Log a warning
*
* @param message The message describing the warning
*/
public void warn(String message) {
out.println(new Date()+" WARN:" +message);
}
/**
* Log an information message
*
* @param message The message describing the infomation
*/
public void info(String message) {
out.println(new Date()+" INFO:" +message);
}
/**
* Log a debug message
*
* @param message The message describing the debug
*/
public void debug(String message) {
out.println(new Date()+" DEBUG:" +message);
}
/**
* Log a warning with an exception that caused it
*
* @param message The message describing the warning
* @param e The cause of the warning
*/
public void warn(String message, Throwable e) {
warn(message);
e.printStackTrace(out);
}
}

View File

@@ -0,0 +1,56 @@
package org.newdawn.slick.util;
/**
* Utility to handle Java's odd trig performance issues
*
* @author JeffK
*/
public class FastTrig {
/**
* Fast Trig functions for x86.
* This forces the trig functiosn to stay within the safe area on the x86 processor (-45 degrees to +45 degrees)
* The results may be very slightly off from what the Math and StrictMath trig functions give due to
* rounding in the angle reduction but it will be very very close.
*
* @param radians The original angle
* @return The reduced Sin angle
*/
private static double reduceSinAngle(double radians) {
double orig = radians;
radians %= Math.PI * 2.0; // put us in -2PI to +2PI space
if (Math.abs(radians) > Math.PI) { // put us in -PI to +PI space
radians = radians - (Math.PI * 2.0);
}
if (Math.abs(radians) > Math.PI / 2) {// put us in -PI/2 to +PI/2 space
radians = Math.PI - radians;
}
return radians;
}
/**
* Get the sine of an angle
*
* @param radians The angle
* @return The sine of the angle
*/
public static double sin(double radians) {
radians = reduceSinAngle(radians); // limits angle to between -PI/2 and +PI/2
if (Math.abs(radians) <= Math.PI / 4) {
return Math.sin(radians);
} else {
return Math.cos(Math.PI / 2 - radians);
}
}
/**
* Get the cosine of an angle
*
* @param radians The angle
* @return The cosine of the angle
*/
public static double cos(double radians) {
return sin(radians + Math.PI / 2);
}
}

View File

@@ -0,0 +1,61 @@
package org.newdawn.slick.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
/**
* A resource loading location that searches somewhere on the classpath
*
* @author kevin
*/
public class FileSystemLocation implements ResourceLocation {
/** The root of the file system to search */
private File root;
/**
* Create a new resoruce location based on the file system
*
* @param root The root of the file system to search
*/
public FileSystemLocation(File root) {
this.root = root;
}
/**
* @see ResourceLocation#getResource(String)
*/
public URL getResource(String ref) {
try {
File file = new File(root, ref);
if (!file.exists()) {
file = new File(ref);
}
if (!file.exists()) {
return null;
}
return file.toURI().toURL();
} catch (IOException e) {
return null;
}
}
/**
* @see ResourceLocation#getResourceAsStream(String)
*/
public InputStream getResourceAsStream(String ref) {
try {
File file = new File(root, ref);
if (!file.exists()) {
file = new File(ref);
}
return new FileInputStream(file);
} catch (IOException e) {
return null;
}
}
}

View File

@@ -0,0 +1,220 @@
package org.newdawn.slick.util;
import java.awt.Graphics2D;
import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
/**
* Simple utility class to support justified text
*
* http://slick.javaunlimited.net/viewtopic.php?t=2640
*
* @author zenzei
*/
public class FontUtils {
/**
* Alignment indicators
*/
public class Alignment {
/** Left alignment */
public static final int LEFT = 1;
/** Center alignment */
public static final int CENTER = 2;
/** Right alignment */
public static final int RIGHT = 3;
/** Justify alignment */
public static final int JUSTIFY = 4;
}
/**
* Draw text left justified
*
* @param font The font to draw with
* @param s The string to draw
* @param x The x location to draw at
* @param y The y location to draw at
*/
public static void drawLeft(Font font, String s, int x, int y) {
drawString(font, s, Alignment.LEFT, x, y, 0, Color.white);
}
/**
* Draw text center justified
*
* @param font The font to draw with
* @param s The string to draw
* @param x The x location to draw at
* @param y The y location to draw at
* @param width The width to fill with the text
*/
public static void drawCenter(Font font, String s, int x, int y, int width) {
drawString(font, s, Alignment.CENTER, x, y, width, Color.white);
}
/**
* Draw text center justified
*
* @param font The font to draw with
* @param s The string to draw
* @param x The x location to draw at
* @param y The y location to draw at
* @param width The width to fill with the text
* @param color The color to draw in
*/
public static void drawCenter(Font font, String s, int x, int y, int width,
Color color) {
drawString(font, s, Alignment.CENTER, x, y, width, color);
}
/**
* Draw text right justified
*
* @param font The font to draw with
* @param s The string to draw
* @param x The x location to draw at
* @param y The y location to draw at
* @param width The width to fill with the text
*/
public static void drawRight(Font font, String s, int x, int y, int width) {
drawString(font, s, Alignment.RIGHT, x, y, width, Color.white);
}
/**
* Draw text right justified
*
* @param font The font to draw with
* @param s The string to draw
* @param x The x location to draw at
* @param y The y location to draw at
* @param width The width to fill with the text
* @param color The color to draw in
*/
public static void drawRight(Font font, String s, int x, int y, int width,
Color color) {
drawString(font, s, Alignment.RIGHT, x, y, width, color);
}
/**
* Draw a string
*
* @param font The font to draw with
* @param s The text to draw
* @param alignment The alignment to apply
* @param x The x location to draw at
* @param y The y location to draw at
* @param width The width to fill with the string
* @param color The color to draw in
* @return The final x coordinate of the text
*/
public static final int drawString(Font font, final String s,
final int alignment, final int x, final int y, final int width,
Color color) {
int resultingXCoordinate = 0;
if (alignment == Alignment.LEFT) {
font.drawString(x, y, s, color);
} else if (alignment == Alignment.CENTER) {
font.drawString(x + (width / 2) - (font.getWidth(s) / 2), y, s,
color);
} else if (alignment == Alignment.RIGHT) {
font.drawString(x + width - font.getWidth(s), y, s, color);
} else if (alignment == Alignment.JUSTIFY) {
// calculate left width
int leftWidth = width - font.getWidth(s);
if (leftWidth <= 0) {
// no width left, use standard draw string
font.drawString(x, y, s, color);
}
return FontUtils.drawJustifiedSpaceSeparatedSubstrings(font, s, x,
y, FontUtils.calculateWidthOfJustifiedSpaceInPixels(font,
s, leftWidth));
}
return resultingXCoordinate;
}
/**
* Calculates and returns the width of a single justified space for the
* given {@link String}, in pixels.
*
* @param font The font to draw with
* @param s
* The given non-null {@link String} to use to calculate the
* width of a space for.
* @param leftWidth
* The integer specifying the left width buffer to use to
* calculate how much space a space should take up in
* justification.
* @return The width of a single justified space for the given
* {@link String}, in pixels.
*/
private static int calculateWidthOfJustifiedSpaceInPixels(final Font font,
final String s, final int leftWidth) {
int space = 0; // hold total space; hold space width in pixel
int curpos = 0; // current string position
// count total space
while (curpos < s.length()) {
if (s.charAt(curpos++) == ' ') {
space++;
}
}
if (space > 0) {
// width left plus with total space
// space width (in pixel) = width left / total space
space = (leftWidth + (font.getWidth(" ") * space)) / space;
}
return space;
}
/**
* Draws justified-space separated substrings based on the given
* {@link String} and the given starting x and y coordinates to the given
* {@link Graphics2D} instance.
*
* @param font
* The font to draw with
* @param s
* The non-null {@link String} to draw as space-separated
* substrings.
* @param x
* The given starting x-coordinate position to use to draw the
* {@link String}.
* @param y
* The given starting y-coordinate position to use to draw the
* {@link String}.
* @param justifiedSpaceWidth
* The integer specifying the width of a justified space
* {@link String}, in pixels.
* @return The resulting x-coordinate of the current cursor after the
* drawing operation completes.
* @throws NullPointerException
* Throws a {@link NullPointerException} if any of the given
* arguments are null.
*/
private static int drawJustifiedSpaceSeparatedSubstrings(Font font,
final String s, final int x, final int y,
final int justifiedSpaceWidth) {
int curpos = 0;
int endpos = 0;
int resultingXCoordinate = x;
while (curpos < s.length()) {
endpos = s.indexOf(' ', curpos); // find space
if (endpos == -1) {
endpos = s.length(); // no space, draw all string directly
}
String substring = s.substring(curpos, endpos);
font.drawString(resultingXCoordinate, y, substring);
resultingXCoordinate += font.getWidth(substring)
+ justifiedSpaceWidth; // increase
// x-coordinate
curpos = endpos + 1;
}
return resultingXCoordinate;
}
}

View File

@@ -0,0 +1,154 @@
package org.newdawn.slick.util;
import org.newdawn.slick.Input;
import org.newdawn.slick.InputListener;
/**
* An implement implementation of the InputListener interface
*
* @author kevin
*/
public class InputAdapter implements InputListener {
/** A flag to indicate if we're accepting input here */
private boolean acceptingInput = true;
/**
* @see org.newdawn.slick.InputListener#controllerButtonPressed(int, int)
*/
public void controllerButtonPressed(int controller, int button) {
}
/**
* @see org.newdawn.slick.InputListener#controllerButtonReleased(int, int)
*/
public void controllerButtonReleased(int controller, int button) {
}
/**
* @see org.newdawn.slick.InputListener#controllerDownPressed(int)
*/
public void controllerDownPressed(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerDownReleased(int)
*/
public void controllerDownReleased(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerLeftPressed(int)
*/
public void controllerLeftPressed(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerLeftReleased(int)
*/
public void controllerLeftReleased(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerRightPressed(int)
*/
public void controllerRightPressed(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerRightReleased(int)
*/
public void controllerRightReleased(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerUpPressed(int)
*/
public void controllerUpPressed(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#controllerUpReleased(int)
*/
public void controllerUpReleased(int controller) {
}
/**
* @see org.newdawn.slick.InputListener#inputEnded()
*/
public void inputEnded() {
}
/**
* @see org.newdawn.slick.InputListener#isAcceptingInput()
*/
public boolean isAcceptingInput() {
return acceptingInput;
}
/**
* Indicate if we should be accepting input of any sort
*
* @param acceptingInput True if we should accept input
*/
public void setAcceptingInput(boolean acceptingInput) {
this.acceptingInput = acceptingInput;
}
/**
* @see org.newdawn.slick.InputListener#keyPressed(int, char)
*/
public void keyPressed(int key, char c) {
}
/**
* @see org.newdawn.slick.InputListener#keyReleased(int, char)
*/
public void keyReleased(int key, char c) {
}
/**
* @see org.newdawn.slick.InputListener#mouseMoved(int, int, int, int)
*/
public void mouseMoved(int oldx, int oldy, int newx, int newy) {
}
/**
* @see org.newdawn.slick.InputListener#mousePressed(int, int, int)
*/
public void mousePressed(int button, int x, int y) {
}
/**
* @see org.newdawn.slick.InputListener#mouseReleased(int, int, int)
*/
public void mouseReleased(int button, int x, int y) {
}
/**
* @see org.newdawn.slick.InputListener#mouseWheelMoved(int)
*/
public void mouseWheelMoved(int change) {
}
/**
* @see org.newdawn.slick.InputListener#setInput(org.newdawn.slick.Input)
*/
public void setInput(Input input) {
}
/**
* @see org.newdawn.slick.InputListener#mouseClicked(int, int, int, int)
*/
public void mouseClicked(int button, int x, int y, int clickCount) {
}
public void mouseDragged(int oldx, int oldy, int newx, int newy) {
}
/**
* @see org.newdawn.slick.ControlledInputReciever#inputStarted()
*/
public void inputStarted() {
}
}

View File

@@ -0,0 +1,137 @@
package org.newdawn.slick.util;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
/**
* An image along with state information that allows it to be drawn without
* specifing the state in which to render.
*
* @author kevin
*/
public class LocatedImage {
/** The image to be rendered - prefer aggregation */
private Image image;
/** The x coordinate at which the image should be rendered */
private int x;
/** The y coordinate at which the image should be rendered */
private int y;
/** The filter to apply across the image */
private Color filter = Color.white;
/** The width to render the image */
private float width;
/** The height to render the image */
private float height;
/**
* Create a new located image
*
* @param image The image to be drawn
* @param x The x location at which the image should be drawn
* @param y The y location at which the image should be drawn
*/
public LocatedImage(Image image, int x, int y) {
this.image = image;
this.x = x;
this.y = y;
this.width = image.getWidth();
this.height = image.getHeight();
}
/**
* Get the height the image will be drawn at
*
* @return The height
*/
public float getHeight() {
return height;
}
/**
* Get the width the image will be drawn at
*
* @return The width
*/
public float getWidth() {
return width;
}
/**
* Set the height the image should be drawn at
*
* @param height The height the image should be drawn at
*/
public void setHeight(float height) {
this.height = height;
}
/**
* Set the width the image should be drawn at
*
* @param width The width the image should be drawn at
*/
public void setWidth(float width) {
this.width = width;
}
/**
* Set the colour filter to apply to the image
*
* @param c The color filter to apply to the image
*/
public void setColor(Color c) {
this.filter = c;
}
/**
* Get the colour filter being applied
*
* @return The color the being applied
*/
public Color getColor() {
return filter;
}
/**
* Set the x position at which the image should be drawn
*
* @param x The x coordinate of the position
*/
public void setX(int x) {
this.x = x;
}
/**
* Set the y position at which the image should be drawn
*
* @param y The y coordinate of the position
*/
public void setY(int y) {
this.y = y;
}
/**
* Get the x position at which the image will be drawn
*
* @return The x position at which the image will be drawn
*/
public int getX() {
return x;
}
/**
* Get the y position at which the image will be drawn
*
* @return The y position at which the image will be drawn
*/
public int getY() {
return y;
}
/**
* Draw the image based on the current configured state
*/
public void draw() {
image.draw(x,y,width,height,filter);
}
}

View File

@@ -0,0 +1,161 @@
package org.newdawn.slick.util;
import java.security.AccessController;
import java.security.PrivilegedAction;
/**
* A simple central logging system
*
* @author kevin
*/
public final class Log {
/** True if we're doing verbose logging INFO and DEBUG */
private static boolean verbose = true;
/** true if activated by the system property "org.newdawn.slick.forceVerboseLog" */
private static boolean forcedVerbose = false;
/**
* The debug property which can be set via JNLP or startup parameter to switch
* logging mode to verbose for games that were released without verbose logging
* value must be "true"
*/
private static final String forceVerboseProperty = "org.newdawn.slick.forceVerboseLog";
/**
* the verbose property must be set to "true" to switch on verbose logging
*/
private static final String forceVerbosePropertyOnValue = "true";
/** The log system plugin in use */
private static LogSystem logSystem = new DefaultLogSystem();
/**
* The log is a simple static utility, no construction
*/
private Log() {
}
/**
* Set the log system that will have all of the log info
* sent to it.
*
* @param system The system to use for logging.
*/
public static void setLogSystem(LogSystem system) {
logSystem = system;
}
/**
* Indicate that we want verbose logging.
* The call is ignored if verbose logging is forced by the system property
* "org.newdawn.slick.forceVerboseLog"
*
* @param v True if we want verbose logging (INFO and DEBUG)
*/
public static void setVerbose(boolean v) {
if (forcedVerbose)
return;
verbose = v;
}
/**
* Check if the system property org.newdawn.slick.verboseLog is set to true.
* If this is the case we activate the verbose logging mode
*/
public static void checkVerboseLogSetting() {
try {
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
String val = System.getProperty(Log.forceVerboseProperty);
if ((val != null) && (val.equalsIgnoreCase(Log.forceVerbosePropertyOnValue))) {
Log.setForcedVerboseOn();
}
return null;
}
});
} catch (Throwable e) {
// ignore, security failure - probably an applet
}
}
/**
* Indicate that we want verbose logging, even if switched off in game code.
* Only be called when system property "org.newdawn.slick.forceVerboseLog" is set to true.
* You must not call this method directly.
*/
public static void setForcedVerboseOn() {
forcedVerbose = true;
verbose = true;
}
/**
* Log an error
*
* @param message The message describing the error
* @param e The exception causing the error
*/
public static void error(String message, Throwable e) {
logSystem.error(message, e);
}
/**
* Log an error
*
* @param e The exception causing the error
*/
public static void error(Throwable e) {
logSystem.error(e);
}
/**
* Log an error
*
* @param message The message describing the error
*/
public static void error(String message) {
logSystem.error(message);
}
/**
* Log a warning
*
* @param message The message describing the warning
*/
public static void warn(String message) {
logSystem.warn(message);
}
/**
* Log a warning
*
* @param message The message describing the warning
* @param e The issue causing the warning
*/
public static void warn(String message, Throwable e) {
logSystem.warn(message, e);
}
/**
* Log an information message
*
* @param message The message describing the infomation
*/
public static void info(String message) {
if (verbose || forcedVerbose) {
logSystem.info(message);
}
}
/**
* Log a debug message
*
* @param message The message describing the debug
*/
public static void debug(String message) {
if (verbose || forcedVerbose) {
logSystem.debug(message);
}
}
}

View File

@@ -0,0 +1,60 @@
package org.newdawn.slick.util;
/**
* Plugin in interface for the logging of Slick
*
* @author kevin
*/
public interface LogSystem {
/**
* Log an error
*
* @param message The message describing the error
* @param e The exception causing the error
*/
public void error(String message, Throwable e);
/**
* Log an error
*
* @param e The exception causing the error
*/
public void error(Throwable e);
/**
* Log an error
*
* @param message The message describing the error
*/
public void error(String message);
/**
* Log a warning
*
* @param message The message describing the warning
*/
public void warn(String message);
/**
* Log a warning
*
* @param message The message describing the warning
* @param e The cause of the warning
*/
public void warn(String message, Throwable e);
/**
* Log an information message
*
* @param message The message describing the infomation
*/
public void info(String message);
/**
* Log a debug message
*
* @param message The message describing the debug
*/
public void debug(String message);
}

View File

@@ -0,0 +1,62 @@
package org.newdawn.slick.util;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
/**
* A utility to provide full screen masking
*
* @author kevin
*/
public class MaskUtil {
/** The renderer to use for all GL operations */
protected static SGL GL = Renderer.get();
/**
* Start defining the screen mask. After calling this use graphics functions to
* mask out the area
*/
public static void defineMask() {
GL.glDepthMask(true);
GL.glClearDepth(1);
GL.glClear(SGL.GL_DEPTH_BUFFER_BIT);
GL.glDepthFunc(SGL.GL_ALWAYS);
GL.glEnable(SGL.GL_DEPTH_TEST);
GL.glDepthMask(true);
GL.glColorMask(false, false, false, false);
}
/**
* Finish defining the screen mask
*/
public static void finishDefineMask() {
GL.glDepthMask(false);
GL.glColorMask(true, true, true, true);
}
/**
* Start drawing only on the masked area
*/
public static void drawOnMask() {
GL.glDepthFunc(SGL.GL_EQUAL);
}
/**
* Start drawing only off the masked area
*/
public static void drawOffMask() {
GL.glDepthFunc(SGL.GL_NOTEQUAL);
}
/**
* Reset the masked area - should be done after you've finished rendering
*/
public static void resetMask() {
GL.glDepthMask(true);
GL.glClearDepth(0);
GL.glClear(SGL.GL_DEPTH_BUFFER_BIT);
GL.glDepthMask(false);
GL.glDisable(SGL.GL_DEPTH_TEST);
}
}

View File

@@ -0,0 +1,18 @@
package org.newdawn.slick.util;
/**
* Thrown to indicate that a limited implementation of a class can not
* support the operation requested.
*
* @author kevin
*/
public class OperationNotSupportedException extends RuntimeException {
/**
* Create a new exception
*
* @param msg The message describing the limitation
*/
public OperationNotSupportedException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,120 @@
package org.newdawn.slick.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
/**
* A simple wrapper around resource loading should anyone decide to change
* their minds how this is meant to work in the future.
*
* @author Kevin Glass
*/
public class ResourceLoader {
/** The list of locations to be searched */
private static ArrayList locations = new ArrayList();
static {
locations.add(new ClasspathLocation());
locations.add(new FileSystemLocation(new File(".")));
}
/**
* Add a location that will be searched for resources
*
* @param location The location that will be searched for resoruces
*/
public static void addResourceLocation(ResourceLocation location) {
locations.add(location);
}
/**
* Remove a location that will be no longer be searched for resources
*
* @param location The location that will be removed from the search list
*/
public static void removeResourceLocation(ResourceLocation location) {
locations.remove(location);
}
/**
* Remove all the locations, no resources will be found until
* new locations have been added
*/
public static void removeAllResourceLocations() {
locations.clear();
}
/**
* Get a resource
*
* @param ref The reference to the resource to retrieve
* @return A stream from which the resource can be read
*/
public static InputStream getResourceAsStream(String ref) {
InputStream in = null;
for (int i=0;i<locations.size();i++) {
ResourceLocation location = (ResourceLocation) locations.get(i);
in = location.getResourceAsStream(ref);
if (in != null) {
break;
}
}
if (in == null)
{
throw new RuntimeException("Resource not found: "+ref);
}
return new BufferedInputStream(in);
}
/**
* Check if a resource is available from any given resource loader
*
* @param ref A reference to the resource that should be checked
* @return True if the resource can be located
*/
public static boolean resourceExists(String ref) {
URL url = null;
for (int i=0;i<locations.size();i++) {
ResourceLocation location = (ResourceLocation) locations.get(i);
url = location.getResource(ref);
if (url != null) {
return true;
}
}
return false;
}
/**
* Get a resource as a URL
*
* @param ref The reference to the resource to retrieve
* @return A URL from which the resource can be read
*/
public static URL getResource(String ref) {
URL url = null;
for (int i=0;i<locations.size();i++) {
ResourceLocation location = (ResourceLocation) locations.get(i);
url = location.getResource(ref);
if (url != null) {
break;
}
}
if (url == null)
{
throw new RuntimeException("Resource not found: "+ref);
}
return url;
}
}

View File

@@ -0,0 +1,29 @@
package org.newdawn.slick.util;
import java.io.InputStream;
import java.net.URL;
/**
* A location from which resources can be loaded
*
* @author kevin
*/
public interface ResourceLocation {
/**
* Get a resource as an input stream
*
* @param ref The reference to the resource to retrieve
* @return A stream from which the resource can be read or
* null if the resource can't be found in this location
*/
public InputStream getResourceAsStream(String ref);
/**
* Get a resource as a URL
*
* @param ref The reference to the resource to retrieve
* @return A URL from which the resource can be read
*/
public URL getResource(String ref);
}

View File

@@ -0,0 +1,3 @@
<BODY>
Utilities to support the library. Basically anything that didn't fit elsewhere.
</BODY>

View File

@@ -0,0 +1,27 @@
package org.newdawn.slick.util.pathfinding;
/**
* The description of a class providing a cost for a given tile based
* on a target location and entity being moved. This heuristic controls
* what priority is placed on different tiles during the search for a path
*
* @author Kevin Glass
*/
public interface AStarHeuristic {
/**
* Get the additional heuristic cost of the given tile. This controls the
* order in which tiles are searched while attempting to find a path to the
* target location. The lower the cost the more likely the tile will
* be searched.
*
* @param map The map on which the path is being found
* @param mover The entity that is moving along the path
* @param x The x coordinate of the tile being evaluated
* @param y The y coordinate of the tile being evaluated
* @param tx The x coordinate of the target location
* @param ty Teh y coordinate of the target location
* @return The cost associated with the given tile
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx, int ty);
}

View File

@@ -0,0 +1,590 @@
package org.newdawn.slick.util.pathfinding;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.newdawn.slick.util.pathfinding.heuristics.ClosestHeuristic;
/**
* A path finder implementation that uses the AStar heuristic based algorithm
* to determine a path.
*
* @author Kevin Glass
*/
public class AStarPathFinder implements PathFinder, PathFindingContext {
/** The set of nodes that have been searched through */
private ArrayList closed = new ArrayList();
/** The set of nodes that we do not yet consider fully searched */
private PriorityList open = new PriorityList();
/** The map being searched */
private TileBasedMap map;
/** The maximum depth of search we're willing to accept before giving up */
private int maxSearchDistance;
/** The complete set of nodes across the map */
private Node[][] nodes;
/** True if we allow diaganol movement */
private boolean allowDiagMovement;
/** The heuristic we're applying to determine which nodes to search first */
private AStarHeuristic heuristic;
/** The node we're currently searching from */
private Node current;
/** The mover going through the path */
private Mover mover;
/** The x coordinate of the source tile we're moving from */
private int sourceX;
/** The y coordinate of the source tile we're moving from */
private int sourceY;
/** The distance searched so far */
private int distance;
/**
* Create a path finder with the default heuristic - closest to target.
*
* @param map The map to be searched
* @param maxSearchDistance The maximum depth we'll search before giving up
* @param allowDiagMovement True if the search should try diaganol movement
*/
public AStarPathFinder(TileBasedMap map, int maxSearchDistance, boolean allowDiagMovement) {
this(map, maxSearchDistance, allowDiagMovement, new ClosestHeuristic());
}
/**
* Create a path finder
*
* @param heuristic The heuristic used to determine the search order of the map
* @param map The map to be searched
* @param maxSearchDistance The maximum depth we'll search before giving up
* @param allowDiagMovement True if the search should try diaganol movement
*/
public AStarPathFinder(TileBasedMap map, int maxSearchDistance,
boolean allowDiagMovement, AStarHeuristic heuristic) {
this.heuristic = heuristic;
this.map = map;
this.maxSearchDistance = maxSearchDistance;
this.allowDiagMovement = allowDiagMovement;
nodes = new Node[map.getWidthInTiles()][map.getHeightInTiles()];
for (int x=0;x<map.getWidthInTiles();x++) {
for (int y=0;y<map.getHeightInTiles();y++) {
nodes[x][y] = new Node(x,y);
}
}
}
/**
* @see PathFinder#findPath(Mover, int, int, int, int)
*/
public Path findPath(Mover mover, int sx, int sy, int tx, int ty) {
current = null;
// easy first check, if the destination is blocked, we can't get there
this.mover = mover;
this.sourceX = tx;
this.sourceY = ty;
this.distance = 0;
if (map.blocked(this, tx, ty)) {
return null;
}
for (int x=0;x<map.getWidthInTiles();x++) {
for (int y=0;y<map.getHeightInTiles();y++) {
nodes[x][y].reset();
}
}
// initial state for A*. The closed group is empty. Only the starting
// tile is in the open list and it's cost is zero, i.e. we're already there
nodes[sx][sy].cost = 0;
nodes[sx][sy].depth = 0;
closed.clear();
open.clear();
addToOpen(nodes[sx][sy]);
nodes[tx][ty].parent = null;
// while we haven't found the goal and haven't exceeded our max search depth
int maxDepth = 0;
while ((maxDepth < maxSearchDistance) && (open.size() != 0)) {
// pull out the first node in our open list, this is determined to
// be the most likely to be the next step based on our heuristic
int lx = sx;
int ly = sy;
if (current != null) {
lx = current.x;
ly = current.y;
}
current = getFirstInOpen();
distance = current.depth;
if (current == nodes[tx][ty]) {
if (isValidLocation(mover,lx,ly,tx,ty)) {
break;
}
}
removeFromOpen(current);
addToClosed(current);
// search through all the neighbours of the current node evaluating
// them as next steps
for (int x=-1;x<2;x++) {
for (int y=-1;y<2;y++) {
// not a neighbour, its the current tile
if ((x == 0) && (y == 0)) {
continue;
}
// if we're not allowing diaganol movement then only
// one of x or y can be set
if (!allowDiagMovement) {
if ((x != 0) && (y != 0)) {
continue;
}
}
// determine the location of the neighbour and evaluate it
int xp = x + current.x;
int yp = y + current.y;
if (isValidLocation(mover,current.x,current.y,xp,yp)) {
// the cost to get to this node is cost the current plus the movement
// cost to reach this node. Note that the heursitic value is only used
// in the sorted open list
float nextStepCost = current.cost + getMovementCost(mover, current.x, current.y, xp, yp);
Node neighbour = nodes[xp][yp];
map.pathFinderVisited(xp, yp);
// if the new cost we've determined for this node is lower than
// it has been previously makes sure the node hasn't been discarded. We've
// determined that there might have been a better path to get to
// this node so it needs to be re-evaluated
if (nextStepCost < neighbour.cost) {
if (inOpenList(neighbour)) {
removeFromOpen(neighbour);
}
if (inClosedList(neighbour)) {
removeFromClosed(neighbour);
}
}
// if the node hasn't already been processed and discarded then
// reset it's cost to our current cost and add it as a next possible
// step (i.e. to the open list)
if (!inOpenList(neighbour) && !(inClosedList(neighbour))) {
neighbour.cost = nextStepCost;
neighbour.heuristic = getHeuristicCost(mover, xp, yp, tx, ty);
maxDepth = Math.max(maxDepth, neighbour.setParent(current));
addToOpen(neighbour);
}
}
}
}
}
// since we've got an empty open list or we've run out of search
// there was no path. Just return null
if (nodes[tx][ty].parent == null) {
return null;
}
// At this point we've definitely found a path so we can uses the parent
// references of the nodes to find out way from the target location back
// to the start recording the nodes on the way.
Path path = new Path();
Node target = nodes[tx][ty];
while (target != nodes[sx][sy]) {
path.prependStep(target.x, target.y);
target = target.parent;
}
path.prependStep(sx,sy);
// thats it, we have our path
return path;
}
/**
* Get the X coordinate of the node currently being evaluated
*
* @return The X coordinate of the node currently being evaluated
*/
public int getCurrentX() {
if (current == null) {
return -1;
}
return current.x;
}
/**
* Get the Y coordinate of the node currently being evaluated
*
* @return The Y coordinate of the node currently being evaluated
*/
public int getCurrentY() {
if (current == null) {
return -1;
}
return current.y;
}
/**
* Get the first element from the open list. This is the next
* one to be searched.
*
* @return The first element in the open list
*/
protected Node getFirstInOpen() {
return (Node) open.first();
}
/**
* Add a node to the open list
*
* @param node The node to be added to the open list
*/
protected void addToOpen(Node node) {
node.setOpen(true);
open.add(node);
}
/**
* Check if a node is in the open list
*
* @param node The node to check for
* @return True if the node given is in the open list
*/
protected boolean inOpenList(Node node) {
return node.isOpen();
}
/**
* Remove a node from the open list
*
* @param node The node to remove from the open list
*/
protected void removeFromOpen(Node node) {
node.setOpen(false);
open.remove(node);
}
/**
* Add a node to the closed list
*
* @param node The node to add to the closed list
*/
protected void addToClosed(Node node) {
node.setClosed(true);
closed.add(node);
}
/**
* Check if the node supplied is in the closed list
*
* @param node The node to search for
* @return True if the node specified is in the closed list
*/
protected boolean inClosedList(Node node) {
return node.isClosed();
}
/**
* Remove a node from the closed list
*
* @param node The node to remove from the closed list
*/
protected void removeFromClosed(Node node) {
node.setClosed(false);
closed.remove(node);
}
/**
* Check if a given location is valid for the supplied mover
*
* @param mover The mover that would hold a given location
* @param sx The starting x coordinate
* @param sy The starting y coordinate
* @param x The x coordinate of the location to check
* @param y The y coordinate of the location to check
* @return True if the location is valid for the given mover
*/
protected boolean isValidLocation(Mover mover, int sx, int sy, int x, int y) {
boolean invalid = (x < 0) || (y < 0) || (x >= map.getWidthInTiles()) || (y >= map.getHeightInTiles());
if ((!invalid) && ((sx != x) || (sy != y))) {
this.mover = mover;
this.sourceX = sx;
this.sourceY = sy;
invalid = map.blocked(this, x, y);
}
return !invalid;
}
/**
* Get the cost to move through a given location
*
* @param mover The entity that is being moved
* @param sx The x coordinate of the tile whose cost is being determined
* @param sy The y coordiante of the tile whose cost is being determined
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @return The cost of movement through the given tile
*/
public float getMovementCost(Mover mover, int sx, int sy, int tx, int ty) {
this.mover = mover;
this.sourceX = sx;
this.sourceY = sy;
return map.getCost(this, tx, ty);
}
/**
* Get the heuristic cost for the given location. This determines in which
* order the locations are processed.
*
* @param mover The entity that is being moved
* @param x The x coordinate of the tile whose cost is being determined
* @param y The y coordiante of the tile whose cost is being determined
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @return The heuristic cost assigned to the tile
*/
public float getHeuristicCost(Mover mover, int x, int y, int tx, int ty) {
return heuristic.getCost(map, mover, x, y, tx, ty);
}
/**
* A list that sorts any element provided into the list
*
* @author kevin
*/
private class PriorityList {
/** The list of elements */
private List list = new LinkedList();
/**
* Retrieve the first element from the list
*
* @return The first element from the list
*/
public Object first() {
return list.get(0);
}
/**
* Empty the list
*/
public void clear() {
list.clear();
}
/**
* Add an element to the list - causes sorting
*
* @param o The element to add
*/
public void add(Object o) {
// float the new entry
for (int i=0;i<list.size();i++) {
if (((Comparable) list.get(i)).compareTo(o) > 0) {
list.add(i, o);
break;
}
}
if (!list.contains(o)) {
list.add(o);
}
//Collections.sort(list);
}
/**
* Remove an element from the list
*
* @param o The element to remove
*/
public void remove(Object o) {
list.remove(o);
}
/**
* Get the number of elements in the list
*
* @return The number of element in the list
*/
public int size() {
return list.size();
}
/**
* Check if an element is in the list
*
* @param o The element to search for
* @return True if the element is in the list
*/
public boolean contains(Object o) {
return list.contains(o);
}
public String toString() {
String temp = "{";
for (int i=0;i<size();i++) {
temp += list.get(i).toString()+",";
}
temp += "}";
return temp;
}
}
/**
* A single node in the search graph
*/
private class Node implements Comparable {
/** The x coordinate of the node */
private int x;
/** The y coordinate of the node */
private int y;
/** The path cost for this node */
private float cost;
/** The parent of this node, how we reached it in the search */
private Node parent;
/** The heuristic cost of this node */
private float heuristic;
/** The search depth of this node */
private int depth;
/** In the open list */
private boolean open;
/** In the closed list */
private boolean closed;
/**
* Create a new node
*
* @param x The x coordinate of the node
* @param y The y coordinate of the node
*/
public Node(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Set the parent of this node
*
* @param parent The parent node which lead us to this node
* @return The depth we have no reached in searching
*/
public int setParent(Node parent) {
depth = parent.depth + 1;
this.parent = parent;
return depth;
}
/**
* @see Comparable#compareTo(Object)
*/
public int compareTo(Object other) {
Node o = (Node) other;
float f = heuristic + cost;
float of = o.heuristic + o.cost;
if (f < of) {
return -1;
} else if (f > of) {
return 1;
} else {
return 0;
}
}
/**
* Indicate whether the node is in the open list
*
* @param open True if the node is in the open list
*/
public void setOpen(boolean open) {
this.open = open;
}
/**
* Check if the node is in the open list
*
* @return True if the node is in the open list
*/
public boolean isOpen() {
return open;
}
/**
* Indicate whether the node is in the closed list
*
* @param closed True if the node is in the closed list
*/
public void setClosed(boolean closed) {
this.closed = closed;
}
/**
* Check if the node is in the closed list
*
* @return True if the node is in the closed list
*/
public boolean isClosed() {
return closed;
}
/**
* Reset the state of this node
*/
public void reset() {
closed = false;
open = false;
cost = 0;
depth = 0;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
return "[Node "+x+","+y+"]";
}
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getMover()
*/
public Mover getMover() {
return mover;
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getSearchDistance()
*/
public int getSearchDistance() {
return distance;
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getSourceX()
*/
public int getSourceX() {
return sourceX;
}
/**
* @see org.newdawn.slick.util.pathfinding.PathFindingContext#getSourceY()
*/
public int getSourceY() {
return sourceY;
}
}

View File

@@ -0,0 +1,17 @@
package org.newdawn.slick.util.pathfinding;
/**
* A tagging interface for an object representing the entity in the game that
* is going to moving along the path. This allows us to pass around entity/state
* information to determine whether a particular tile is blocked, or how much
* cost to apply on a particular tile.
*
* For instance, a Mover might represent a tank or plane on a game map. Passing round
* this entity allows us to determine whether rough ground on a map should effect
* the unit's cost for moving through the tile.
*
* @author Kevin Glass
*/
public interface Mover {
}

View File

@@ -0,0 +1,158 @@
package org.newdawn.slick.util.pathfinding;
import java.io.Serializable;
import java.util.ArrayList;
/**
* A path determined by some path finding algorithm. A series of steps from
* the starting location to the target location. This includes a step for the
* initial location.
*
* @author Kevin Glass
*/
public class Path implements Serializable {
/** The serial identifier for this class */
private static final long serialVersionUID = 1L;
/** The list of steps building up this path */
private ArrayList steps = new ArrayList();
/**
* Create an empty path
*/
public Path() {
}
/**
* Get the length of the path, i.e. the number of steps
*
* @return The number of steps in this path
*/
public int getLength() {
return steps.size();
}
/**
* Get the step at a given index in the path
*
* @param index The index of the step to retrieve. Note this should
* be >= 0 and < getLength();
* @return The step information, the position on the map.
*/
public Step getStep(int index) {
return (Step) steps.get(index);
}
/**
* Get the x coordinate for the step at the given index
*
* @param index The index of the step whose x coordinate should be retrieved
* @return The x coordinate at the step
*/
public int getX(int index) {
return getStep(index).x;
}
/**
* Get the y coordinate for the step at the given index
*
* @param index The index of the step whose y coordinate should be retrieved
* @return The y coordinate at the step
*/
public int getY(int index) {
return getStep(index).y;
}
/**
* Append a step to the path.
*
* @param x The x coordinate of the new step
* @param y The y coordinate of the new step
*/
public void appendStep(int x, int y) {
steps.add(new Step(x,y));
}
/**
* Prepend a step to the path.
*
* @param x The x coordinate of the new step
* @param y The y coordinate of the new step
*/
public void prependStep(int x, int y) {
steps.add(0, new Step(x, y));
}
/**
* Check if this path contains the given step
*
* @param x The x coordinate of the step to check for
* @param y The y coordinate of the step to check for
* @return True if the path contains the given step
*/
public boolean contains(int x, int y) {
return steps.contains(new Step(x,y));
}
/**
* A single step within the path
*
* @author Kevin Glass
*/
public class Step implements Serializable {
/** The x coordinate at the given step */
private int x;
/** The y coordinate at the given step */
private int y;
/**
* Create a new step
*
* @param x The x coordinate of the new step
* @param y The y coordinate of the new step
*/
public Step(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Get the x coordinate of the new step
*
* @return The x coodindate of the new step
*/
public int getX() {
return x;
}
/**
* Get the y coordinate of the new step
*
* @return The y coodindate of the new step
*/
public int getY() {
return y;
}
/**
* @see Object#hashCode()
*/
public int hashCode() {
return x*y;
}
/**
* @see Object#equals(Object)
*/
public boolean equals(Object other) {
if (other instanceof Step) {
Step o = (Step) other;
return (o.x == x) && (o.y == y);
}
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
package org.newdawn.slick.util.pathfinding;
/**
* A description of an implementation that can find a path from one
* location on a tile map to another based on information provided
* by that tile map.
*
* @see TileBasedMap
* @author Kevin Glass
*/
public interface PathFinder {
/**
* Find a path from the starting location provided (sx,sy) to the target
* location (tx,ty) avoiding blockages and attempting to honour costs
* provided by the tile map.
*
* @param mover The entity that will be moving along the path. This provides
* a place to pass context information about the game entity doing the moving, e.g.
* can it fly? can it swim etc.
*
* @param sx The x coordinate of the start location
* @param sy The y coordinate of the start location
* @param tx The x coordinate of the target location
* @param ty Teh y coordinate of the target location
* @return The path found from start to end, or null if no path can be found.
*/
public Path findPath(Mover mover, int sx, int sy, int tx, int ty);
}

View File

@@ -0,0 +1,36 @@
package org.newdawn.slick.util.pathfinding;
/**
* The context describing the current path finding state
*
* @author kevin
*/
public interface PathFindingContext {
/**
* Get the object being moved along the path if any
*
* @return The object being moved along the path
*/
public Mover getMover();
/**
* Get the x coordinate of the source location
*
* @return The x coordinate of the source location
*/
public int getSourceX();
/**
* Get the y coordinate of the source location
*
* @return The y coordinate of the source location
*/
public int getSourceY();
/**
* Get the distance that has been searched to reach this point
*
* @return The distance that has been search to reach this point
*/
public int getSearchDistance();
}

View File

@@ -0,0 +1,58 @@
package org.newdawn.slick.util.pathfinding;
/**
* The description for the data we're pathfinding over. This provides the contract
* between the data being searched (i.e. the in game map) and the path finding
* generic tools
*
* @author Kevin Glass
*/
public interface TileBasedMap {
/**
* Get the width of the tile map. The slightly odd name is used
* to distiguish this method from commonly used names in game maps.
*
* @return The number of tiles across the map
*/
public int getWidthInTiles();
/**
* Get the height of the tile map. The slightly odd name is used
* to distiguish this method from commonly used names in game maps.
*
* @return The number of tiles down the map
*/
public int getHeightInTiles();
/**
* Notification that the path finder visited a given tile. This is
* used for debugging new heuristics.
*
* @param x The x coordinate of the tile that was visited
* @param y The y coordinate of the tile that was visited
*/
public void pathFinderVisited(int x, int y);
/**
* Check if the given location is blocked, i.e. blocks movement of
* the supplied mover.
*
* @param context The context describing the pathfinding at the time of this request
* @param tx The x coordinate of the tile we're moving to
* @param ty The y coordinate of the tile we're moving to
* @return True if the location is blocked
*/
public boolean blocked(PathFindingContext context, int tx, int ty);
/**
* Get the cost of moving through the given tile. This can be used to
* make certain areas more desirable. A simple and valid implementation
* of this method would be to return 1 in all cases.
*
* @param context The context describing the pathfinding at the time of this request
* @param tx The x coordinate of the tile we're moving to
* @param ty The y coordinate of the tile we're moving to
* @return The relative cost of moving across the given tile
*/
public float getCost(PathFindingContext context, int tx, int ty);
}

View File

@@ -0,0 +1,26 @@
package org.newdawn.slick.util.pathfinding.heuristics;
import org.newdawn.slick.util.pathfinding.AStarHeuristic;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* A heuristic that uses the tile that is closest to the target
* as the next best tile.
*
* @author Kevin Glass
*/
public class ClosestHeuristic implements AStarHeuristic {
/**
* @see AStarHeuristic#getCost(TileBasedMap, Mover, int, int, int, int)
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx, int ty) {
float dx = tx - x;
float dy = ty - y;
float result = (float) (Math.sqrt((dx*dx)+(dy*dy)));
return result;
}
}

View File

@@ -0,0 +1,26 @@
package org.newdawn.slick.util.pathfinding.heuristics;
import org.newdawn.slick.util.pathfinding.AStarHeuristic;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* A heuristic that uses the tile that is closest to the target
* as the next best tile. In this case the sqrt is removed
* and the distance squared is used instead
*
* @author Kevin Glass
*/
public class ClosestSquaredHeuristic implements AStarHeuristic {
/**
* @see AStarHeuristic#getCost(TileBasedMap, Mover, int, int, int, int)
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx, int ty) {
float dx = tx - x;
float dy = ty - y;
return ((dx*dx)+(dy*dy));
}
}

View File

@@ -0,0 +1,34 @@
package org.newdawn.slick.util.pathfinding.heuristics;
import org.newdawn.slick.util.pathfinding.AStarHeuristic;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* A heuristic that drives the search based on the Manhattan distance
* between the current location and the target
*
* @author Kevin Glass
*/
public class ManhattanHeuristic implements AStarHeuristic {
/** The minimum movement cost from any one square to the next */
private int minimumCost;
/**
* Create a new heuristic
*
* @param minimumCost The minimum movement cost from any one square to the next
*/
public ManhattanHeuristic(int minimumCost) {
this.minimumCost = minimumCost;
}
/**
* @see AStarHeuristic#getCost(TileBasedMap, Mover, int, int, int, int)
*/
public float getCost(TileBasedMap map, Mover mover, int x, int y, int tx,
int ty) {
return minimumCost * (Math.abs(x-tx) + Math.abs(y-ty));
}
}

View File

@@ -0,0 +1,69 @@
package org.newdawn.slick.util.pathfinding.navmesh;
/**
* A link between this space and another
*
* @author kevin
*/
public class Link {
/** The x coordinate of the joining point */
private float px;
/** The y coordinate of the joining point */
private float py;
/** The target space we'd be linking to */
private Space target;
/**
* Create a new link
*
* @param px The x coordinate of the linking point
* @param py The y coordinate of the linking point
* @param target The target space we're linking to
*/
public Link(float px, float py, Space target) {
this.px = px;
this.py = py;
this.target = target;
}
/**
* Get the distance squared from this link to the given position
*
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @return The distance squared from this link to the target
*/
public float distance2(float tx, float ty) {
float dx = tx - px;
float dy = ty - py;
return ((dx*dx) + (dy*dy));
}
/**
* Get the x coordinate of the link
*
* @return The x coordinate of the link
*/
public float getX() {
return px;
}
/**
* Get the y coordinate of the link
*
* @return The y coordinate of the link
*/
public float getY() {
return py;
}
/**
* Get the space this object links to
*
* @return The space this object links to
*/
public Space getTarget() {
return target;
}
}

View File

@@ -0,0 +1,176 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
/**
* A nav-mesh is a set of shapes that describe the navigation of a map. These
* shapes are linked together allow path finding but without the high
* resolution that tile maps require. This leads to fast path finding and
* potentially much more accurate map definition.
*
* @author kevin
*
*/
public class NavMesh {
/** The list of spaces that build up this navigation mesh */
private ArrayList spaces = new ArrayList();
/**
* Create a new empty mesh
*/
public NavMesh() {
}
/**
* Create a new mesh with a set of spaces
*
* @param spaces The spaces included in the mesh
*/
public NavMesh(ArrayList spaces) {
this.spaces.addAll(spaces);
}
/**
* Get the number of spaces that are in the mesh
*
* @return The spaces in the mesh
*/
public int getSpaceCount() {
return spaces.size();
}
/**
* Get the space at a given index
*
* @param index The index of the space to retrieve
* @return The space at the given index
*/
public Space getSpace(int index) {
return (Space) spaces.get(index);
}
/**
* Add a single space to the mesh
*
* @param space The space to be added
*/
public void addSpace(Space space) {
spaces.add(space);
}
/**
* Find the space at a given location
*
* @param x The x coordinate at which to find the space
* @param y The y coordinate at which to find the space
* @return The space at the given location
*/
public Space findSpace(float x, float y) {
for (int i=0;i<spaces.size();i++) {
Space space = getSpace(i);
if (space.contains(x,y)) {
return space;
}
}
return null;
}
/**
* Find a path from the source to the target coordinates
*
* @param sx The x coordinate of the source location
* @param sy The y coordinate of the source location
* @param tx The x coordinate of the target location
* @param ty The y coordinate of the target location
* @param optimize True if paths should be optimized
* @return The path between the two spaces
*/
public NavPath findPath(float sx, float sy, float tx, float ty, boolean optimize) {
Space source = findSpace(sx,sy);
Space target = findSpace(tx,ty);
if ((source == null) || (target == null)) {
return null;
}
for (int i=0;i<spaces.size();i++) {
((Space) spaces.get(i)).clearCost();
}
target.fill(source,tx, ty, 0);
if (target.getCost() == Float.MAX_VALUE) {
return null;
}
if (source.getCost() == Float.MAX_VALUE) {
return null;
}
NavPath path = new NavPath();
path.push(new Link(sx, sy, null));
if (source.pickLowestCost(target, path)) {
path.push(new Link(tx, ty, null));
if (optimize) {
optimize(path);
}
return path;
}
return null;
}
/**
* Check if a particular path is clear
*
* @param x1 The x coordinate of the starting point
* @param y1 The y coordinate of the starting point
* @param x2 The x coordinate of the ending point
* @param y2 The y coordinate of the ending point
* @param step The size of the step between points
* @return True if there are no blockages along the path
*/
private boolean isClear(float x1, float y1, float x2, float y2, float step) {
float dx = (x2 - x1);
float dy = (y2 - y1);
float len = (float) Math.sqrt((dx*dx)+(dy*dy));
dx *= step;
dx /= len;
dy *= step;
dy /= len;
int steps = (int) (len / step);
for (int i=0;i<steps;i++) {
float x = x1 + (dx*i);
float y = y1 + (dy*i);
if (findSpace(x,y) == null) {
return false;
}
}
return true;
}
/**
* Optimize a path by removing segments that arn't required
* to reach the end point
*
* @param path The path to optimize. Redundant segments will be removed
*/
private void optimize(NavPath path) {
int pt = 0;
while (pt < path.length()-2) {
float sx = path.getX(pt);
float sy = path.getY(pt);
float nx = path.getX(pt+2);
float ny = path.getY(pt+2);
if (isClear(sx,sy,nx,ny,0.1f)) {
path.remove(pt+1);
} else {
pt++;
}
}
}
}

View File

@@ -0,0 +1,222 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
import org.newdawn.slick.util.pathfinding.Mover;
import org.newdawn.slick.util.pathfinding.PathFindingContext;
import org.newdawn.slick.util.pathfinding.TileBasedMap;
/**
* The builder responsible for converting a tile based map into
* a navigation mesh
*
* @author kevin
*/
public class NavMeshBuilder implements PathFindingContext {
/** The current x position we're searching */
private int sx;
/** The current y position we've searching */
private int sy;
/** The smallest space allowed */
private float smallestSpace = 0.2f;
/** True if we're working tile based */
private boolean tileBased;
/**
* Build a navigation mesh based on a tile map
*
* @param map The map to build the navigation mesh from
*
* @return The newly created navigation mesh
*/
public NavMesh build(TileBasedMap map) {
return build(map, true);
}
/**
* Build a navigation mesh based on a tile map
*
* @param map The map to build the navigation mesh from
* @param tileBased True if we'll use the tiles for the mesh initially
* rather than quad spacing
* @return The newly created navigation mesh
*/
public NavMesh build(TileBasedMap map, boolean tileBased) {
this.tileBased = tileBased;
ArrayList spaces = new ArrayList();
if (tileBased) {
for (int x=0;x<map.getWidthInTiles();x++) {
for (int y=0;y<map.getHeightInTiles();y++) {
if (!map.blocked(this, x, y)) {
spaces.add(new Space(x,y,1,1));
}
}
}
} else {
Space space = new Space(0,0,map.getWidthInTiles(),map.getHeightInTiles());
subsection(map, space, spaces);
}
while (mergeSpaces(spaces)) {}
linkSpaces(spaces);
return new NavMesh(spaces);
}
/**
* Merge the spaces that have been created to optimize out anywhere
* we can.
*
* @param spaces The list of spaces to be merged
* @return True if a merge occured and we'll have to start the merge
* process again
*/
private boolean mergeSpaces(ArrayList spaces) {
for (int source=0;source<spaces.size();source++) {
Space a = (Space) spaces.get(source);
for (int target=source+1;target<spaces.size();target++) {
Space b = (Space) spaces.get(target);
if (a.canMerge(b)) {
spaces.remove(a);
spaces.remove(b);
spaces.add(a.merge(b));
return true;
}
}
}
return false;
}
/**
* Determine the links between spaces
*
* @param spaces The spaces to link up
*/
private void linkSpaces(ArrayList spaces) {
for (int source=0;source<spaces.size();source++) {
Space a = (Space) spaces.get(source);
for (int target=source+1;target<spaces.size();target++) {
Space b = (Space) spaces.get(target);
if (a.hasJoinedEdge(b)) {
a.link(b);
b.link(a);
}
}
}
}
/**
* Check if a particular space is clear of blockages
*
* @param map The map the spaces are being built from
* @param space The space to check
* @return True if there are no blockages in the space
*/
public boolean clear(TileBasedMap map, Space space) {
if (tileBased) {
return true;
}
float x = 0;
boolean donex = false;
while (x < space.getWidth()) {
float y = 0;
boolean doney = false;
while (y < space.getHeight()) {
sx = (int) (space.getX()+x);
sy = (int) (space.getY()+y);
if (map.blocked(this, sx, sy)) {
return false;
}
y += 0.1f;
if ((y > space.getHeight()) && (!doney)) {
y = space.getHeight();
doney = true;
}
}
x += 0.1f;
if ((x > space.getWidth()) && (!donex)) {
x = space.getWidth();
donex = true;
}
}
return true;
}
/**
* Subsection a space into smaller spaces if required to find a non-blocked
* area.
*
* @param map The map being processed
* @param space The space being sections
* @param spaces The list of spaces that have been created
*/
private void subsection(TileBasedMap map, Space space, ArrayList spaces) {
if (!clear(map, space)) {
float width2 = space.getWidth()/2;
float height2 = space.getHeight()/2;
if ((width2 < smallestSpace) && (height2 < smallestSpace)) {
return;
}
subsection(map, new Space(space.getX(), space.getY(), width2, height2), spaces);
subsection(map, new Space(space.getX(), space.getY()+height2, width2, height2), spaces);
subsection(map, new Space(space.getX()+width2, space.getY(), width2, height2), spaces);
subsection(map, new Space(space.getX()+width2, space.getY()+height2, width2, height2), spaces);
} else {
spaces.add(space);
}
}
/**
* Path finding context implementation
*
* @return The current mover
*/
public Mover getMover() {
return null;
}
/**
* Path finding context implementation
*
* @return The current search distance
*/
public int getSearchDistance() {
return 0;
}
/**
* Path finding context implementation
*
* @return The current x location
*/
public int getSourceX() {
return sx;
}
/**
* Path finding context implementation
*
* @return The current y location
*/
public int getSourceY() {
return sy;
}
}

View File

@@ -0,0 +1,75 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
/**
* A path across a navigation mesh
*
* @author kevin
*/
public class NavPath {
/** The list of links that form this path */
private ArrayList links = new ArrayList();
/**
* Create a new path
*/
public NavPath() {
}
/**
* Push a link to the end of the path
*
* @param link The link to the end of the path
*/
public void push(Link link) {
links.add(link);
}
/**
* Get the length of the path
*
* @return The number of steps in the path
*/
public int length() {
return links.size();
}
/**
* Get the x coordinate of the given step
*
* @param step The index of the step to retrieve
* @return The x coordinate at the given step index
*/
public float getX(int step) {
return ((Link) links.get(step)).getX();
}
/**
* Get the y coordinate of the given step
*
* @param step The index of the step to retrieve
* @return The y coordinate at the given step index
*/
public float getY(int step) {
return ((Link) links.get(step)).getY();
}
/**
* Get a string representation of this instance
*
* @return The string representation of this instance
*/
public String toString() {
return "[Path length="+length()+"]";
}
/**
* Remove a step in the path
*
* @param i
*/
public void remove(int i) {
links.remove(i);
}
}

View File

@@ -0,0 +1,319 @@
package org.newdawn.slick.util.pathfinding.navmesh;
import java.util.ArrayList;
import java.util.HashMap;
/**
* A quad space within a navigation mesh
*
* @author kevin
*/
public class Space {
/** The x coordinate of the top corner of the space */
private float x;
/** The y coordinate of the top corner of the space */
private float y;
/** The width of the space */
private float width;
/** The height of the space */
private float height;
/** A map from spaces to the links that connect them to this space */
private HashMap links = new HashMap();
/** A list of the links from this space to others */
private ArrayList linksList = new ArrayList();
/** The cost to get to this node */
private float cost;
/**
* Create a new space
*
* @param x The x coordinate of the top corner of the space
* @param y The y coordinate of the top corner of the space
* @param width The width of the space
* @param height The height of the space
*/
public Space(float x, float y, float width, float height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
/**
* Get the width of the space
*
* @return The width of the space
*/
public float getWidth() {
return width;
}
/**
* Get the height of the space
*
* @return The height of the space
*/
public float getHeight() {
return height;
}
/**
* Get the x coordinate of the top corner of the space
*
* @return The x coordinate of the top corner of the space
*/
public float getX() {
return x;
}
/**
* Get the y coordinate of the top corner of the space
*
* @return The y coordinate of the top corner of the space
*/
public float getY() {
return y;
}
/**
* Link this space to another by creating a link and finding the point
* at which the spaces link up
*
* @param other The other space to link to
*/
public void link(Space other) {
// aligned vertical edges
if (inTolerance(x,other.x+other.width) || inTolerance(x+width, other.x)) {
float linkx = x;
if (x+width == other.x) {
linkx = x+width;
}
float top = Math.max(y, other.y);
float bottom = Math.min(y+height, other.y+other.height);
float linky = top + ((bottom-top)/2);
Link link = new Link(linkx, linky, other);
links.put(other,link);
linksList.add(link);
}
// aligned horizontal edges
if (inTolerance(y, other.y+other.height) || inTolerance(y+height, other.y)) {
float linky = y;
if (y+height == other.y) {
linky = y+height;
}
float left = Math.max(x, other.x);
float right = Math.min(x+width, other.x+other.width);
float linkx = left + ((right-left)/2);
Link link = new Link(linkx, linky, other);
links.put(other, link);
linksList.add(link);
}
}
/**
* Check whether two locations are within tolerance distance. This is
* used when finding aligned edges to remove float rounding errors
*
* @param a The first value
* @param b The second value
* @return True if the edges are close enough (tm)
*/
private boolean inTolerance(float a, float b) {
return a == b;
}
/**
* Check if this space has an edge that is joined with another
*
* @param other The other space to check against
* @return True if the spaces have a shared edge
*/
public boolean hasJoinedEdge(Space other) {
// aligned vertical edges
if (inTolerance(x,other.x+other.width) || inTolerance(x+width,other.x)) {
if ((y >= other.y) && (y <= other.y + other.height)) {
return true;
}
if ((y+height >= other.y) && (y+height <= other.y + other.height)) {
return true;
}
if ((other.y >= y) && (other.y <= y + height)) {
return true;
}
if ((other.y+other.height >= y) && (other.y+other.height <= y + height)) {
return true;
}
}
// aligned horizontal edges
if (inTolerance(y, other.y+other.height) || inTolerance(y+height, other.y)) {
if ((x >= other.x) && (x <= other.x + other.width)) {
return true;
}
if ((x+width >= other.x) && (x+width <= other.x + other.width)) {
return true;
}
if ((other.x >= x) && (other.x <= x + width)) {
return true;
}
if ((other.x+other.width >= x) && (other.x+other.width <= x + width)) {
return true;
}
}
return false;
}
/**
* Merge this space with another
*
* @param other The other space to merge with
* @return The result space created by joining the two
*/
public Space merge(Space other) {
float minx = Math.min(x, other.x);
float miny = Math.min(y, other.y);
float newwidth = width+other.width;
float newheight = height+other.height;
if (x == other.x) {
newwidth = width;
} else {
newheight = height;
}
return new Space(minx, miny, newwidth, newheight);
}
/**
* Check if the given space can be merged with this one. It must have
* an adjacent edge and have the same height or width as this space.
*
* @param other The other space to be considered
* @return True if the spaces can be joined together
*/
public boolean canMerge(Space other) {
if (!hasJoinedEdge(other)) {
return false;
}
if ((x == other.x) && (width == other.width)) {
return true;
}
if ((y == other.y) && (height == other.height)) {
return true;
}
return false;
}
/**
* Get the number of links
*
* @return The number of links from the space to others
*/
public int getLinkCount() {
return linksList.size();
}
/**
* Get the link from this space to another at a particular index
*
* @param index The index of the link to retrieve
* @return The link from this space to another
*/
public Link getLink(int index) {
return (Link) linksList.get(index);
}
/**
* Check if this space contains a given point
*
* @param xp The x coordinate to check
* @param yp The y coordinate to check
* @return True if this space container the coordinate given
*/
public boolean contains(float xp, float yp) {
return (xp >= x) && (xp < x+width) && (yp >= y) && (yp < y+height);
}
/**
* Fill the spaces based on the cost from a given starting point
*
* @param target The target space we're heading for
* @param sx The x coordinate of the starting point
* @param sy The y coordinate of the starting point
* @param cost The cost up to this point
*/
public void fill(Space target, float sx, float sy, float cost) {
if (cost >= this.cost) {
return;
}
this.cost = cost;
if (target == this) {
return;
}
for (int i=0;i<getLinkCount();i++) {
Link link = getLink(i);
float extraCost = link.distance2(sx,sy);
float nextCost = cost + extraCost;
link.getTarget().fill(target, link.getX(), link.getY(), nextCost);
}
}
/**
* Clear the costing values across the whole map
*/
public void clearCost() {
cost = Float.MAX_VALUE;
}
/**
* Get the cost to get to this node at the moment
*
* @return The cost to get to this node
*/
public float getCost() {
return cost;
}
/**
* Pick the lowest cost route from this space to another on the path
*
* @param target The target space we're looking for
* @param path The path to add the steps to
* @return True if the path was found
*/
public boolean pickLowestCost(Space target, NavPath path) {
if (target == this) {
return true;
}
if (links.size() == 0) {
return false;
}
Link bestLink = null;
for (int i=0;i<getLinkCount();i++) {
Link link = getLink(i);
if ((bestLink == null) || (link.getTarget().getCost() < bestLink.getTarget().getCost())) {
bestLink = link;
}
}
path.push(bestLink);
return bestLink.getTarget().pickLowestCost(target, path);
}
/**
* Get the string representation of this instance
*
* @return The string representation of this instance
*/
public String toString() {
return "[Space "+x+","+y+" "+width+","+height+"]";
}
}

View File

@@ -0,0 +1,3 @@
<BODY>
A set of classes to provide configurable A* path finding on tilebased maps
</BODY>

View File

@@ -0,0 +1,483 @@
package org.newdawn.slick.util.xml;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* Provides a method of parsing XML into an existing data model. This does not
* provide the same functionality as JAXB or the variety of XML bindings out there. This
* is a utility to map XML onto an existing data model. The idea being that the design level
* model should not be driven by the XML schema thats defined. The two arn't always equal
* and often you end up with a set of class that represent your XML that you then have
* to traverse to extract into your normal data model.
*
* This utility hopes to take a piece of XML and map it onto a previously designed data
* model. At the moment it's way to tied to the structure of the XML but this will
* hopefully change with time.
*
* XML element names must be mapped to class names. This can be done in two ways either:
*
* - Specify an explict mapping with addElementMapping()
* - Specify the default package name and use the element name as the class name
*
* Each attribute in an element is mapped into a property of the element class, preferably
* through a set<AttrName> bean method, but alternatively by direct injection into private
* fields.
*
* Each child element is added to the target class by call the method add() on it with a single
* parameter of the type generated for the child element.
*
* Classes can optionally implement setXMLElementName(String) and setXMLElementContent(String) to
* recieve the name and content respectively of the XMLElement they were parsed from. This can
* help when mapping two elements to a single class.
*
* To reiterate, I'm not sure this is a good idea yet. It helps me as a utility since I've done
* this several times in the past but in the general case it may not be perfect. Consider a custom
* parser using XMLParser or JAXB (et al) seriously instead.
*
* @author kevin
*
*/
public class ObjectTreeParser {
/** The mapping of XML element names to class names */
private HashMap nameToClass = new HashMap();
/** The default package where classes will be searched for */
private String defaultPackage;
/** The list of elements to ignore */
private ArrayList ignored = new ArrayList();
/** The name of the method to add an child object to it's parent */
private String addMethod = "add";
/**
* Create an object tree parser with no default package
*/
public ObjectTreeParser() {
}
/**
* Create an object tree parser specifing the default package
* where classes will be search for using the XML element name
*
* @param defaultPackage The default package to be searched
*/
public ObjectTreeParser(String defaultPackage) {
this.defaultPackage = defaultPackage;
}
/**
* Add a mapping between XML element name and class name
*
* @param elementName The name of the XML element
* @param elementClass The class to be created for the given element
*/
public void addElementMapping(String elementName, Class elementClass) {
nameToClass.put(elementName, elementClass);
}
/**
* Add a name to the list of elements ignored
*
* @param elementName The name to ignore
*/
public void addIgnoredElement(String elementName) {
ignored.add(elementName);
}
/**
* Set the name of the method to use to add child objects to their
* parents. This is sometimes useful to not clash with the existing
* data model methods.
*
* @param methodName The name of the method to call
*/
public void setAddMethodName(String methodName) {
addMethod = methodName;
}
/**
* Set the default package which will be search for classes by their XML
* element name.
*
* @param defaultPackage The default package to be searched
*/
public void setDefaultPackage(String defaultPackage) {
this.defaultPackage = defaultPackage;
}
/**
* Parse the XML document located by the slick resource loader using the
* reference given.
*
* @param ref The reference to the XML document
* @return The root element of the newly parse document
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parse(String ref) throws SlickXMLException {
return parse(ref, ResourceLoader.getResourceAsStream(ref));
}
/**
* Parse the XML document that can be read from the given input stream
*
* @param name The name of the document
* @param in The input stream from which the document can be read
* @return The root element of the newly parse document
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parse(String name, InputStream in) throws SlickXMLException {
XMLParser parser = new XMLParser();
XMLElement root = parser.parse(name, in);
return traverse(root);
}
/**
* Parse the XML document located by the slick resource loader using the
* reference given.
*
* @param ref The reference to the XML document
* @param target The top level object that represents the root node
* @return The root element of the newly parse document
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parseOnto(String ref, Object target) throws SlickXMLException {
return parseOnto(ref, ResourceLoader.getResourceAsStream(ref), target);
}
/**
* Parse the XML document that can be read from the given input stream
*
* @param name The name of the document
* @param in The input stream from which the document can be read
* @param target The top level object that represents the root node
* @return The root element of the newly parse document
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public Object parseOnto(String name, InputStream in, Object target) throws SlickXMLException {
XMLParser parser = new XMLParser();
XMLElement root = parser.parse(name, in);
return traverse(root, target);
}
/**
* Deterine the name of the class that should be used for a given
* XML element name.
*
* @param name The name of the XML element
* @return The class to be used or null if none can be found
*/
private Class getClassForElementName(String name) {
Class clazz = (Class) nameToClass.get(name);
if (clazz != null) {
return clazz;
}
if (defaultPackage != null) {
try {
return Class.forName(defaultPackage+"."+name);
} catch (ClassNotFoundException e) {
// ignore, it's just not there
}
}
return null;
}
/**
* Traverse the XML element specified generating the appropriate object structure
* for it and it's children
*
* @param current The XML element to process
* @return The object created for the given element
* @throws SlickXMLException
*/
private Object traverse(XMLElement current) throws SlickXMLException {
return traverse(current, null);
}
/**
* Traverse the XML element specified generating the appropriate object structure
* for it and it's children
*
* @param current The XML element to process
* @param instance The instance to parse onto, normally null
* @return The object created for the given element
* @throws SlickXMLException
*/
private Object traverse(XMLElement current, Object instance) throws SlickXMLException {
String name = current.getName();
if (ignored.contains(name)) {
return null;
}
Class clazz;
if (instance == null) {
clazz = getClassForElementName(name);
} else {
clazz = instance.getClass();
}
if (clazz == null) {
throw new SlickXMLException("Unable to map element "+name+" to a class, define the mapping");
}
try {
if (instance == null) {
instance = clazz.newInstance();
Method elementNameMethod = getMethod(clazz, "setXMLElementName", new Class[] {String.class});
if (elementNameMethod != null) {
invoke(elementNameMethod, instance, new Object[] {name});
}
Method contentMethod = getMethod(clazz, "setXMLElementContent", new Class[] {String.class});
if (contentMethod != null) {
invoke(contentMethod, instance, new Object[] {current.getContent()});
}
}
String[] attrs = current.getAttributeNames();
for (int i=0;i<attrs.length;i++) {
String methodName = "set"+attrs[i];
Method method = findMethod(clazz, methodName);
if (method == null) {
Field field = findField(clazz, attrs[i]);
if (field != null) {
String value = current.getAttribute(attrs[i]);
Object typedValue = typeValue(value, field.getType());
setField(field, instance, typedValue);
} else {
Log.info("Unable to find property on: "+clazz+" for attribute: "+attrs[i]);
}
} else {
String value = current.getAttribute(attrs[i]);
Object typedValue = typeValue(value, method.getParameterTypes()[0]);
invoke(method, instance, new Object[] {typedValue});
}
}
XMLElementList children = current.getChildren();
for (int i=0;i<children.size();i++) {
XMLElement element = children.get(i);
Object child = traverse(element);
if (child != null) {
String methodName = addMethod;
Method method = findMethod(clazz, methodName, child.getClass());
if (method == null) {
Log.info("Unable to find method to add: "+child+" to "+clazz);
} else {
invoke(method, instance, new Object[] {child});
}
}
}
return instance;
} catch (InstantiationException e) {
throw new SlickXMLException("Unable to instance "+clazz+" for element "+name+", no zero parameter constructor?", e);
} catch (IllegalAccessException e) {
throw new SlickXMLException("Unable to instance "+clazz+" for element "+name+", no zero parameter constructor?", e);
}
}
/**
* Convert a given value to a given type
*
* @param value The value to convert
* @param clazz The class that the returned object must be
* @return The value as the given type
* @throws SlickXMLException Indicates there is no automatic way of converting the value to the type
*/
private Object typeValue(String value, Class clazz) throws SlickXMLException {
if (clazz == String.class) {
return value;
}
try {
clazz = mapPrimitive(clazz);
return clazz.getConstructor(new Class[] {String.class}).newInstance(new Object[] {value});
} catch (Exception e) {
throw new SlickXMLException("Failed to convert: "+value+" to the expected primitive type: "+clazz, e);
}
}
/**
* Map a primitive class type to it's real object wrapper
*
* @param clazz The primitive type class
* @return The object wrapper class
*/
private Class mapPrimitive(Class clazz) {
if (clazz == Integer.TYPE) {
return Integer.class;
}
if (clazz == Double.TYPE) {
return Double.class;
}
if (clazz == Float.TYPE) {
return Float.class;
}
if (clazz == Boolean.TYPE) {
return Boolean.class;
}
if (clazz == Long.TYPE) {
return Long.class;
}
throw new RuntimeException("Unsupported primitive: "+clazz);
}
/**
* Find a field in a class by it's name. Note that this method is
* only needed because the general reflection method is case
* sensitive
*
* @param clazz The clazz to search
* @param name The name of the field to search for
* @return The field or null if none could be located
*/
private Field findField(Class clazz, String name) {
Field[] fields = clazz.getDeclaredFields();
for (int i=0;i<fields.length;i++) {
if (fields[i].getName().equalsIgnoreCase(name)) {
if (fields[i].getType().isPrimitive()) {
return fields[i];
}
if (fields[i].getType() == String.class) {
return fields[i];
}
}
}
return null;
}
/**
* Find a method in a class by it's name. Note that this method is
* only needed because the general reflection method is case
* sensitive
*
* @param clazz The clazz to search
* @param name The name of the method to search for
* @return The method or null if none could be located
*/
private Method findMethod(Class clazz, String name) {
Method[] methods = clazz.getDeclaredMethods();
for (int i=0;i<methods.length;i++) {
if (methods[i].getName().equalsIgnoreCase(name)) {
Method method = methods[i];
Class[] params = method.getParameterTypes();
if (params.length == 1) {
return method;
}
}
}
return null;
}
/**
* Find a method on a class with a single given parameter.
*
* @param clazz The clazz to search through
* @param name The name of the method to locate
* @param parameter The type the single parameter must have
* @return The method or null if none could be located
*/
private Method findMethod(Class clazz, String name, Class parameter) {
Method[] methods = clazz.getDeclaredMethods();
for (int i=0;i<methods.length;i++) {
if (methods[i].getName().equalsIgnoreCase(name)) {
Method method = methods[i];
Class[] params = method.getParameterTypes();
if (params.length == 1) {
if (method.getParameterTypes()[0].isAssignableFrom(parameter)) {
return method;
}
}
}
}
return null;
}
/**
* Set a field value on a object instance
*
* @param field The field to be set
* @param instance The instance of the object to set it on
* @param value The value to set
* @throws SlickXMLException Indicates a failure to set or access the field
*/
private void setField(Field field, Object instance, Object value) throws SlickXMLException {
try {
field.setAccessible(true);
field.set(instance, value);
} catch (IllegalArgumentException e) {
throw new SlickXMLException("Failed to set: "+field+" for an XML attribute, is it valid?", e);
} catch (IllegalAccessException e) {
throw new SlickXMLException("Failed to set: "+field+" for an XML attribute, is it valid?", e);
} finally {
field.setAccessible(false);
}
}
/**
* Call a method on a object
*
* @param method The method to call
* @param instance The objet to call the method on
* @param params The parameters to pass
* @throws SlickXMLException Indicates a failure to call or access the method
*/
private void invoke(Method method, Object instance, Object[] params) throws SlickXMLException {
try {
method.setAccessible(true);
method.invoke(instance, params);
} catch (IllegalArgumentException e) {
throw new SlickXMLException("Failed to invoke: "+method+" for an XML attribute, is it valid?", e);
} catch (IllegalAccessException e) {
throw new SlickXMLException("Failed to invoke: "+method+" for an XML attribute, is it valid?", e);
} catch (InvocationTargetException e) {
throw new SlickXMLException("Failed to invoke: "+method+" for an XML attribute, is it valid?", e);
} finally {
method.setAccessible(false);
}
}
/**
* Get a method on a given class. Only here for tidy purposes,
* hides the the big exceptions.
*
* @param clazz The class to search
* @param name The name of the method
* @param params The parameters for the method
* @return The method or null if none can be found
*/
private Method getMethod(Class clazz, String name, Class[] params) {
try {
return clazz.getMethod(name, params);
} catch (SecurityException e) {
return null;
} catch (NoSuchMethodException e) {
return null;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.newdawn.slick.util.xml;
import org.newdawn.slick.SlickException;
/**
* An exception to describe failures in XML. Made a special case because with XML
* to object parsing you might want to handle it differently
*
* @author kevin
*/
public class SlickXMLException extends SlickException {
/**
* Create a new exception
*
* @param message The message describing the failure
*/
public SlickXMLException(String message) {
super(message);
}
/**
* Create a new exception
*
* @param message The message describing the failure
* @param e The exception causing this failure
*/
public SlickXMLException(String message, Throwable e) {
super(message, e);
}
}

View File

@@ -0,0 +1,260 @@
package org.newdawn.slick.util.xml;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
/**
* A utility wrapper round the standard DOM XML element. This provides a more simple API
* for accessing attributes, children and providing defaults when schemas arn't used - which
* is generally a little simpler for most of us.
*
* @author kevin
*/
public class XMLElement {
/** The Java DOM implementation XML element */
private Element dom;
/** The list of children initialised on first access */
private XMLElementList children;
/** The name of the element */
private String name;
/**
* Create a new element wrapped round a DOM element
*
* @param xmlElement The DOM element to present
*/
XMLElement(Element xmlElement) {
dom = xmlElement;
name = dom.getTagName();
}
/**
* Get the names of the attributes specified on this element
*
* @return The names of the elements specified
*/
public String[] getAttributeNames() {
NamedNodeMap map = dom.getAttributes();
String[] names = new String[map.getLength()];
for (int i=0;i<names.length;i++) {
names[i] = map.item(i).getNodeName();
}
return names;
}
/**
* Get the name of this element
*
* @return The name of this element
*/
public String getName() {
return name;
}
/**
* Get the value specified for a given attribute on this element
*
* @param name The name of the attribute whose value should be retrieved
* @return The value given for the attribute
*/
public String getAttribute(String name) {
return dom.getAttribute(name);
}
/**
* Get the value specified for a given attribute on this element
*
* @param name The name of the attribute whose value should be retrieved
* @param def The default value to return if the attribute is specified
* @return The value given for the attribute
*/
public String getAttribute(String name, String def) {
String value = dom.getAttribute(name);
if ((value == null) || (value.length() == 0)) {
return def;
}
return value;
}
/**
* Get the value specified for a given attribute on this element as an integer.
*
* @param name The name of the attribute whose value should be retrieved
* @return The value given for the attribute
* @throws SlickXMLException Indicates a failure to convert the value into an integer
*/
public int getIntAttribute(String name) throws SlickXMLException {
try {
return Integer.parseInt(getAttribute(name));
} catch (NumberFormatException e) {
throw new SlickXMLException("Value read: '"+getAttribute(name)+"' is not an integer",e);
}
}
/**
* Get the value specified for a given attribute on this element as an integer.
*
* @param name The name of the attribute whose value should be retrieved
* @param def The default value to return if the attribute is specified
* @return The value given for the attribute
* @throws SlickXMLException Indicates a failure to convert the value into an integer
*/
public int getIntAttribute(String name, int def) throws SlickXMLException {
try {
return Integer.parseInt(getAttribute(name,""+def));
} catch (NumberFormatException e) {
throw new SlickXMLException("Value read: '"+getAttribute(name, ""+def)+"' is not an integer",e);
}
}
/**
* Get the value specified for a given attribute on this element as an double.
*
* @param name The name of the attribute whose value should be retrieved
* @return The value given for the attribute
* @throws SlickXMLException Indicates a failure to convert the value into an double
*/
public double getDoubleAttribute(String name) throws SlickXMLException {
try {
return Double.parseDouble(getAttribute(name));
} catch (NumberFormatException e) {
throw new SlickXMLException("Value read: '"+getAttribute(name)+"' is not a double",e);
}
}
/**
* Get the value specified for a given attribute on this element as an double.
*
* @param name The name of the attribute whose value should be retrieved
* @param def The default value to return if the attribute is specified
* @return The value given for the attribute
* @throws SlickXMLException Indicates a failure to convert the value into an double
*/
public double getDoubleAttribute(String name, double def) throws SlickXMLException {
try {
return Double.parseDouble(getAttribute(name,""+def));
} catch (NumberFormatException e) {
throw new SlickXMLException("Value read: '"+getAttribute(name, ""+def)+"' is not a double",e);
}
}
/**
* Get the value specified for a given attribute on this element as a boolean.
*
* @param name The name of the attribute whose value should be retrieved
* @return The value given for the attribute
* @throws SlickXMLException Indicates a failure to convert the value into an boolean
*/
public boolean getBooleanAttribute(String name) throws SlickXMLException {
String value = getAttribute(name);
if (value.equalsIgnoreCase("true")) {
return true;
}
if (value.equalsIgnoreCase("false")) {
return false;
}
throw new SlickXMLException("Value read: '"+getAttribute(name)+"' is not a boolean");
}
/**
* Get the value specified for a given attribute on this element as a boolean.
*
* @param name The name of the attribute whose value should be retrieved
* @param def The default value to return if the attribute is specified
* @return The value given for the attribute
* @throws SlickXMLException Indicates a failure to convert the value into an boolean
*/
public boolean getBooleanAttribute(String name, boolean def) throws SlickXMLException {
String value = getAttribute(name,""+def);
if (value.equalsIgnoreCase("true")) {
return true;
}
if (value.equalsIgnoreCase("false")) {
return false;
}
throw new SlickXMLException("Value read: '"+getAttribute(name, ""+def)+"' is not a boolean");
}
/**
* Get the text content of the element, i.e. the bit between the tags
*
* @return The text content of the node
*/
public String getContent() {
String content = "";
NodeList list = dom.getChildNodes();
for (int i=0;i<list.getLength();i++) {
if (list.item(i) instanceof Text) {
content += (list.item(i).getNodeValue());
}
}
return content;
}
/**
* Get the complete list of children for this node
*
* @return The list of children for this node
*/
public XMLElementList getChildren() {
if (children != null) {
return children;
}
NodeList list = dom.getChildNodes();
children = new XMLElementList();
for (int i=0;i<list.getLength();i++) {
if (list.item(i) instanceof Element) {
children.add(new XMLElement((Element) list.item(i)));
}
}
return children;
}
/**
* Get a list of children with a given element name
*
* @param name The name of the element type that should be retrieved
* @return A list of elements
*/
public XMLElementList getChildrenByName(String name) {
XMLElementList selected = new XMLElementList();
XMLElementList children = getChildren();
for (int i=0;i<children.size();i++) {
if (children.get(i).getName().equals(name)) {
selected.add(children.get(i));
}
}
return selected;
}
/**
* @see java.lang.Object#toString()
*/
public String toString() {
String value = "[XML "+getName();
String[] attrs = getAttributeNames();
for (int i=0;i<attrs.length;i++) {
value += " "+attrs[i]+"="+getAttribute(attrs[i]);
}
value += "]";
return value;
}
}

View File

@@ -0,0 +1,68 @@
package org.newdawn.slick.util.xml;
import java.util.ArrayList;
import java.util.Collection;
/**
* A simple typed list.
*
* @author kevin
*/
public class XMLElementList {
/** The list of elements */
private ArrayList list = new ArrayList();
/**
* Create a new list
*/
public XMLElementList() {
}
/**
* Add an element to the list
*
* @param element The element to be added
*/
public void add(XMLElement element) {
list.add(element);
}
/**
* Get the number of elements in the list
*
* @return The number of elements in the list
*/
public int size() {
return list.size();
}
/**
* Get the element at a specified index
*
* @param i The index of the element
* @return The element at the specified index
*/
public XMLElement get(int i) {
return (XMLElement) list.get(i);
}
/**
* Check if this list contains the given element
*
* @param element The element to check for
* @return True if the element is in the list
*/
public boolean contains(XMLElement element) {
return list.contains(element);
}
/**
* Add all the elements in this list to another collection
*
* @param collection The collection the elements should be added to
*/
public void addAllTo(Collection collection) {
collection.addAll(list);
}
}

View File

@@ -0,0 +1,63 @@
package org.newdawn.slick.util.xml;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.util.ResourceLoader;
import org.w3c.dom.Document;
/**
* A simple utility wrapper around the Java DOM implementation to hopefully
* make XML parsing that bit easier without requiring YAL.
*
* @author kevin
*/
public class XMLParser {
/** The factory used to to create document builders that parse XML into the DOM */
private static DocumentBuilderFactory factory;
/**
* Create a new parser
*/
public XMLParser() {
}
/**
* Parse the XML document located by the slick resource loader using the
* reference given.
*
* @param ref The reference to the XML document
* @return The root element of the newly parse document
* @throws SlickException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public XMLElement parse(String ref) throws SlickException {
return parse(ref, ResourceLoader.getResourceAsStream(ref));
}
/**
* Parse the XML document that can be read from the given input stream
*
* @param name The name of the document
* @param in The input stream from which the document can be read
* @return The root element of the newly parse document
* @throws SlickXMLException Indicates a failure to parse the XML, most likely the
* XML is malformed in some way.
*/
public XMLElement parse(String name, InputStream in) throws SlickXMLException {
try {
if (factory == null) {
factory = DocumentBuilderFactory.newInstance();
}
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(in);
return new XMLElement(doc.getDocumentElement());
} catch (Exception e) {
throw new SlickXMLException("Failed to parse document: "+name, e);
}
}
}

View File

@@ -0,0 +1,3 @@
<BODY>
Some utilities for reading XML using Java DOM and for mapping XML onto existing data models
</BODY>