diff --git a/README.md b/README.md index c74d5dae..695b1cbe 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,11 @@ This database must contain a table called "users" laid out in the following way: **users** -| username | password | salt | loggedin | xCoord | yCoord | zoneID | -| ------------- | ----------------------------------------------------------------- | ------------------------------------------------ | ---------- | ------ | ------ | ------ | -| TestAccount01 | 8b678bbcf5cf2a60c6dc631b01d6b3c77d142d05eb521a62f73014cc987e0156 | 66db065da6853ec1dafb45933c77b3fdac9ce354a391e8d3 | 0 | 0 | 0 | 0 | -| TestAccount02 | 650f00f552d4df0147d236e240ccfc490444f4b358c4ff1d79f5fd90f57243bd | e3c42b85a183d3f654a3d2bb3bc5ea607d0fb529d9b890d3 | 0 | 0 | 0 | 0 | +| username | password | salt | loggedin | xCoord | yCoord | zoneID | +| ---------------------- | ----------------------------------------------------------------- | ------------------------------------------------ | ---------- | ------ | ------ | ------ | +| TestAccount1 | 8b678bbcf5cf2a60c6dc631b01d6b3c77d142d05eb521a62f73014cc987e0156 | 66db065da6853ec1dafb45933c77b3fdac9ce354a391e8d3 | 0 | 0 | 0 | 0 | +| TestAccount2 | 650f00f552d4df0147d236e240ccfc490444f4b358c4ff1d79f5fd90f57243bd | e3c42b85a183d3f654a3d2bb3bc5ea607d0fb529d9b890d3 | 0 | 0 | 0 | 0 | +| TestAccount...(to 99) | 650f00f552d4df0147d236e240ccfc490444f4b358c4ff1d79f5fd90f57243bd | e3c42b85a183d3f654a3d2bb3bc5ea607d0fb529d9b890d3 | 0 | 0 | 0 | 0 | Any tests that utilize the login functionality will fail without this table. @@ -51,9 +52,7 @@ fields in the MySQL database. The Jenjin core architecture uses the following unmodified libraries: -* [JUnit](https://github.com/junit-team/junit) - * License: EPL (Commercial Friendly) -* [Hamcrest](https://github.com/hamcrest/JavaHamcrest) - * License: BSD (Commercial Friendly) +* [TestNG](http://testng.org/doc/index.html) + * License: Apache 2.0 * [Drizzle](https://github.com/krummas/DrizzleJDBC) - * License: BSD (Commercial Friendly) + * License: BSD diff --git a/build.gradle b/build.gradle index da4cd9be..01e63821 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ allprojects { group = 'com.jenjinstudios' - version = '0.4.0-alpha' + version = '0.5.0-alpha' } @@ -22,11 +22,19 @@ subprojects { } dependencies { - testCompile 'junit:junit:4.11' + testCompile 'org.testng:testng:6.8.7' } test { + useTestNG() + maxParallelForks = 8; workingDir = project.rootDir + beforeTest { descriptor -> + logger.lifecycle("Running test: " + descriptor) + } + afterTest { descriptor -> + logger.lifecycle("Completed test: " + descriptor) + } } tasks.withType(Compile) { diff --git a/jenjin-client-world/build.gradle b/jenjin-client-world/build.gradle deleted file mode 100644 index 0a7f2c07..00000000 --- a/jenjin-client-world/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -description = '' - -dependencies { - compile project(':jenjin-client') - compile project(':jenjin-world-utils') -} diff --git a/jenjin-client/build.gradle b/jenjin-client/build.gradle deleted file mode 100644 index fe89f158..00000000 --- a/jenjin-client/build.gradle +++ /dev/null @@ -1,5 +0,0 @@ -description = '' - -dependencies { - compile project(':jenjin-io') -} diff --git a/jenjin-core-client/build.gradle b/jenjin-core-client/build.gradle new file mode 100644 index 00000000..6646ceab --- /dev/null +++ b/jenjin-core-client/build.gradle @@ -0,0 +1,5 @@ +description = '' + +dependencies { + compile project(':jenjin-core') +} diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/AuthClientExecutableMessage.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/AuthClientExecutableMessage.java similarity index 100% rename from jenjin-client/src/main/java/com/jenjinstudios/message/AuthClientExecutableMessage.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/AuthClientExecutableMessage.java diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/ClientExecutableInvalidMessage.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ClientExecutableInvalidMessage.java similarity index 100% rename from jenjin-client/src/main/java/com/jenjinstudios/message/ClientExecutableInvalidMessage.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/ClientExecutableInvalidMessage.java diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/ClientExecutableMessage.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ClientExecutableMessage.java similarity index 92% rename from jenjin-client/src/main/java/com/jenjinstudios/message/ClientExecutableMessage.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/ClientExecutableMessage.java index c268c9ed..7e05b4b7 100644 --- a/jenjin-client/src/main/java/com/jenjinstudios/message/ClientExecutableMessage.java +++ b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ClientExecutableMessage.java @@ -2,7 +2,6 @@ import com.jenjinstudios.io.ExecutableMessage; import com.jenjinstudios.io.Message; -import com.jenjinstudios.io.MessageRegistry; import com.jenjinstudios.io.MessageType; import com.jenjinstudios.net.Client; @@ -30,7 +29,7 @@ public abstract class ClientExecutableMessage extends ExecutableMessage protected ClientExecutableMessage(Client client, Message message) { super(message); - if (!getClass().isAssignableFrom(MessageRegistry.getMessageType(message.getID()).clientExecutableMessageClass)) + if (!getClass().isAssignableFrom(client.getMessageRegistry().getMessageType(message.getID()).clientExecutableMessageClass)) throw new IllegalArgumentException("Message supplied to " + getClass().getName() + "is invalid."); this.client = client; @@ -46,7 +45,7 @@ protected ClientExecutableMessage(Client client, Message message) { public static ExecutableMessage getClientExecutableMessageFor(Client client, Message message) { ExecutableMessage r = null; - MessageType messageType = MessageRegistry.getMessageType(message.getID()); + MessageType messageType = client.getMessageRegistry().getMessageType(message.getID()); Class execClass = messageType.clientExecutableMessageClass; try diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/ExecutableAESKeyMessage.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutableAESKeyMessage.java similarity index 100% rename from jenjin-client/src/main/java/com/jenjinstudios/message/ExecutableAESKeyMessage.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutableAESKeyMessage.java diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/ExecutableLoginResponse.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutableLoginResponse.java similarity index 100% rename from jenjin-client/src/main/java/com/jenjinstudios/message/ExecutableLoginResponse.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutableLoginResponse.java diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/ExecutableLogoutResponse.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutableLogoutResponse.java similarity index 100% rename from jenjin-client/src/main/java/com/jenjinstudios/message/ExecutableLogoutResponse.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutableLogoutResponse.java diff --git a/jenjin-client/src/main/java/com/jenjinstudios/message/ExecutablePingResponse.java b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutablePingResponse.java similarity index 90% rename from jenjin-client/src/main/java/com/jenjinstudios/message/ExecutablePingResponse.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutablePingResponse.java index aefecffe..a2212128 100644 --- a/jenjin-client/src/main/java/com/jenjinstudios/message/ExecutablePingResponse.java +++ b/jenjin-core-client/src/main/java/com/jenjinstudios/message/ExecutablePingResponse.java @@ -23,7 +23,7 @@ public ExecutablePingResponse(Client client, Message message) { public void runSynced() { long requestTime = (long) getMessage().getArgument("requestTimeNanos"); long updateTime = getClient().getPeriod() * 1000000; - getClient().addPingTime(System.nanoTime() - requestTime - updateTime); + getClient().addPingTime((System.nanoTime() - requestTime - updateTime) / 1000000); } /** Run asynchronous portion of this message. */ diff --git a/jenjin-client/src/main/java/com/jenjinstudios/net/AuthClient.java b/jenjin-core-client/src/main/java/com/jenjinstudios/net/AuthClient.java similarity index 85% rename from jenjin-client/src/main/java/com/jenjinstudios/net/AuthClient.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/net/AuthClient.java index f3877358..372db46e 100644 --- a/jenjin-client/src/main/java/com/jenjinstudios/net/AuthClient.java +++ b/jenjin-core-client/src/main/java/com/jenjinstudios/net/AuthClient.java @@ -1,7 +1,10 @@ package com.jenjinstudios.net; import com.jenjinstudios.io.Message; +import org.xml.sax.SAXException; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.logging.Level; import java.util.logging.Logger; @@ -37,8 +40,11 @@ public class AuthClient extends Client * @param username The username that will be used by this client. * @param password The password that will be used by this client. * @throws java.security.NoSuchAlgorithmException If there is an error generating encryption keys. + * @throws java.io.IOException If there is an IO exception when reading XML files. + * @throws javax.xml.parsers.ParserConfigurationException If there is an error parsing XML files. + * @throws org.xml.sax.SAXException If there is an error parsing XML files. */ - public AuthClient(String address, int port, String username, String password) throws NoSuchAlgorithmException { + public AuthClient(String address, int port, String username, String password) throws NoSuchAlgorithmException, IOException, SAXException, ParserConfigurationException { super(address, port); this.username = username; this.password = password; @@ -51,8 +57,8 @@ public AuthClient(String address, int port, String username, String password) th public boolean sendBlockingLoginRequest() { sendLoginRequest(); long startTime = System.currentTimeMillis(); - long timepast = System.currentTimeMillis() - startTime; - while (isWaitingForLoginResponse() && (timepast < TIMEOUT_MILLIS)) + long timePast = System.currentTimeMillis() - startTime; + while (isWaitingForLoginResponse() && (timePast < TIMEOUT_MILLIS)) { try { @@ -61,7 +67,7 @@ public boolean sendBlockingLoginRequest() { { LOGGER.log(Level.WARNING, "Interrupted while waiting for login response.", e); } - timepast = System.currentTimeMillis() - startTime; + timePast = System.currentTimeMillis() - startTime; } return isLoggedIn(); } @@ -100,7 +106,7 @@ private void sendLoginRequest() { * @return The LoginRequest message. */ private Message generateLoginRequest() {// Create the login request. - Message loginRequest = new Message("LoginRequest"); + Message loginRequest = new Message(this, "LoginRequest"); loginRequest.setArgument("username", username); loginRequest.setArgument("password", password); return loginRequest; @@ -129,8 +135,8 @@ public boolean isWaitingForLoginResponse() { public boolean sendBlockingLogoutRequest() { sendLogoutRequest(); long startTime = System.currentTimeMillis(); - long timepast = System.currentTimeMillis() - startTime; - while (isWaitingForLogoutResponse() && (timepast < TIMEOUT_MILLIS)) + long timePast = System.currentTimeMillis() - startTime; + while (isWaitingForLogoutResponse() && (timePast < TIMEOUT_MILLIS)) { try { @@ -139,14 +145,14 @@ public boolean sendBlockingLogoutRequest() { { LOGGER.log(Level.WARNING, "Interrupted while waiting for login response.", e); } - timepast = System.currentTimeMillis() - startTime; + timePast = System.currentTimeMillis() - startTime; } return !isLoggedIn(); } /** Send a logout request to the server. */ protected void sendLogoutRequest() { - Message logoutRequest = new Message("LogoutRequest"); + Message logoutRequest = new Message(this, "LogoutRequest"); // Send the request, continue when response is received. setWaitingForLogoutResponse(true); diff --git a/jenjin-client/src/main/java/com/jenjinstudios/net/Client.java b/jenjin-core-client/src/main/java/com/jenjinstudios/net/Client.java similarity index 85% rename from jenjin-client/src/main/java/com/jenjinstudios/net/Client.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/net/Client.java index b5c223e6..971613fc 100644 --- a/jenjin-client/src/main/java/com/jenjinstudios/net/Client.java +++ b/jenjin-core-client/src/main/java/com/jenjinstudios/net/Client.java @@ -1,7 +1,8 @@ package com.jenjinstudios.net; -import com.jenjinstudios.message.ClientExecutableMessage; import com.jenjinstudios.io.Message; +import com.jenjinstudios.io.MessageRegistry; +import com.jenjinstudios.message.ClientExecutableMessage; import java.io.IOException; import java.net.Socket; @@ -19,6 +20,8 @@ public class Client extends Connection { /** The logger associated with this class. */ public static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + /** The number of milliseconds before a blocking method should time out. */ + public static long TIMEOUT_MILLIS = 30000; /** The port over which the client communicates with the server. */ private final int PORT; /** The address of the server to which this client will connect. */ @@ -33,20 +36,17 @@ public class Client extends Connection private PublicKey publicKey; /** The private key sent to the server. */ private PrivateKey privateKey; - /** The number of milliseconds before a blocking method should time out. */ - public static long TIMEOUT_MILLIS = 30000; /** * Construct a new client and attempt to connect to the server over the specified port. * @param address The address of the server to which to connect * @param port The port over which to connect to the server. - * @throws java.security.NoSuchAlgorithmException If there is an error generating encryption keys. */ - protected Client(String address, int port) throws NoSuchAlgorithmException { + protected Client(String address, int port) { ADDRESS = address; PORT = port; repeatedSyncedTasks = new LinkedList<>(); - + setMessageRegistry(new MessageRegistry(false)); try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); @@ -57,7 +57,6 @@ protected Client(String address, int port) throws NoSuchAlgorithmException { } catch (NoSuchAlgorithmException e) { LOGGER.log(Level.SEVERE, "Unable to create RSA key pair!", e); - throw e; } } @@ -95,14 +94,49 @@ public boolean blockingStart() throws InterruptedException { @Override public final void run() { if (!isConnected()) connect(); - // The ClientLoop is used to send messages in the outgoing queue and do syncrhonized executables. - + if (!isConnected()) + return; + // The ClientLoop is used to send messages in the outgoing queue and do synchronized actions. sendMessagesTimer = new Timer("Client Update Loop", false); sendMessagesTimer.scheduleAtFixedRate(new ClientLoop(this), 0, period); super.run(); } + /** Tell the client threads to stop running. */ + public void shutdown() { + super.shutdown(); + sendMessagesTimer.cancel(); + closeLink(); + } + + /** + * Add a task to the list of repeated synchronized tasks. + * @param r The task to add. + */ + public void addRepeatedSyncedTask(Runnable r) { + synchronized (repeatedSyncedTasks) + { + repeatedSyncedTasks.add(r); + } + } + + /** + * Get the private key. + * @return The private key. + */ + public PrivateKey getPrivateKey() { + return privateKey; + } + + /** + * Get the update period of this client. + * @return The update period of this client. + */ + public int getPeriod() { + return period; + } + /** * Attempt to connect to the server at {@code ADDRESS} over {@code PORT} This method must be called before the * client thread is started. @@ -113,8 +147,7 @@ private void connect() { try { super.setSocket(new Socket(ADDRESS, PORT)); - doPostConnectInit(); - super.setConnected(true); + super.setConnected(doPostConnectInit()); } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Unable to connect to server.", ex); @@ -124,28 +157,27 @@ private void connect() { /** * Take care of all the necessary initialization messages between client and server. These include things like RSA key * exchanges and latency checks. + * @return Whether the init was successful. * @throws IOException If there's an IOException when attempting to communicate with the server. */ - private void doPostConnectInit() throws IOException { + private boolean doPostConnectInit() throws IOException { // First, get and process the required FirstConnectResponse message from the server. Message firstConnectResponse = getInputStream().readMessage(); + if (firstConnectResponse == null) + { + return false; + } int ups = (int) firstConnectResponse.getArgument("ups"); period = 1000 / ups; // Next, queue up the PublicKeyMessage used to exchange the encrypted AES key used for encryption. - Message publicKeyMessage = new Message("PublicKeyMessage"); + Message publicKeyMessage = new Message(this, "PublicKeyMessage"); publicKeyMessage.setArgument("key", publicKey.getEncoded()); queueMessage(publicKeyMessage); // Finally, send a ping request to establish latency. sendPing(); - } - - /** Tell the client threads to stop running. */ - public void shutdown() { - super.shutdown(); - sendMessagesTimer.cancel(); - closeLink(); + return true; } /** @@ -158,27 +190,12 @@ protected ClientExecutableMessage getExecutableMessage(Message message) { return (ClientExecutableMessage) ClientExecutableMessage.getClientExecutableMessageFor(this, message); } - /** - * Get the list of repeating tasks. - * @return The list of repeating tasks. - */ - public LinkedList getRepeatedSyncedTasks() { - return repeatedSyncedTasks; - } - - /** - * Get the private key. - * @return The private key. - */ - public PrivateKey getPrivateKey() { - return privateKey; - } - - /** - * Get the update period of this client. - * @return The update period of this client. - */ - public int getPeriod() { - return period; + /** Run the repeated synchronized tasks. */ + void runRepeatedSyncedTasks() { + synchronized (repeatedSyncedTasks) + { + for (Runnable r : repeatedSyncedTasks) + r.run(); + } } } diff --git a/jenjin-client/src/main/java/com/jenjinstudios/net/ClientLoop.java b/jenjin-core-client/src/main/java/com/jenjinstudios/net/ClientLoop.java similarity index 80% rename from jenjin-client/src/main/java/com/jenjinstudios/net/ClientLoop.java rename to jenjin-core-client/src/main/java/com/jenjinstudios/net/ClientLoop.java index 7299dc79..f8d33838 100644 --- a/jenjin-client/src/main/java/com/jenjinstudios/net/ClientLoop.java +++ b/jenjin-core-client/src/main/java/com/jenjinstudios/net/ClientLoop.java @@ -22,13 +22,9 @@ public ClientLoop(Client client) { @Override public void run() { - for (Runnable r : client.getRepeatedSyncedTasks()) - r.run(); - for (Runnable r : client.getSyncedTasks()) - r.run(); - + client.runRepeatedSyncedTasks(); + client.runSyncedTasks(); client.sendAllMessages(); - } } \ No newline at end of file diff --git a/jenjin-client/src/main/resources/com/jenjinstudios/net/Messages.xml b/jenjin-core-client/src/main/resources/com/jenjinstudios/net/Messages.xml similarity index 91% rename from jenjin-client/src/main/resources/com/jenjinstudios/net/Messages.xml rename to jenjin-core-client/src/main/resources/com/jenjinstudios/net/Messages.xml index 3615dc6c..73550a4f 100644 --- a/jenjin-client/src/main/resources/com/jenjinstudios/net/Messages.xml +++ b/jenjin-core-client/src/main/resources/com/jenjinstudios/net/Messages.xml @@ -2,6 +2,10 @@ + + com.jenjinstudios.message.DisabledExecutableMessage + + @@ -54,6 +58,6 @@ com.jenjinstudios.message.ExecutablePingResponse - + \ No newline at end of file diff --git a/jenjin-server/build.gradle b/jenjin-core-server/build.gradle similarity index 58% rename from jenjin-server/build.gradle rename to jenjin-core-server/build.gradle index 0f441718..8d806e84 100644 --- a/jenjin-server/build.gradle +++ b/jenjin-core-server/build.gradle @@ -3,7 +3,7 @@ description = '' dependencies { compile group: 'org.drizzle.jdbc', name: 'drizzle-jdbc', version: '1.2' - compile project(':jenjin-io') - compile project(':jenjin-client') + compile project(':jenjin-core') + compile project(':jenjin-core-client') } diff --git a/jenjin-core-server/src/main/java/com/jenjinstudios/message/DisabledExecutableMessage.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/DisabledExecutableMessage.java new file mode 100644 index 00000000..9a585a1a --- /dev/null +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/message/DisabledExecutableMessage.java @@ -0,0 +1,38 @@ +package com.jenjinstudios.message; + +import com.jenjinstudios.io.Message; +import com.jenjinstudios.net.ClientHandler; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class is used only for testing disabled messages. + * @author Caleb Brinkman + */ +@SuppressWarnings("unused") +public class DisabledExecutableMessage extends ServerExecutableMessage +{ + /** The logger for this class. */ + private static final Logger LOGGER = Logger.getLogger(DisabledExecutableMessage.class.getName()); + + /** + * Construct a new ExecutableMessage. Must be implemented by subclasses. + * @param handler The handler using this ExecutableMessage. + * @param message The message. + */ + protected DisabledExecutableMessage(ClientHandler handler, Message message) { + super(handler, message); + } + + /** Run the synced portion of this message. */ + @Override + public void runSynced() { + } + + /** Run asynchronous portion of this message. */ + @Override + public void runASync() { + LOGGER.log(Level.SEVERE, "You should never see this, this message is disabled."); + } +} diff --git a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutableLoginRequest.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutableLoginRequest.java similarity index 91% rename from jenjin-server/src/main/java/com/jenjinstudios/message/ExecutableLoginRequest.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutableLoginRequest.java index 3b28aacd..fa13cb2a 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutableLoginRequest.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutableLoginRequest.java @@ -33,7 +33,7 @@ public void runASync() { boolean success = false; if (sqlHandler == null || getClientHandler().isLoggedIn()) { - Message loginResponse = new Message("LoginResponse"); + Message loginResponse = new Message(getClientHandler(), "LoginResponse"); loginResponse.setArgument("success", success); loginResponse.setArgument("loginTime", getClientHandler().getLoggedInTime()); getClientHandler().queueMessage(loginResponse); @@ -45,7 +45,7 @@ public void runASync() { getClientHandler().setLoginStatus(success); - Message loginResponse = new Message("LoginResponse"); + Message loginResponse = new Message(getClientHandler(), "LoginResponse"); loginResponse.setArgument("success", success); loginResponse.setArgument("loginTime", getClientHandler().getLoggedInTime()); getClientHandler().queueMessage(loginResponse); diff --git a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutableLogoutRequest.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutableLogoutRequest.java similarity index 100% rename from jenjin-server/src/main/java/com/jenjinstudios/message/ExecutableLogoutRequest.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutableLogoutRequest.java diff --git a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutablePingRequest.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutablePingRequest.java similarity index 94% rename from jenjin-server/src/main/java/com/jenjinstudios/message/ExecutablePingRequest.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutablePingRequest.java index 20aafcf7..8ba35878 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutablePingRequest.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutablePingRequest.java @@ -28,7 +28,7 @@ public void runSynced() { /** Run asynchronous portion of this message. */ @Override public void runASync() { - Message pingResponse = new Message("PingResponse"); + Message pingResponse = new Message(getClientHandler(), "PingResponse"); pingResponse.setArgument("requestTimeNanos", getMessage().getArgument("requestTimeNanos")); try { diff --git a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutablePublicKeyMessage.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutablePublicKeyMessage.java similarity index 97% rename from jenjin-server/src/main/java/com/jenjinstudios/message/ExecutablePublicKeyMessage.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutablePublicKeyMessage.java index 4701a64a..e52eb106 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/message/ExecutablePublicKeyMessage.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ExecutablePublicKeyMessage.java @@ -38,7 +38,7 @@ public void runSynced() { @Override public void runASync() { - Message aesMessage = new Message("AESKeyMessage"); + Message aesMessage = new Message(getClientHandler(), "AESKeyMessage"); byte[] encryptedAESKey = MessageInputStream.NO_KEY; try { diff --git a/jenjin-server/src/main/java/com/jenjinstudios/message/ServerExecutableInvalidMessage.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ServerExecutableInvalidMessage.java similarity index 100% rename from jenjin-server/src/main/java/com/jenjinstudios/message/ServerExecutableInvalidMessage.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/message/ServerExecutableInvalidMessage.java diff --git a/jenjin-server/src/main/java/com/jenjinstudios/message/ServerExecutableMessage.java b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ServerExecutableMessage.java similarity index 93% rename from jenjin-server/src/main/java/com/jenjinstudios/message/ServerExecutableMessage.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/message/ServerExecutableMessage.java index 1bd2b825..20cdd1e9 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/message/ServerExecutableMessage.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/message/ServerExecutableMessage.java @@ -2,7 +2,6 @@ import com.jenjinstudios.io.ExecutableMessage; import com.jenjinstudios.io.Message; -import com.jenjinstudios.io.MessageRegistry; import com.jenjinstudios.io.MessageType; import com.jenjinstudios.net.ClientHandler; @@ -41,7 +40,7 @@ protected ServerExecutableMessage(ClientHandler handler, Message message) { @SuppressWarnings("unchecked") public static ExecutableMessage getServerExecutableMessageFor(ClientHandler handler, Message message) { ExecutableMessage r = null; - MessageType messageType = MessageRegistry.getMessageType(message.getID()); + MessageType messageType = handler.getMessageRegistry().getMessageType(message.getID()); // Get the executable message classes registered. Class execClass = messageType.serverExecutableMessageClass; try @@ -69,7 +68,7 @@ public static ExecutableMessage getServerExecutableMessageFor(ClientHandler hand LOGGER.log(Level.SEVERE, "Constructor not correct for: " + execClass.getName(), e); } catch (NullPointerException e) { - LOGGER.log(Level.SEVERE, "No client-side executable message found for: " + message, e); + LOGGER.log(Level.SEVERE, "No server-side executable message found for: " + message + " " + messageType, e); } return r; diff --git a/jenjin-server/src/main/java/com/jenjinstudios/net/AuthServer.java b/jenjin-core-server/src/main/java/com/jenjinstudios/net/AuthServer.java similarity index 79% rename from jenjin-server/src/main/java/com/jenjinstudios/net/AuthServer.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/net/AuthServer.java index 3904b8ba..52e677e7 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/net/AuthServer.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/net/AuthServer.java @@ -1,7 +1,9 @@ package com.jenjinstudios.net; import com.jenjinstudios.sql.SQLHandler; +import org.xml.sax.SAXException; +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; /** @@ -21,8 +23,10 @@ public class AuthServer extends TaskedServer * @param sqlHandler The SqlHandler responsible for communicating with a MySql database. * @throws java.io.IOException If there is an IO Error when initializing the server. * @throws NoSuchMethodException If there is no appropriate constructor for the specified ClientHandler constructor. + * @throws javax.xml.parsers.ParserConfigurationException If there is an error parsing XML files. + * @throws org.xml.sax.SAXException If there is an error parsing XML files. */ - public AuthServer(int ups, int port, Class handlerClass, SQLHandler sqlHandler) throws IOException, NoSuchMethodException { + public AuthServer(int ups, int port, Class handlerClass, SQLHandler sqlHandler) throws IOException, NoSuchMethodException, ParserConfigurationException, SAXException { super(ups, port, handlerClass); this.sqlHandler = sqlHandler; } diff --git a/jenjin-server/src/main/java/com/jenjinstudios/net/ClientHandler.java b/jenjin-core-server/src/main/java/com/jenjinstudios/net/ClientHandler.java similarity index 92% rename from jenjin-server/src/main/java/com/jenjinstudios/net/ClientHandler.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/net/ClientHandler.java index 5e40b8cb..e875a503 100755 --- a/jenjin-server/src/main/java/com/jenjinstudios/net/ClientHandler.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/net/ClientHandler.java @@ -2,6 +2,7 @@ import com.jenjinstudios.io.ExecutableMessage; import com.jenjinstudios.io.Message; +import com.jenjinstudios.io.MessageRegistry; import com.jenjinstudios.message.ServerExecutableMessage; import java.io.IOException; @@ -30,14 +31,16 @@ public class ClientHandler extends Connection * send the client a FirstConnectResponse message with the server's UPS * @param s The server for which this handler works. * @param sk The socket used to communicate with the client. + * @param messageRegistry The MessageRegistry for this ClientHandler. * @throws IOException If the socket is unable to connect. */ - public ClientHandler(AuthServer s, Socket sk) throws IOException { + public ClientHandler(AuthServer s, Socket sk, MessageRegistry messageRegistry) throws IOException { setName("ClientHandler: " + sk.getInetAddress()); + setMessageRegistry(messageRegistry); server = s; super.setSocket(sk); - Message firstConnectResponse = new Message("FirstConnectResponse"); + Message firstConnectResponse = new Message(this, "FirstConnectResponse"); firstConnectResponse.setArgument("ups", server.UPS); queueMessage(firstConnectResponse); } @@ -116,7 +119,7 @@ public void setLoginStatus(boolean success) { */ public void sendLogoutStatus(boolean success) { loggedIn = !success; - Message logoutResponse = new Message("LogoutResponse"); + Message logoutResponse = new Message(this, "LogoutResponse"); logoutResponse.setArgument("success", success); queueMessage(logoutResponse); } diff --git a/jenjin-server/src/main/java/com/jenjinstudios/net/ClientListener.java b/jenjin-core-server/src/main/java/com/jenjinstudios/net/ClientListener.java similarity index 85% rename from jenjin-server/src/main/java/com/jenjinstudios/net/ClientListener.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/net/ClientListener.java index aff33f16..8decbc5e 100755 --- a/jenjin-server/src/main/java/com/jenjinstudios/net/ClientListener.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/net/ClientListener.java @@ -1,5 +1,7 @@ package com.jenjinstudios.net; +import com.jenjinstudios.io.MessageRegistry; + import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -27,26 +29,24 @@ class ClientListener implements Runnable /** The server socket. */ private ServerSocket serverSock; /** The server. */ - private final Server server; + private Server server; /** The constructor called to create new handlers. */ private Constructor handlerConstructor; /** * Construct a new ClientListener for the given server on the given port. - * @param s The server for which this listener will listen. + * @param serverClass The server for which this listener will listen. * @param p The port on which to listen. * @param handlerClass The class of the ClientHandler to be used by this server. * @throws IOException If there is an error listening on the port. * @throws NoSuchMethodException If there is no appropriate constructor for the specified ClientHandler constructor. */ - public ClientListener(Server s, int p, Class handlerClass) throws IOException, NoSuchMethodException { - server = s; + public ClientListener(Class serverClass, int p, Class handlerClass) throws IOException, NoSuchMethodException { PORT = p; /* The class of client handlers created by this listener. */ try { - handlerConstructor = handlerClass.getConstructor(s.getClass(), Socket.class); - + handlerConstructor = handlerClass.getConstructor(serverClass, Socket.class, MessageRegistry.class); } catch (NoSuchMethodException e) { LOGGER.log(Level.SEVERE, "Unable to find appropriate ClientHandler constructor: " + handlerClass.getName(), e); @@ -83,10 +83,12 @@ public void stopListening() throws IOException { serverSock.close(); } - /** Listen for clients in a new thread. If already listening this method does nothing. */ - public void listen() { + /** Listen for clients in a new thread. If already listening this method does nothing. + * @param tServer The server */ + public void startListening(Server tServer) { if (listening) return; + this.server = tServer; listening = true; new Thread(this, "Client Listener " + PORT).start(); } @@ -109,8 +111,7 @@ void addNewClient(T h) { private void addNewClient(Socket sock) { try { - T newHandler = handlerConstructor.newInstance(server, sock); - + T newHandler = handlerConstructor.newInstance(server, sock, server.getMessageRegistry()); addNewClient(newHandler); } catch (InstantiationException | IllegalAccessException e) { @@ -131,7 +132,6 @@ public void run() { addNewClient(sock); } catch (SocketException ignored) { - Server.LOGGER.log(Level.FINE, "Socket closed on connection attempt.: ", ignored); } catch (IOException e) { Server.LOGGER.log(Level.WARNING, "Error connecting to client: ", e); diff --git a/jenjin-server/src/main/java/com/jenjinstudios/net/Server.java b/jenjin-core-server/src/main/java/com/jenjinstudios/net/Server.java similarity index 81% rename from jenjin-server/src/main/java/com/jenjinstudios/net/Server.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/net/Server.java index 26bdbfe2..fba39cad 100755 --- a/jenjin-server/src/main/java/com/jenjinstudios/net/Server.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/net/Server.java @@ -1,5 +1,9 @@ package com.jenjinstudios.net; +import com.jenjinstudios.io.MessageRegistry; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; @@ -35,6 +39,8 @@ public class Server extends Thread private volatile boolean initialized; /** The current number of connected clients. */ private int numClients; + /** The MessageRegistry used by this server. */ + private final MessageRegistry messageRegistry; /** * Construct a new Server without a SQLHandler. @@ -43,8 +49,10 @@ public class Server extends Thread * @param handlerClass The class of ClientHandler used by this Server. * @throws java.io.IOException If there is an IO Error initializing the server. * @throws NoSuchMethodException If there is no appropriate constructor for the specified ClientHandler constructor. + * @throws javax.xml.parsers.ParserConfigurationException If there is an error parsing XML files. + * @throws org.xml.sax.SAXException If there is an error parsing XML files. */ - public Server(int ups, int port, Class handlerClass) throws IOException, NoSuchMethodException { + public Server(int ups, int port, Class handlerClass) throws IOException, NoSuchMethodException, ParserConfigurationException, SAXException { this(ups, port, handlerClass, DEFAULT_MAX_CLIENTS); } @@ -60,31 +68,18 @@ public Server(int ups, int port, Class handlerClass) throws IOExcep @SuppressWarnings("unchecked") public Server(int ups, int port, Class handlerClass, int maxClients) throws IOException, NoSuchMethodException { super("Server"); + messageRegistry = new MessageRegistry(true); LOGGER.log(Level.FINE, "Initializing Server."); UPS = ups; PERIOD = 1000 / ups; clientsByUsername = new TreeMap<>(); - clientListener = (ClientListener) new ClientListener<>(this, port, handlerClass); + clientListener = (ClientListener) new ClientListener<>(getClass(), port, handlerClass); clientHandlers = new ArrayList<>(); for (int i = 0; i < maxClients; i++) clientHandlers.add(null); numClients = 0; } - /** - * Schedule a client to be removed during the next update. - * @param handler The client handler to be removed. - */ - void removeClient(ClientHandler handler) { - synchronized (clientHandlers) - { - String username = handler.getUsername(); - if (username != null) { clientsByUsername.remove(username); } - clientHandlers.set(handler.getHandlerId(), null); - numClients--; - } - } - /** * Add new clients that have connected to the client listeners. * @return true if new clients were added. @@ -141,7 +136,7 @@ public void refresh() { /** Run the server. */ @Override public void run() { - clientListener.listen(); + clientListener.startListening(this); initialized = true; } @@ -151,14 +146,14 @@ public void run() { */ public final boolean blockingStart() { long startTime = System.currentTimeMillis(); - long timepast = System.currentTimeMillis() - startTime; + long timePast = System.currentTimeMillis() - startTime; start(); - while (!initialized && (timepast < TIMEOUT_MILLIS)) + while (!initialized && (timePast < TIMEOUT_MILLIS)) { try { Thread.sleep(10); - timepast = System.currentTimeMillis() - startTime; + timePast = System.currentTimeMillis() - startTime; } catch (InterruptedException e) { LOGGER.log(Level.WARNING, "Server blocking start was interrupted.", e); @@ -193,15 +188,12 @@ public void shutdown() throws IOException { * @param username The username of the client to look up. * @return The client with the username specified; null if there is no client with this username. */ - public T getClientHandlerByUsername(String username) { return clientsByUsername.get(username); } - - /** - * Called by ClientHandler when the client sets a username. - * @param username The username assigned to the ClientHandler. - * @param handler The ClientHandler that has had a username set. - */ - @SuppressWarnings("unchecked") - void clientUsernameSet(String username, ClientHandler handler) { clientsByUsername.put(username, (T) handler); } + public T getClientHandlerByUsername(String username) { + synchronized (clientsByUsername) + { + return clientsByUsername.get(username); + } + } /** * Get the current number of connected clients. @@ -210,4 +202,37 @@ public void shutdown() throws IOException { public int getNumClients() { return numClients; } + + /** + * Get the MessageRegistry used by this server. + * @return The MessageRegistry used by this server, + */ + public MessageRegistry getMessageRegistry() { return messageRegistry; } + + /** + * Schedule a client to be removed during the next update. + * @param handler The client handler to be removed. + */ + void removeClient(ClientHandler handler) { + String username = handler.getUsername(); + if (username != null) { clientsByUsername.remove(username); } + synchronized (clientHandlers) + { + clientHandlers.set(handler.getHandlerId(), null); + } + numClients--; + } + + /** + * Called by ClientHandler when the client sets a username. + * @param username The username assigned to the ClientHandler. + * @param handler The ClientHandler that has had a username set. + */ + @SuppressWarnings("unchecked") + void clientUsernameSet(String username, ClientHandler handler) { + synchronized (clientsByUsername) + { + clientsByUsername.put(username, (T) handler); + } + } } diff --git a/jenjin-server/src/main/java/com/jenjinstudios/net/ServerLoop.java b/jenjin-core-server/src/main/java/com/jenjinstudios/net/ServerLoop.java similarity index 100% rename from jenjin-server/src/main/java/com/jenjinstudios/net/ServerLoop.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/net/ServerLoop.java diff --git a/jenjin-server/src/main/java/com/jenjinstudios/net/TaskedServer.java b/jenjin-core-server/src/main/java/com/jenjinstudios/net/TaskedServer.java similarity index 86% rename from jenjin-server/src/main/java/com/jenjinstudios/net/TaskedServer.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/net/TaskedServer.java index 3ab7a0bc..a64f0e08 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/net/TaskedServer.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/net/TaskedServer.java @@ -1,5 +1,8 @@ package com.jenjinstudios.net; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.util.LinkedList; import java.util.Timer; @@ -26,8 +29,10 @@ public class TaskedServer extends Server * @param handlerClass The class of ClientHandler used by this Server. * @throws java.io.IOException If there is an IO Error initializing the server. * @throws NoSuchMethodException If there is no appropriate constructor for the specified ClientHandler constructor. + * @throws javax.xml.parsers.ParserConfigurationException If there is an error parsing XML files. + * @throws org.xml.sax.SAXException If there is an error parsing XML files. */ - public TaskedServer(int ups, int port, Class handlerClass) throws IOException, NoSuchMethodException { + public TaskedServer(int ups, int port, Class handlerClass) throws IOException, NoSuchMethodException, ParserConfigurationException, SAXException { super(ups, port, handlerClass); repeatedTasks = new LinkedList<>(); syncedTasks = new LinkedList<>(); @@ -101,7 +106,7 @@ LinkedList getRepeatedTasks() { /** * Synced tasks scheduled by client handlers. - * @return The list of syncrhonized tasks scheduled by ClientHandlers. + * @return The list of synchronized tasks scheduled by ClientHandlers. */ LinkedList getSyncedTasks() { return syncedTasks; diff --git a/jenjin-server/src/main/java/com/jenjinstudios/sql/SQLHandler.java b/jenjin-core-server/src/main/java/com/jenjinstudios/sql/SQLHandler.java similarity index 82% rename from jenjin-server/src/main/java/com/jenjinstudios/sql/SQLHandler.java rename to jenjin-core-server/src/main/java/com/jenjinstudios/sql/SQLHandler.java index 5fabb274..d30c3fe0 100644 --- a/jenjin-server/src/main/java/com/jenjinstudios/sql/SQLHandler.java +++ b/jenjin-core-server/src/main/java/com/jenjinstudios/sql/SQLHandler.java @@ -23,20 +23,14 @@ public class SQLHandler private static final Logger LOGGER = Logger.getLogger(SQLHandler.class.getName()); /** The String used in connection protocol. */ private static final String connectionStringProtocol = "jdbc:mysql:thin://"; - /** The username used to access the database. */ - private final String dbUsername; - /** The password used to access the database. */ - private final String dbPassword; /** The name of the database used by this server. */ protected final String dbName; - /** The url used to connect with the SQL database. */ - private final String dbUrl; + /** The connection used to communicate with the SQL database. */ + protected final Connection dbConnection; /** The string used to get all information about the user. */ private final String USER_QUERY; /** Flags whether this SQLHandler is connected to the database. */ private boolean connected; - /** The connection used to communicate with the SQL database. */ - protected Connection dbConnection; /** * Create a new SQLHandler with the given database information, and connect to the database. @@ -47,11 +41,8 @@ public class SQLHandler * @throws SQLException If there is an issue connecting to the database. */ public SQLHandler(String dbAddress, String dbName, String dbUsername, String dbPassword) throws SQLException { - /* The address of the database to which to connect. */ - this.dbUsername = dbUsername; - this.dbPassword = dbPassword; this.dbName = dbName; - dbUrl = connectionStringProtocol + dbAddress + "/" + dbName; + String dbUrl = connectionStringProtocol + dbAddress + "/" + dbName; try { Class.forName("org.drizzle.jdbc.DrizzleDriver").newInstance(); @@ -61,7 +52,8 @@ public SQLHandler(String dbAddress, String dbName, String dbUsername, String dbP } USER_QUERY = "SELECT * FROM " + dbName + ".users WHERE username = ?"; - connectToDatabase(); + dbConnection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword); + connected = true; } /** @@ -75,7 +67,7 @@ public SQLHandler(String dbAddress, String dbName, String dbUsername, String dbP * @return true if the user was logged in successfully, false if the user was already logged in or the update to the * database failed. */ - public synchronized boolean logInUser(String username, String password) { + public boolean logInUser(String username, String password) { boolean success = false; if (!connected) return success; @@ -112,7 +104,7 @@ public synchronized boolean logInUser(String username, String password) { * @return true if the user was logged out successfully, false if the user was already logged out or the update to the * database failed. */ - public synchronized boolean logOutUser(String username) { + public boolean logOutUser(String username) { boolean success = false; if (!connected) return success; @@ -136,16 +128,6 @@ public synchronized boolean logOutUser(String username) { return success; } - - /** - * Attempt to connect to the database. - * @throws SQLException If there is an error connecting to the SQL database. - */ - private void connectToDatabase() throws SQLException { - dbConnection = DriverManager.getConnection(dbUrl, dbUsername, dbPassword); - connected = true; - } - /** * Get whether this SQLHandler is connected to the database. * @return true if the SQLHandler has successfully connected to the database. @@ -161,9 +143,12 @@ public boolean isConnected() { * @throws SQLException If there is a SQL error. */ protected ResultSet makeUserQuery(String username) throws SQLException { - PreparedStatement statement = dbConnection.prepareStatement(USER_QUERY, TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE); - statement.setString(1, username); - return statement.executeQuery(); + synchronized (dbConnection) + { + PreparedStatement statement = dbConnection.prepareStatement(USER_QUERY, TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE); + statement.setString(1, username); + return statement.executeQuery(); + } } /** @@ -176,10 +161,13 @@ protected void updateLoggedinColumn(String username, boolean status) throws SQLE String newValue = status ? "1" : "0"; String updateLoggedInQuery = "UPDATE " + dbName + ".users SET " + LOGGED_IN_COLUMN + "=" + newValue + " WHERE " + "username = ?"; - PreparedStatement updateLoggedin; - updateLoggedin = dbConnection.prepareStatement(updateLoggedInQuery); - updateLoggedin.setString(1, username); - updateLoggedin.executeUpdate(); - updateLoggedin.close(); + PreparedStatement updateLoggedIn; + synchronized (dbConnection) + { + updateLoggedIn = dbConnection.prepareStatement(updateLoggedInQuery); + updateLoggedIn.setString(1, username); + updateLoggedIn.executeUpdate(); + updateLoggedIn.close(); + } } } diff --git a/jenjin-server/src/test/java/test/jenjinstudios/net/ServerTest.java b/jenjin-core-server/src/test/java/test/jenjinstudios/net/ServerTest.java similarity index 90% rename from jenjin-server/src/test/java/test/jenjinstudios/net/ServerTest.java rename to jenjin-core-server/src/test/java/test/jenjinstudios/net/ServerTest.java index bcc5ee6d..8a8ea26e 100644 --- a/jenjin-server/src/test/java/test/jenjinstudios/net/ServerTest.java +++ b/jenjin-core-server/src/test/java/test/jenjinstudios/net/ServerTest.java @@ -1,13 +1,12 @@ package test.jenjinstudios.net; -import com.jenjinstudios.io.MessageRegistry; import com.jenjinstudios.net.AuthClient; -import com.jenjinstudios.net.ClientHandler; import com.jenjinstudios.net.AuthServer; +import com.jenjinstudios.net.ClientHandler; import com.jenjinstudios.sql.SQLHandler; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; import java.io.IOException; @@ -34,7 +33,6 @@ public class ServerTest */ @BeforeClass public static void construct() throws Exception { - MessageRegistry.registerXmlMessages(true); /* The SQLHandler used for testing. */ SQLHandler sqlHandler = new SQLHandler("localhost", "jenjin_test", "jenjin_user", "jenjin_password"); @@ -63,7 +61,6 @@ public static void destroy() throws IOException, InterruptedException { while ((System.currentTimeMillis() - startTime) < 1500) Thread.sleep(1); - assertEquals(server.UPS, server.getAverageUPS(), 0.1); server.shutdown(); } @@ -75,7 +72,7 @@ public static void destroy() throws IOException, InterruptedException { public void testLoginLogout() throws Exception { assertEquals(0, server.getNumClients()); - goodClient01 = new AuthClient("localhost", 51019, "TestAccount01", "testPassword"); + goodClient01 = new AuthClient("localhost", 51019, "TestAccount1", "testPassword"); goodClient01.blockingStart(); assertTrue(goodClient01.sendBlockingLoginRequest()); @@ -96,7 +93,7 @@ public void testLoginLogout() throws Exception { @Test public void testIncorrectPassword() throws Exception { /* This client should fail to login. */ - AuthClient badClient = new AuthClient("127.0.0.1", 51019, "TestAccount02", "This is an incorrect password. Teehee."); + AuthClient badClient = new AuthClient("127.0.0.1", 51019, "TestAccount2", "This is an incorrect password."); badClient.blockingStart(); assertFalse(badClient.sendBlockingLoginRequest()); @@ -111,13 +108,13 @@ public void testIncorrectPassword() throws Exception { */ @Test public void testGetLoggedInTime() throws Exception { - goodClient01 = new AuthClient("localhost", 51019, "TestAccount01", "testPassword"); + goodClient01 = new AuthClient("localhost", 51019, "TestAccount1", "testPassword"); goodClient01.blockingStart(); assertTrue(goodClient01.isRunning()); assertTrue(goodClient01.sendBlockingLoginRequest()); - ClientHandler handler = server.getClientHandlerByUsername("TestAccount01"); + ClientHandler handler = server.getClientHandlerByUsername("TestAccount1"); assertEquals(handler.getLoggedInTime(), goodClient01.getLoggedInTime()); assertTrue(goodClient01.sendBlockingLogoutRequest()); @@ -130,10 +127,10 @@ public void testGetLoggedInTime() throws Exception { */ @Test public void testAlreadyLoggedIn() throws Exception { - goodClient01 = new AuthClient("localhost", 51019, "TestAccount01", "testPassword"); + goodClient01 = new AuthClient("localhost", 51019, "TestAccount1", "testPassword"); assertTrue(goodClient01.blockingStart()); - sameClient = new AuthClient("127.0.0.1", 51019, "TestAccount01", "testPassword"); + sameClient = new AuthClient("127.0.0.1", 51019, "TestAccount1", "testPassword"); sameClient.blockingStart(); assertTrue(goodClient01.sendBlockingLoginRequest()); @@ -155,10 +152,10 @@ public void testAlreadyLoggedIn() throws Exception { */ @Test public void testEmergencyLogout() throws Exception { - goodClient01 = new AuthClient("localhost", 51019, "TestAccount01", "testPassword"); + goodClient01 = new AuthClient("localhost", 51019, "TestAccount1", "testPassword"); goodClient01.blockingStart(); - sameClient = new AuthClient("127.0.0.1", 51019, "TestAccount01", "testPassword"); + sameClient = new AuthClient("127.0.0.1", 51019, "TestAccount1", "testPassword"); sameClient.blockingStart(); // This client logs in and shuts down before sending a proper logout request. diff --git a/jenjin-io/build.gradle b/jenjin-core/build.gradle similarity index 100% rename from jenjin-io/build.gradle rename to jenjin-core/build.gradle diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/ArgumentType.java b/jenjin-core/src/main/java/com/jenjinstudios/io/ArgumentType.java similarity index 92% rename from jenjin-io/src/main/java/com/jenjinstudios/io/ArgumentType.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/ArgumentType.java index 26eeff50..7dff360f 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/ArgumentType.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/ArgumentType.java @@ -25,4 +25,7 @@ public ArgumentType(String name, Class type, boolean encrypt) { this.name = name; this.type = type; } + + @Override + public String toString() { return name + ", " + type; } } diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/ExecutableMessage.java b/jenjin-core/src/main/java/com/jenjinstudios/io/ExecutableMessage.java similarity index 100% rename from jenjin-io/src/main/java/com/jenjinstudios/io/ExecutableMessage.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/ExecutableMessage.java diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/Message.java b/jenjin-core/src/main/java/com/jenjinstudios/io/Message.java similarity index 89% rename from jenjin-io/src/main/java/com/jenjinstudios/io/Message.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/Message.java index e35a21fb..e1bba019 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/Message.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/Message.java @@ -1,5 +1,7 @@ package com.jenjinstudios.io; +import com.jenjinstudios.net.Connection; + import java.util.TreeMap; /** @@ -24,13 +26,14 @@ public class Message * shouldn't) the arguments you pass must fill every available argument and be passed in the order in which they * appear in the XML file. * + * @param connection The Connection creating this message. * @param id The ID of the message type for this message. * @param args The arguments of this message. This must fill every available argument for the message. */ - public Message(short id, Object... args) + public Message(Connection connection, short id, Object... args) { this.id = id; - messageType = MessageRegistry.getMessageType(id); + messageType = connection.getMessageRegistry().getMessageType(id); name = messageType.name; argumentsByName = new TreeMap<>(); for (int i = 0; i < messageType.argumentTypes.length; i++) @@ -44,11 +47,12 @@ public Message(short id, Object... args) * Construct a new Message using the MessageType specified by the given name; every argument in this message must be * set using the {@code setArgument} method before it can be sent properly over socket. * + * @param connection The Connection creating this message. * @param name The name of the MessageType being filled by this message. */ - public Message(String name) + public Message(Connection connection, String name) { - messageType = MessageRegistry.getMessageType(name); + messageType = connection.getMessageRegistry().getMessageType(name); this.name = messageType.name; id = messageType.id; argumentsByName = new TreeMap<>(); @@ -124,4 +128,10 @@ public final Object[] getArgs() } return argsArray; } + + @Override + public String toString() + { + return "Message " + id + " " + name + " " + argumentsByName; + } } diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageInputStream.java b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageInputStream.java similarity index 91% rename from jenjin-io/src/main/java/com/jenjinstudios/io/MessageInputStream.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/MessageInputStream.java index 64c72f49..c136b00f 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageInputStream.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageInputStream.java @@ -1,13 +1,13 @@ package com.jenjinstudios.io; +import com.jenjinstudios.net.Connection; + import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import java.io.DataInputStream; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; -import java.net.SocketException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.LinkedList; @@ -26,6 +26,8 @@ public class MessageInputStream private static final Logger LOGGER = Logger.getLogger(MessageInputStream.class.getName()); /** The output stream used by this message stream. */ private final DataInputStream inputStream; + /** The Connection using this stream. */ + private final Connection connection; /** The AES key used to encrypt outgoing messages. */ private SecretKey aesKey; /** The cipher used to decrypt messages. */ @@ -33,9 +35,11 @@ public class MessageInputStream /** * Construct a new {@code MessageInputStream} from the given InputStream. + * @param connection The connection using this stream. * @param inputStream The InputStream from which messages will be read. */ - public MessageInputStream(InputStream inputStream) { + public MessageInputStream(Connection connection, InputStream inputStream) { + this.connection = connection; this.inputStream = new DataInputStream(inputStream); } @@ -48,15 +52,38 @@ public Message readMessage() throws IOException { try { short id = inputStream.readShort(); - LinkedList classes = MessageRegistry.getArgumentClasses(id); + LinkedList classes =connection.getMessageRegistry().getArgumentClasses(id); Class[] classArray = new Class[classes.size()]; classes.toArray(classArray); Object[] args = readMessageArgs(classes); - return new Message(id, args); - } catch (EOFException | SocketException e) + return new Message(connection, id, args); + } catch (Exception e) { return null; - // This means the stream has closed. + // This means the stream has closed, or the an invalid message was found. + } + } + + /** + * Close the input stream. + * @throws java.io.IOException If there is an IO error. + */ + public void close() throws IOException { inputStream.close(); } + + /** + * Set the AES key used to decrypt messages. + * @param key The AES key used to decrypt messages. + */ + public void setAESKey(byte[] key) { + try + { + aesKey = new SecretKeySpec(key, "AES"); + aesDecryptCipher = Cipher.getInstance("AES"); + aesDecryptCipher.init(Cipher.DECRYPT_MODE, aesKey); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) + { + LOGGER.log(Level.SEVERE, "Unable to create cipher, messages will not be decrypted.", e); + aesKey = null; } } @@ -176,27 +203,4 @@ private String readString(DataInputStream inputStream) throws IOException { return received; } - /** - * Close the input stream. - * @throws java.io.IOException If there is an IO error. - */ - public void close() throws IOException { inputStream.close(); } - - /** - * Set the AES key used to decrypt messages. - * @param key The AES key used to decrypt messages. - */ - public void setAESKey(byte[] key) { - try - { - aesKey = new SecretKeySpec(key, "AES"); - aesDecryptCipher = Cipher.getInstance("AES"); - aesDecryptCipher.init(Cipher.DECRYPT_MODE, aesKey); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) - { - LOGGER.log(Level.SEVERE, "Unable to create cipher, messages will not be decrypted.", e); - aesKey = null; - } - } - } diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageOutputStream.java b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageOutputStream.java similarity index 93% rename from jenjin-io/src/main/java/com/jenjinstudios/io/MessageOutputStream.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/MessageOutputStream.java index 81fa1952..bbe75317 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageOutputStream.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageOutputStream.java @@ -1,5 +1,7 @@ package com.jenjinstudios.io; +import com.jenjinstudios.net.Connection; + import javax.crypto.*; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; @@ -21,6 +23,8 @@ public class MessageOutputStream private static final Logger LOGGER = Logger.getLogger(MessageOutputStream.class.getName()); /** The output stream used by this message stream. */ private final DataOutputStream outputStream; + /** The connection using this stream. */ + private final Connection connection; /** The AES key used to encrypt messages from this client handler. */ private SecretKey aesKey; /** The AES cipher. */ @@ -29,10 +33,12 @@ public class MessageOutputStream /** * Creates a new message output stream to write data to the specified underlying output stream. The counter {@code * written} is set to zero. + * @param connection The connection using this stream. * @param out the underlying output stream, to be saved for later use. * @see java.io.FilterOutputStream#out */ - public MessageOutputStream(OutputStream out) { + public MessageOutputStream(Connection connection, OutputStream out) { + this.connection = connection; outputStream = new DataOutputStream(out); } @@ -43,7 +49,7 @@ public MessageOutputStream(OutputStream out) { */ public void writeMessage(Message message) throws IOException { Object[] args = message.getArgs(); - MessageType messageType = MessageRegistry.getMessageType(message.getID()); + MessageType messageType = connection.getMessageRegistry().getMessageType(message.getID()); ArgumentType[] argumentTypes = messageType.argumentTypes; int id = message.getID(); outputStream.writeShort(id); diff --git a/jenjin-core/src/main/java/com/jenjinstudios/io/MessageRegistry.java b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageRegistry.java new file mode 100644 index 00000000..2a244874 --- /dev/null +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageRegistry.java @@ -0,0 +1,255 @@ +package com.jenjinstudios.io; + +import com.jenjinstudios.util.FileUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Handles the registration of message classes and the information on how to reconstruct them from raw data. + * @author Caleb Brinkman + */ +public class MessageRegistry +{ + /** The logger for this class. */ + private static final Logger LOGGER = Logger.getLogger(MessageRegistry.class.getName()); + /** The file name of message registry classed. */ + private static final String messageFileName = "Messages.xml"; + /** A map that stores messages types sorted by ID. */ + private final TreeMap messageTypesByID = new TreeMap<>(); + /** A map that stores message types sorted by name. */ + private final TreeMap messageTypesByName = new TreeMap<>(); + /** Whether this registry is for a server or not. */ + private final boolean isServer; + /** Flags whether messages have been registered. */ + private boolean messagesRegistered; + + /** + * Construct a new MessageRegistry. + * @param isServer Whether or not this registry is for a server. + */ + public MessageRegistry(boolean isServer) { + this.isServer = isServer; + registerXmlMessages(); + } + + /** + * Find the Messages.xml ZipEntry objects in the classpath. + * @return The list of found entries. + */ + private static LinkedList findJarMessageEntries() { + LinkedList jarMessageEntries = new LinkedList<>(); + String classPath = System.getProperty("java.class.path"); + String[] pathElements = classPath.split(System.getProperty("path.separator")); + + for (String fileName : pathElements) + { + File file = new File(fileName); + if (file.isDirectory() || !file.exists()) { continue; } + try + { + FileInputStream inputStream = new FileInputStream(file); + ZipInputStream zip = new ZipInputStream(inputStream); + ZipEntry ze; + while ((ze = zip.getNextEntry()) != null) + { + String entryName = ze.getName(); + if (entryName.endsWith("Messages.xml")) { jarMessageEntries.add(entryName); } + } + } catch (IOException ex) + { + LOGGER.log(Level.WARNING, "Unable to read JAR entry " + fileName, ex); + } + + } + return jarMessageEntries; + } + + /** + * Look for files that match the message registry format. + * @return An ArrayList of message registry files. + */ + private static ArrayList findMessageFiles() { + String rootDir = Paths.get("").toAbsolutePath().toString() + File.separator; + File rootFile = new File(rootDir); + return FileUtil.findFilesWithName(rootFile, messageFileName); + } + + /** + * Get the message type with the given name. + * @param name The name of the message type. + * @return The MessageType with the given name. + */ + public MessageType getMessageType(String name) { + if (!messagesRegistered) + { + LOGGER.log(Level.SEVERE, "Messages not registered! Please remember to call MessageRegistry.registerXmlMessages()"); + } + + return messageTypesByName.get(name); + } + + /** + * Get the MessageType with the given ID. + * @param id The id. + * @return The MessageType with the given ID. + */ + public MessageType getMessageType(short id) { + if (!messagesRegistered) + { + LOGGER.log(Level.SEVERE, "Messages not registered! Please remember to call MessageRegistry.registerXmlMessages()"); + } + + return messageTypesByID.get(id); + } + + /** + * Get the class names of argumentTypes for the class with the given registration ID. + * @param id The ID to lookup. + * @return A LinkedList of class names. + */ + public LinkedList getArgumentClasses(short id) { + if (!messagesRegistered) + { + LOGGER.log(Level.SEVERE, "Messages not registered! Please remember to call MessageRegistry.registerXmlMessages()"); + } + + LinkedList temp = new LinkedList<>(); + + MessageType type = messageTypesByID.get(id); + if (type == null) + { + String message = "Message " + id + " not registered."; + LOGGER.log(Level.SEVERE, message); + throw new RuntimeException(message); + } else if (type.argumentTypes == null) + { + String message = "Message " + id + " contains null argument types."; + LOGGER.log(Level.SEVERE, message); + throw new RuntimeException(message); + } else + { + for (int i = 0; i < type.argumentTypes.length; i++) + temp.add(type.argumentTypes[i].type); + } + + return temp; + } + + /** + * Disable the ExecutableMessage invoked by the message with the given name. + * @param messageName The name of the message. + */ + public void disableExecutableMessage(String messageName) { + LOGGER.log(Level.FINE, "Disabling message: {0}", messageName); + MessageType type = messageTypesByName.get(messageName); + short id = type.id; + ArgumentType[] argumentTypes = type.argumentTypes; + Class clientExecutableMessageClass = type.clientExecutableMessageClass; + Class serverExecutableMessageClass = type.serverExecutableMessageClass; + MessageType newMessageType; + if (isServer) + { + newMessageType = new MessageType(id, messageName, argumentTypes, clientExecutableMessageClass, null); + } else + { + newMessageType = new MessageType(id, messageName, argumentTypes, null, serverExecutableMessageClass); + } + messageTypesByName.put(messageName, newMessageType); + messageTypesByID.put(id, newMessageType); + + } + + /** Register all messages found in registry files. Also checks the JAR file. */ + private void registerXmlMessages() { + LinkedList streamsToRead = new LinkedList<>(); + addJarMessageEntries(streamsToRead); + addMessageFiles(streamsToRead); + readXmlStreams(streamsToRead); + messagesRegistered = true; + } + + /** + * Parse the XML streams and register the discovered MessageTypes. + * @param streamsToRead The streams containing the XML data to be parsed. + */ + private void readXmlStreams(LinkedList streamsToRead) { + LinkedList disabled = new LinkedList<>(); + for (InputStream inputStream : streamsToRead) + { + try + { + MessageXmlReader reader = new MessageXmlReader(inputStream); + addAllMessages(reader.readMessageTypes(isServer)); + disabled.addAll(reader.readDisabledMessages()); + } catch (Exception ex) + { + LOGGER.log(Level.INFO, "Unable to parse XML file", ex); + } + } + for(String disabledMessageName : disabled) + { + disableExecutableMessage(disabledMessageName); + } + } + + /** + * Add the Messages.xml entries in the working directory and add their InputStream to the given list. + * @param streamsToRead The list to which to add the input streams. + */ + private void addMessageFiles(LinkedList streamsToRead) { + ArrayList messageFiles = findMessageFiles(); + for (File f : messageFiles) + { + LOGGER.log(Level.INFO, "Registering XML file {0}", f); + try + { + streamsToRead.add(new FileInputStream(f)); + } catch (IOException ex) + { + LOGGER.log(Level.WARNING, "Unable to create input stream for " + f, ex); + } + } + } + + /** + * Add the Messages.xml entries in the classpath and add their InputStream to the given list. + * @param streamsToRead The list to which to add the input streams. + */ + private void addJarMessageEntries(LinkedList streamsToRead) { + LinkedList jarMessageEntries = findJarMessageEntries(); + for (String entry : jarMessageEntries) + { + LOGGER.log(Level.INFO, "Registering XML entry {0}", entry); + streamsToRead.add(getClass().getClassLoader().getResourceAsStream(entry)); + } + } + + /** + * Register all message types within the given list. + * @param messageTypes The list of message types to add. + */ + private void addAllMessages(List messageTypes) { + for (MessageType messageType : messageTypes) + { + if (messageType != null && !messageTypesByID.containsKey(messageType.id) && !messageTypesByName.containsKey(messageType.name)) + { + // Add the message type to the two trees. + messageTypesByID.put(messageType.id, messageType); + messageTypesByName.put(messageType.name, messageType); + } + } + } + +} diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageType.java b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageType.java similarity index 92% rename from jenjin-io/src/main/java/com/jenjinstudios/io/MessageType.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/MessageType.java index 5d1e4676..9956a3c6 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageType.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageType.java @@ -1,5 +1,6 @@ package com.jenjinstudios.io; +import java.util.Arrays; import java.util.TreeMap; /** @@ -48,4 +49,7 @@ public MessageType(short id, String name, ArgumentType[] argumentTypes, * @return The ArgumentType with the given name. */ public ArgumentType getArgumentType(String name) { return argumentTypeTreeMap.get(name); } + + @Override + public String toString() { return name + " " + id + ": " + Arrays.toString(argumentTypes) + " " + serverExecutableMessageClass; } } diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageTypeFactory.java b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageTypeParser.java similarity index 96% rename from jenjin-io/src/main/java/com/jenjinstudios/io/MessageTypeFactory.java rename to jenjin-core/src/main/java/com/jenjinstudios/io/MessageTypeParser.java index cdef4104..687581ba 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageTypeFactory.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageTypeParser.java @@ -11,10 +11,10 @@ * Generates MessageTypes based on XML elements. * @author Caleb Brinkman */ -public class MessageTypeFactory +public class MessageTypeParser { /** The Logger for this class. */ - private static final Logger LOGGER = Logger.getLogger(MessageTypeFactory.class.getName()); + private static final Logger LOGGER = Logger.getLogger(MessageTypeParser.class.getName()); /** * Get a message type by parsing the XML element specified. Returns null if the element could not be properly parsed. @@ -64,7 +64,7 @@ private static Class getClientExecutableMessageClas String languageAttribute = currentExecutableElement.getAttribute("language"); String sideAttribute = currentExecutableElement.getAttribute("side"); // If it's in java, set the executable message class name. - if (languageAttribute.equalsIgnoreCase("java") && sideAttribute.equalsIgnoreCase("client")) + if ("java".equalsIgnoreCase(languageAttribute) && "client".equalsIgnoreCase(sideAttribute)) executableMessageClassName = currentExecutableElement.getTextContent(); } if (executableMessageClassName != null) @@ -99,7 +99,7 @@ private static Class getServerExecutableMessageClas String languageAttribute = currentExecutableElement.getAttribute("language"); String sideAttribute = currentExecutableElement.getAttribute("side"); // If it's in java, set the executable message class name. - if (languageAttribute.equalsIgnoreCase("java") && sideAttribute.equalsIgnoreCase("server")) + if ("java".equalsIgnoreCase(languageAttribute) && "server".equalsIgnoreCase(sideAttribute)) executableMessageClassName = currentExecutableElement.getTextContent(); } if (executableMessageClassName != null) diff --git a/jenjin-core/src/main/java/com/jenjinstudios/io/MessageXmlReader.java b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageXmlReader.java new file mode 100644 index 00000000..29f44a2b --- /dev/null +++ b/jenjin-core/src/main/java/com/jenjinstudios/io/MessageXmlReader.java @@ -0,0 +1,76 @@ +package com.jenjinstudios.io; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * Used to parse Messages.xml files. + * @author Caleb Brinkman + */ +public class MessageXmlReader +{ + /** The XML document storing the NPC data. */ + private final Document messageDoc; + + /** + * Construct a new MessageXmlReader. + * @param inputStream The stream containing the xml. + * @throws java.io.IOException If there is an error reading the InputStream. + * @throws javax.xml.parsers.ParserConfigurationException If there is an error configuring the XML parser. + * @throws org.xml.sax.SAXException If there is an error parsing the XML. + */ + public MessageXmlReader(InputStream inputStream) throws ParserConfigurationException, IOException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + messageDoc = builder.parse(inputStream); + messageDoc.getDocumentElement().normalize(); + } + + /** + * Read the generated XML document and return a list of message types. + * @param isServer Whether the parser should look for server-side executable messages. + * @return The list of parsed message types. + */ + public List readMessageTypes(boolean isServer) + { + LinkedList messageTypes = new LinkedList<>(); + NodeList messageElements = messageDoc.getElementsByTagName("message"); + + for (int i = 0; i < messageElements.getLength(); i++) + { + Element currentMessageElement = (Element) messageElements.item(i); + MessageType messageType = MessageTypeParser.parseMessageElement(currentMessageElement, isServer); + messageTypes.add(messageType); + } + + return messageTypes; + } + + /** + * Read all the message types that should be disabled. + * @return The disabled message types. + */ + public Collection readDisabledMessages() { + LinkedList messageNames = new LinkedList<>(); + NodeList disabledElements = messageDoc.getElementsByTagName("disabled_message"); + + for (int i = 0; i < disabledElements.getLength(); i++) + { + Element currentMessageElement = (Element) disabledElements.item(i); + messageNames.add(currentMessageElement.getAttribute("name")); + } + + return messageNames; + } +} diff --git a/jenjin-io/src/main/java/com/jenjinstudios/net/Connection.java b/jenjin-core/src/main/java/com/jenjinstudios/net/Connection.java similarity index 84% rename from jenjin-io/src/main/java/com/jenjinstudios/net/Connection.java rename to jenjin-core/src/main/java/com/jenjinstudios/net/Connection.java index b5e212a6..fe040aa0 100644 --- a/jenjin-io/src/main/java/com/jenjinstudios/net/Connection.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/net/Connection.java @@ -1,9 +1,6 @@ package com.jenjinstudios.net; -import com.jenjinstudios.io.ExecutableMessage; -import com.jenjinstudios.io.Message; -import com.jenjinstudios.io.MessageInputStream; -import com.jenjinstudios.io.MessageOutputStream; +import com.jenjinstudios.io.*; import java.io.IOException; import java.net.Socket; @@ -38,8 +35,10 @@ public abstract class Connection extends Thread private volatile boolean connected; /** The AES key of this client. */ private byte[] aesKey; + /** The message registry for this class. */ + private MessageRegistry messageRegistry; - /** Cosntruct a new Commuicator. */ + /** Construct a new Communicator. */ protected Connection() { outgoingMessages = new LinkedList<>(); pingTimes = new ArrayList<>(); @@ -54,13 +53,13 @@ protected Connection() { */ public void setSocket(Socket socket) throws IOException { this.socket = socket; - setOutputStream(new MessageOutputStream(socket.getOutputStream())); - setInputStream(new MessageInputStream(socket.getInputStream())); + setOutputStream(new MessageOutputStream(this, socket.getOutputStream())); + setInputStream(new MessageInputStream(this, socket.getInputStream())); } /** Send a ping request. */ public void sendPing() { - Message pingRequest = new Message("PingRequest"); + Message pingRequest = new Message(this, "PingRequest"); pingRequest.setArgument("requestTimeNanos", System.nanoTime()); queueMessage(pingRequest); } @@ -115,6 +114,18 @@ protected void setConnected(boolean connected) { this.connected = connected; } + /** + * Get the MessageRegistry for this Connection. + * @return The MessageRegistry for this Connection. + */ + public MessageRegistry getMessageRegistry() { return messageRegistry; } + + /** + * Set the MessageRegistry for this Connection. + * @param messageRegistry The MessageRegistry for this Connection. + */ + protected void setMessageRegistry(MessageRegistry messageRegistry) { this.messageRegistry = messageRegistry; } + /** Send all messages in the outgoing queue. This method should only be called from the client update thread. */ public void sendAllMessages() { synchronized (outgoingMessages) @@ -133,6 +144,7 @@ public void sendAllMessages() { public void writeMessage(Message o) { try { + LOGGER.log(Level.FINEST, "Connection {0} writing message {1}", new Object[]{getName(), o}); getOutputStream().writeMessage(o); } catch (IOException e) { @@ -202,7 +214,10 @@ public void run() { { Message currentMessage; while ((currentMessage = getInputStream().readMessage()) != null) + { + LOGGER.log(Level.FINEST, "Connection {0} reading message {1}", new Object[]{getName(), currentMessage}); processMessage(currentMessage); + } } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Error retrieving message from server.", ex); @@ -234,6 +249,12 @@ public LinkedList getSyncedTasks() { return temp; } + /** Run the list of synchronized tasks. */ + void runSyncedTasks() { + for (Runnable r : getSyncedTasks()) + r.run(); + } + /** Close the link with the server. */ protected void closeLink() { try @@ -243,7 +264,7 @@ protected void closeLink() { socket.close(); } catch (IOException ignored) { - // Link closing, possible _because_ of an IOExeption; will be shutting down. + // Link closing, possible _because_ of an IOException; will be shutting down. } finally { connected = false; @@ -273,7 +294,7 @@ protected void processMessage(Message message) { } } else { - Message invalid = new Message("InvalidMessage"); + Message invalid = new Message(this, "InvalidMessage"); invalid.setArgument("messageName", message.name); invalid.setArgument("messageID", message.getID()); queueMessage(invalid); diff --git a/jenjin-io/src/main/java/com/jenjinstudios/util/FileUtil.java b/jenjin-core/src/main/java/com/jenjinstudios/util/FileUtil.java similarity index 55% rename from jenjin-io/src/main/java/com/jenjinstudios/util/FileUtil.java rename to jenjin-core/src/main/java/com/jenjinstudios/util/FileUtil.java index 2c40f8cd..282cc106 100755 --- a/jenjin-io/src/main/java/com/jenjinstudios/util/FileUtil.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/util/FileUtil.java @@ -29,4 +29,37 @@ public static ArrayList findFilesWithName(File dir, String fileName) { return files; } + + /** + * Delete the specified file, recursively if the file is a directory. + * @param file The file or directory to be deleted. + * @return Whether the file or directory was successfully deleted. + */ + public static boolean deleteRecursively(File file) { + boolean deleted = false; + if (file.isDirectory()) + { + if (file.list().length == 0) + { + deleted = (file.delete()); + } else + { + String files[] = file.list(); + for (String temp : files) + { + File fileDelete = new File(file, temp); + deleted = deleteRecursively(fileDelete); + } + if (file.list().length == 0) + { + deleted = file.delete(); + } + } + + } else + { + deleted = file.delete(); + } + return deleted; + } } \ No newline at end of file diff --git a/jenjin-io/src/main/java/com/jenjinstudios/util/Hash.java b/jenjin-core/src/main/java/com/jenjinstudios/util/Hash.java similarity index 93% rename from jenjin-io/src/main/java/com/jenjinstudios/util/Hash.java rename to jenjin-core/src/main/java/com/jenjinstudios/util/Hash.java index ac39d600..8d91d72c 100755 --- a/jenjin-io/src/main/java/com/jenjinstudios/util/Hash.java +++ b/jenjin-core/src/main/java/com/jenjinstudios/util/Hash.java @@ -19,9 +19,9 @@ private static String getHashedString(String input) { try { //Convert the pass to an md5 hash string - byte[] pwbytes = input.getBytes("UTF-8"); + byte[] passBytes = input.getBytes("UTF-8"); MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] encryption = md.digest(pwbytes); + byte[] encryption = md.digest(passBytes); StringBuilder hexString = new StringBuilder(); for (byte anEncryption : encryption) { // Convert back to a string, making sure to include leading zeros. diff --git a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageRegistry.java b/jenjin-io/src/main/java/com/jenjinstudios/io/MessageRegistry.java deleted file mode 100644 index 3e94b5ab..00000000 --- a/jenjin-io/src/main/java/com/jenjinstudios/io/MessageRegistry.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.jenjinstudios.io; - -import com.jenjinstudios.util.FileUtil; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.TreeMap; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * Handles the registration of message classes and the information on how to reconstruct them from raw data. - * - * @author Caleb Brinkman - */ -public class MessageRegistry -{ - /** The logger for this class. */ - private static final Logger LOGGER = Logger.getLogger(MessageRegistry.class.getName()); - /** The file name of message registry classed. */ - private static final String messageFileName = "Messages.xml"; - /** A map that stores messages types sorted by ID. */ - private static final TreeMap messageTypesByID = new TreeMap<>(); - /** A map that stores message types sorted by name. */ - private static final TreeMap messageTypesByName = new TreeMap<>(); - /** Flags whether messages have been registered. */ - private static boolean messagesRegistered; - - /** - * Register all messages found in registry files. Also checks the JAR file. - * - * @param isServer Whether the program registering messages is a server or client-side program. - * @throws java.io.IOException If there is an IO exception when reading XML files. - * @throws javax.xml.parsers.ParserConfigurationException - * If there is an error parsing XML files. - * @throws org.xml.sax.SAXException If there is an error parsing XML files. - */ - public static void registerXmlMessages(boolean isServer) throws ParserConfigurationException, SAXException, IOException - { - try - { - String classPath = System.getProperty("java.class.path"); - String[] pathElements = classPath.split(System.getProperty("path.separator")); - - for (String fileName : pathElements) - { - File file = new File(fileName); - if (file.isDirectory() || !file.exists()) - { - continue; - } - FileInputStream inputStream = new FileInputStream(file); - ZipInputStream zip = new ZipInputStream(inputStream); - ZipEntry ze; - while ((ze = zip.getNextEntry()) != null) - { - String entryName = ze.getName(); - if (entryName.endsWith("Messages.xml")) - { - System.out.println("Registering messages in: " + entryName); - parseXmlStream(MessageRegistry.class.getClassLoader().getResourceAsStream(entryName), isServer); - } - } - } - - // Search the directories - ArrayList messageFiles = findMessageFiles(); - for (File f : messageFiles) parseXmlFile(f, isServer); - messagesRegistered = true; - - } catch (IOException | SAXException | ParserConfigurationException e) - { - LOGGER.log(Level.INFO, "Unable to parse XML files.", e); - throw e; - } - } - - /** - * Look for files that match the message registry format. - * - * @return An ArrayList of message registry files. - */ - private static ArrayList findMessageFiles() - { - String rootDir = Paths.get("").toAbsolutePath().toString() + File.separator; - File rootFile = new File(rootDir); - return FileUtil.findFilesWithName(rootFile, messageFileName); - } - - /** - * Parse a stream for XML messages. - * - * @param stream The stream to parse. - * @param isServer Whether the program registering message is server or client side. - * @throws java.io.IOException If this exception occurs. - * @throws javax.xml.parsers.ParserConfigurationException - * If this exception occurs. - * @throws org.xml.sax.SAXException If this exception occurs. - */ - private static void parseXmlStream(InputStream stream, boolean isServer) throws IOException, SAXException, ParserConfigurationException - { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(stream); - - doc.getDocumentElement().normalize(); - - NodeList messageElements = doc.getElementsByTagName("message"); - - for (int i = 0; i < messageElements.getLength(); i++) - { - Element currentMessageElement = (Element) messageElements.item(i); - MessageType messageType = MessageTypeFactory.parseMessageElement(currentMessageElement, isServer); - if (messageType != null) - { - // Add the message type to the two trees. - messageTypesByID.put(messageType.id, messageType); - messageTypesByName.put(messageType.name, messageType); - } - } - } - - /** - * Parse the given XML file and register message therein. - * - * @param xmlFile The XML file to be parsed. - * @param isServer Whether the program registering message is server or client side. - * @throws java.io.IOException If this exception occurs. - * @throws javax.xml.parsers.ParserConfigurationException - * If this exception occurs. - * @throws org.xml.sax.SAXException If this exception occurs. - */ - private static void parseXmlFile(File xmlFile, boolean isServer) throws IOException, SAXException, ParserConfigurationException - { - InputStream xmlStream = new FileInputStream(xmlFile); - parseXmlStream(xmlStream, isServer); - } - - /** - * Get the message type with the given name. - * - * @param name The name of the message type. - * @return The MessageType with the given name. - */ - public static MessageType getMessageType(String name) - { - if (!messagesRegistered) - { - LOGGER.log(Level.SEVERE, "Messages not registered! Please remeber to call MessageRegistry.registerXmlMessages()"); - } - - return messageTypesByName.get(name); - } - - /** - * Get the MessageType with the given ID. - * - * @param id The id. - * @return The MessageType with the given ID. - */ - public static MessageType getMessageType(short id) - { - if (!messagesRegistered) - { - LOGGER.log(Level.SEVERE, "Messages not registered! Please remeber to call MessageRegistry.registerXmlMessages()"); - } - - return messageTypesByID.get(id); - } - - /** - * Get the class names of argumentTypes for the class with the given registration ID. - * - * @param id The ID to lookup. - * @return A LinkedList of class names. - */ - public static LinkedList getArgumentClasses(short id) - { - if (!messagesRegistered) - { - LOGGER.log(Level.SEVERE, "Messages not registered! Please remeber to call MessageRegistry.registerXmlMessages()"); - } - - LinkedList temp = new LinkedList<>(); - - synchronized (messageTypesByID) - { - MessageType type = messageTypesByID.get(id); - for (int i = 0; i < type.argumentTypes.length; i++) - temp.add(type.argumentTypes[i].type); - } - return temp; - } - -} diff --git a/jenjin-server-world/build.gradle b/jenjin-server-world/build.gradle deleted file mode 100644 index 1a03bcc0..00000000 --- a/jenjin-server-world/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -description = '' - -dependencies { - compile project(':jenjin-server') - compile project(':jenjin-world-utils') - compile project(':jenjin-client-world') -} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/BlockedLocationTest.java b/jenjin-server-world/src/test/java/test/jenjinstudios/world/BlockedLocationTest.java deleted file mode 100644 index a8fea077..00000000 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/BlockedLocationTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package test.jenjinstudios.world; - -import com.jenjinstudios.io.MessageRegistry; -import com.jenjinstudios.world.*; -import com.jenjinstudios.world.io.WorldFileReader; -import com.jenjinstudios.world.math.Vector2D; -import com.jenjinstudios.world.sql.WorldSQLHandler; -import org.junit.*; - -import java.io.File; - -/** - * Test the file-loading and blocked location functionality. - * @author Caleb Brinkman - */ -public class BlockedLocationTest -{ - /** The world server used to test. */ - private WorldServer worldServer; - /** The server-side actor representing the player. */ - private Actor serverPlayer; - - // Client fields - /** The world client used to test. */ - private WorldClient worldClient; - /** The client-side player used for testing. */ - private ClientPlayer clientPlayer; - - /** - * Construct the test. - * @throws Exception If there's an Exception. - */ - @BeforeClass - public static void construct() throws Exception { MessageRegistry.registerXmlMessages(true); } - - /** - * Set up the test. - * @throws Exception If there's an Exception. - */ - @Before - public void setUp() throws Exception { - initWorldServer(); - initWorldClient(); - } - - /** - * Tear down the test. - * @throws Exception If there's an Exception. - */ - @After - public void tearDown() throws Exception { - serverPlayer.setVector2D(new Vector2D(0, 0)); - worldClient.sendBlockingLogoutRequest(); - worldClient.shutdown(); - - worldServer.shutdown(); - - if(!new File("resources/WorldTestFile.xml").delete()) - { - System.out.println("Unable to delete world file."); - } - } - - /** - * Test attempting to walk into a "blocked" location. - * @throws Exception If there's an Exception. - */ - @Test - public void TestAttemptBlockedLocation() throws Exception { - Vector2D vector1 = new Vector2D(35, 0); - Vector2D attemptedVector2 = new Vector2D(35, 35); - Vector2D actualVector2 = new Vector2D(35, 29.8); - Vector2D vector3 = new Vector2D(35, 25); - Vector2D vector4 = new Vector2D(25, 25); - Vector2D vector5 = new Vector2D(25, 35); - Vector2D attemptedVector6 = new Vector2D(35, 35); - Vector2D actualVector6 = new Vector2D(29.8, 35); - Vector2D attemptedVector7 = new Vector2D(35, 35); - Vector2D actualVector7 = new Vector2D(29.8, 35); - - // Move to (35, 0) - WorldTestUtils.moveClientPlayerTowardVector(vector1, clientPlayer, serverPlayer); - Assert.assertEquals(vector1, clientPlayer.getVector2D()); - - // Attempt to move to (35, 35) - // This attempt should be forced to stop one step away from - WorldTestUtils.moveClientPlayerTowardVector(attemptedVector2, clientPlayer, serverPlayer); - Assert.assertEquals(actualVector2, clientPlayer.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(vector3, clientPlayer, serverPlayer); - Assert.assertEquals(vector3, clientPlayer.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(vector4, clientPlayer, serverPlayer); - Assert.assertEquals(vector4, clientPlayer.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(vector5, clientPlayer, serverPlayer); - Assert.assertEquals(vector5, clientPlayer.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(vector5, clientPlayer, serverPlayer); - Assert.assertEquals(vector5, clientPlayer.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(attemptedVector6, clientPlayer, serverPlayer); - Assert.assertEquals(actualVector6, clientPlayer.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(attemptedVector7, clientPlayer, serverPlayer); - Assert.assertEquals(actualVector7, clientPlayer.getVector2D()); - } - - /** - * Initialize and log the client in. - * @throws Exception If there's an exception. - */ - private void initWorldClient() throws Exception { - worldClient = new WorldClient(new File("resources/WorldTestFile.xml"), "localhost", WorldServer.DEFAULT_PORT, "TestAccount01", "testPassword"); - worldClient.blockingStart(); - worldClient.sendBlockingWorldFileRequest(); - worldClient.sendBlockingLoginRequest(); - - /* The WorldClientHandler used to test. */ - WorldClientHandler worldClientHandler = worldServer.getClientHandlerByUsername(worldClient.getUsername()); - clientPlayer = worldClient.getPlayer(); - serverPlayer = worldClientHandler.getPlayer(); - } - - /** - * Initialize the world and world server. - * @throws Exception If there's an exception. - */ - private void initWorldServer() throws Exception { - /* The world SQL handler used to test. */ - WorldSQLHandler worldSQLHandler = new WorldSQLHandler("localhost", "jenjin_test", "jenjin_user", "jenjin_password"); - worldServer = new WorldServer(new WorldFileReader(getClass().getResourceAsStream("/WorldFile01.xml")), - WorldServer.DEFAULT_UPS, WorldServer.DEFAULT_PORT, WorldClientHandler.class, worldSQLHandler); - worldServer.blockingStart(); - } -} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/LocationTest.java b/jenjin-server-world/src/test/java/test/jenjinstudios/world/LocationTest.java deleted file mode 100644 index 3bee937e..00000000 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/LocationTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package test.jenjinstudios.world; - -import com.jenjinstudios.world.Location; -import org.junit.Assert; -import org.junit.Test; - -/** - * Test the Location class. - * @author Caleb Brinkman - */ -public class LocationTest -{ - /** Test the coordinate values. */ - @Test - public void testCoordinates() { - Location loc = new Location(0, 1); - Assert.assertEquals(0, loc.X_COORDINATE); - Assert.assertEquals(1, loc.Y_COORDINATE); - Assert.assertEquals(10, Location.SIZE); - } -} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldServerTest.java b/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldServerTest.java deleted file mode 100644 index 9d0ae38c..00000000 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldServerTest.java +++ /dev/null @@ -1,196 +0,0 @@ -package test.jenjinstudios.world; - -import com.jenjinstudios.io.MessageRegistry; -import com.jenjinstudios.world.*; -import com.jenjinstudios.world.io.WorldFileReader; -import com.jenjinstudios.world.math.MathUtil; -import com.jenjinstudios.world.math.Vector2D; -import com.jenjinstudios.world.sql.WorldSQLHandler; -import org.junit.*; - -import java.io.File; - -/** - * Test the world server. - * @author Caleb Brinkman - */ -public class WorldServerTest -{ - // Server fields - /** The world server used to test. */ - private WorldServer worldServer; - /** The world used for testing. */ - private World world; - /** The server-side actor representing the player. */ - private Actor serverPlayer; - - // Client fields - /** The world client used to test. */ - private WorldClient worldClient; - /** The client-side player used for testing. */ - private ClientPlayer clientPlayer; - - /** - * Construct the test. - * @throws Exception If there's an Exception. - */ - @BeforeClass - public static void construct() throws Exception { MessageRegistry.registerXmlMessages(true); } - - /** - * Set up the client and server. - * @throws Exception If there's an exception. - */ - @Before - public void setUp() throws Exception { - initWorldServer(); - initWorldClient(); - } - - /** - * Tear down the client and server. - * @throws Exception If there's an exception. - */ - @After - public void tearDown() throws Exception { - serverPlayer.setVector2D(new Vector2D(0, 0)); - worldClient.sendBlockingLogoutRequest(); - worldClient.shutdown(); - - worldServer.shutdown(); - - if(!new File("resources/WorldTestFile.xml").delete()) - { - System.out.println("Unable to delete world file."); - } - } - - /** - * Test the actor visibility after player and actor movement. - * @throws Exception If there's an exception. - */ - @Test - public void testActorVisibility() throws Exception { - Vector2D serverActorStartPosition = new Vector2D(0, Location.SIZE * (SightedObject.VIEW_RADIUS + 2)); - Vector2D serverActorTargetPosition = new Vector2D(0, Location.SIZE * SightedObject.VIEW_RADIUS - 1); - Actor serverActor = new Actor("TestActor"); - serverActor.setVector2D(serverActorStartPosition); - world.addObject(serverActor); - - WorldTestUtils.moveServerActorToVector(serverActor, serverActorTargetPosition); - - WorldObject clientActor = worldClient.getPlayer().getVisibleObjects().get(serverActor.getId()); - Assert.assertEquals(1, worldClient.getPlayer().getVisibleObjects().size()); - Assert.assertNotNull(clientActor); - Thread.sleep(50); - Assert.assertEquals(serverActor.getVector2D(), clientActor.getVector2D()); - - WorldTestUtils.moveServerActorToVector(serverActor, serverActorStartPosition); - Assert.assertEquals(0, worldClient.getPlayer().getVisibleObjects().size()); - - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(0, Location.SIZE + 1), clientPlayer, serverPlayer); - Assert.assertEquals(1, worldClient.getPlayer().getVisibleObjects().size()); - clientActor = worldClient.getPlayer().getVisibleObjects().get(serverActor.getId()); - Assert.assertEquals(serverActor.getVector2D(), clientActor.getVector2D()); - - WorldTestUtils.moveClientPlayerTowardVector(Vector2D.ORIGIN, clientPlayer, serverPlayer); - Assert.assertEquals(0, worldClient.getPlayer().getVisibleObjects().size()); - } - - /** - * Test the state-forcing funcionalty. - * @throws Exception If there's an exception. - */ - @Test - public void testForcedStateFromEdge() throws Exception { - WorldTestUtils.idleClientPlayer(1, clientPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(-1.0, 0), clientPlayer, serverPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(1, 0), clientPlayer, serverPlayer); - Assert.assertFalse(clientPlayer.isForcedState()); - Assert.assertEquals(serverPlayer.getVector2D(), clientPlayer.getVector2D()); - } - - /** - * Test the state forcing functionality. - * @throws Exception If there's an exception. - */ - @Test - public void testForcedState() throws Exception { - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(0.5, 0.5), clientPlayer, serverPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(-0.5, -0.5), clientPlayer, serverPlayer); - WorldTestUtils.idleClientPlayer(5, clientPlayer); - Assert.assertEquals(clientPlayer.getVector2D(), serverPlayer.getVector2D()); - } - - /** - * Test basic movement. - * @throws Exception If there's an exception. - */ - @Test - public void testMovement() throws Exception { - Vector2D targetVector = new Vector2D(3.956, 3.7468); - WorldTestUtils.moveClientPlayerTowardVector(targetVector, clientPlayer, serverPlayer); - } - - /** - * Test repeatedly forcing client. - * @throws Exception If there's an exception. - */ - @Test - public void testRepeatedForcedState() throws Exception { - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(.5, .5), clientPlayer, serverPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(-1, -1), clientPlayer, serverPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(.5, .5), clientPlayer, serverPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(-1, -1), clientPlayer, serverPlayer); - WorldTestUtils.moveClientPlayerTowardVector(new Vector2D(.5, .5), clientPlayer, serverPlayer); - } - - /** - * Test movement to various random vectors. - * @throws Exception If there's an exception. - */ - @Test - public void testRandomMovement() throws Exception { - WorldTestUtils.idleClientPlayer(1, clientPlayer); - int maxCoord = 5; - for (int i = 0; i < 10; i++) - { - double randomX = MathUtil.round(java.lang.Math.random() * maxCoord, 4); - double randomY = MathUtil.round(java.lang.Math.random() * maxCoord, 4); - Vector2D random = new Vector2D(randomX, randomY); - WorldTestUtils.moveClientPlayerTowardVector(random, clientPlayer, serverPlayer); - double distance = clientPlayer.getVector2D().getDistanceToVector(serverPlayer.getVector2D()); - Assert.assertEquals("Movement number " + i + " to " + random, 0, distance, .001); - } - } - - /** - * Initialize and log the client in. - * @throws Exception If there's an exception. - */ - private void initWorldClient() throws Exception { - worldClient = new WorldClient(new File("resources/WorldTestFile.xml"), "localhost", WorldServer.DEFAULT_PORT, "TestAccount01", "testPassword"); - worldClient.blockingStart(); - worldClient.sendBlockingWorldFileRequest(); - worldClient.sendBlockingLoginRequest(); - - /* The WorldClientHandler used to test. */ - WorldClientHandler worldClientHandler = worldServer.getClientHandlerByUsername(worldClient.getUsername()); - clientPlayer = worldClient.getPlayer(); - serverPlayer = worldClientHandler.getPlayer(); - } - - /** - * Initialize the world and world server. - * @throws Exception If there's an exception. - */ - private void initWorldServer() throws Exception { - /* The world SQL handler used to test. */ - WorldSQLHandler worldSQLHandler = new WorldSQLHandler("localhost", "jenjin_test", "jenjin_user", "jenjin_password"); - worldServer = new WorldServer(new WorldFileReader(getClass().getResourceAsStream("/WorldFile01.xml")), - WorldServer.DEFAULT_UPS, WorldServer.DEFAULT_PORT, WorldClientHandler.class, worldSQLHandler); - world = worldServer.getWorld(); - worldServer.blockingStart(); - } - -} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldTest.java b/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldTest.java deleted file mode 100644 index 7b9fcc4e..00000000 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package test.jenjinstudios.world; - -import com.jenjinstudios.world.math.Vector2D; -import com.jenjinstudios.world.Location; -import com.jenjinstudios.world.World; -import org.junit.Assert; -import org.junit.Test; - -import java.util.ArrayList; - -/** - * Tests the World class. - * @author Caleb Brinkman - */ -public class WorldTest -{ - /** - * Test the getLocationArea() method. - * @throws Exception If there's an exception. - */ - @Test - public void testGetLocationArea() throws Exception { - World testWorld = new World(); - ArrayList testGrid = testWorld.getLocationArea(0, new Vector2D(0, 0), 3); - Assert.assertEquals(9, testGrid.size()); - - testGrid = testWorld.getLocationArea(0, new Vector2D(50, 50), 3); - Assert.assertEquals(25, testGrid.size()); - - testGrid = testWorld.getLocationArea(0, new Vector2D(50, 50), 4); - Assert.assertEquals(49, testGrid.size()); - } -} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldTestUtils.java b/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldTestUtils.java deleted file mode 100644 index 1e3acd65..00000000 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldTestUtils.java +++ /dev/null @@ -1,74 +0,0 @@ -package test.jenjinstudios.world; - -import com.jenjinstudios.world.Actor; -import com.jenjinstudios.world.ClientPlayer; -import com.jenjinstudios.world.math.Vector2D; -import com.jenjinstudios.world.state.MoveState; -import org.junit.Assert; - -/** - * Used to assist in testing World and WorldServer. - * @author Caleb Brinkman - */ -public class WorldTestUtils -{ - /** - * Make the client player stay idle for the given number of steps. - * @param i The number of steps. - * @param clientPlayer The client player. - * @throws InterruptedException If there's an issue waiting for the player to be idle for the given number of steps. - */ - public static void idleClientPlayer(int i, ClientPlayer clientPlayer) throws InterruptedException { - clientPlayer.setNewRelativeAngle(MoveState.IDLE); - while (clientPlayer.getRelativeAngle() != MoveState.IDLE || clientPlayer.getStepsTaken() < i) - { - Thread.sleep(1); - } - } - - /** - * Move the specified actor to within one STEP_LENGTH of the specified vector. - * @param serverActor The actor. - * @param newVector The target vector. - * @throws InterruptedException If there is an error blocking until the target is reached. - */ - public static void moveServerActorToVector(Actor serverActor, Vector2D newVector) throws InterruptedException { - int stepsTaken = serverActor.getStepsTaken(); - double newAngle = serverActor.getVector2D().getAngleToVector(newVector); - MoveState newState = new MoveState(newAngle, stepsTaken, 0); - serverActor.addMoveState(newState); - double distanceToNewVector = serverActor.getVector2D().getDistanceToVector(newVector); - while (distanceToNewVector > Actor.STEP_LENGTH && !serverActor.isForcedState()) - { - Thread.sleep(10); - distanceToNewVector = serverActor.getVector2D().getDistanceToVector(newVector); - } - MoveState idleState = new MoveState(MoveState.IDLE, serverActor.getStepsTaken(), 0); - serverActor.addMoveState(idleState); - Thread.sleep(10); - } - - /** - * Move the client and server player to the given vector, by initiating the move client-side. - * @param newVector The vector to which to move. - * @param clientPlayer The client player. - * @param serverPlayer The server player. - * @throws InterruptedException If there's an exception. - */ - public static void moveClientPlayerTowardVector(Vector2D newVector, ClientPlayer clientPlayer, Actor serverPlayer) throws InterruptedException { - // Make sure not to send multiple states during the same update. - idleClientPlayer(1, clientPlayer); - double newAngle = clientPlayer.getVector2D().getAngleToVector(newVector); - clientPlayer.setNewRelativeAngle(newAngle); - while (clientPlayer.getVector2D().getDistanceToVector(newVector) >= Actor.STEP_LENGTH) - { - if (clientPlayer.isForcedState()) { break; } - Thread.sleep(10); - } - idleClientPlayer(15, clientPlayer); - double distance = clientPlayer.getVector2D().getDistanceToVector(serverPlayer.getVector2D()); - if (distance > .001) { Thread.sleep(250); } - distance = clientPlayer.getVector2D().getDistanceToVector(serverPlayer.getVector2D()); - Assert.assertEquals(distance, 0, .001); - } -} \ No newline at end of file diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/sql/WorldSQLHandlerTest.java b/jenjin-server-world/src/test/java/test/jenjinstudios/world/sql/WorldSQLHandlerTest.java deleted file mode 100644 index 1940c437..00000000 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/sql/WorldSQLHandlerTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package test.jenjinstudios.world.sql; - -import com.jenjinstudios.world.math.Vector2D; -import com.jenjinstudios.world.Actor; -import com.jenjinstudios.world.sql.WorldSQLHandler; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - - -/** - * Test the WorldSQLHandler. - * @author Caleb Brinkman - */ -public class WorldSQLHandlerTest -{ - /** - * Test logging the player into and out of the world, including updating coordinates. - * @throws Exception If there's an exception. - */ - @Test - public void testLoginLogout() throws Exception { - WorldSQLHandler worldSQLHandler = new WorldSQLHandler("localhost", "jenjin_test", "jenjin_user", - "jenjin_password"); - - assertTrue(worldSQLHandler.isConnected()); - - Actor player = worldSQLHandler.logInPlayer("TestAccount01", "testPassword"); - Vector2D origin = player.getVector2D(); - Vector2D secondVector = new Vector2D(50, 50); - - assertEquals(origin, player.getVector2D()); - - player.setVector2D(secondVector); - assertTrue(worldSQLHandler.logOutPlayer(player)); - - player = worldSQLHandler.logInPlayer("TestAccount01", "testPassword"); - assertEquals(secondVector, player.getVector2D()); - - player.setVector2D(origin); - assertTrue(worldSQLHandler.logOutPlayer(player)); - - player = worldSQLHandler.logInPlayer("TestAccount01", "testPassword"); - assertEquals(origin, player.getVector2D()); - - assertTrue(worldSQLHandler.logOutPlayer(player)); - } -} diff --git a/jenjin-world-client/build.gradle b/jenjin-world-client/build.gradle new file mode 100644 index 00000000..40cb5682 --- /dev/null +++ b/jenjin-world-client/build.gradle @@ -0,0 +1,6 @@ +description = '' + +dependencies { + compile project(':jenjin-core-client') + compile project(':jenjin-world-core') +} diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/ClientActor.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/ClientActor.java similarity index 93% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/ClientActor.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/ClientActor.java index 3da20926..eadbe143 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/ClientActor.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/ClientActor.java @@ -11,7 +11,7 @@ * The {@code ClientActor} class is used to represent a server-side {@code Actor} object on the client side. It is an * object capable of movement. *

- * Actors start with a {@code MoveState} with {@code MoveiDirection.IDLE}. Each update, the Actor checks to see if + * Actors start with a {@code MoveState} with {@code MoveState.IDLE}. Each update, the Actor checks to see if * there are any MoveStates in the queue. If there are, it checks the first state in line for the number of steps * needed before the state changes. Once the number of steps has been reached, the state switches to that of the first * position in the queue, and the Actor's step counter is reset. If an Actor "oversteps," which is determined if the @@ -42,7 +42,8 @@ public class ClientActor extends WorldObject * @param name The name. */ public ClientActor(int id, String name) { - super(name, id); + super(name); + setId(id); nextMoveStates = new LinkedList<>(); currentMoveState = new MoveState(MoveState.IDLE, 0, 0); } @@ -52,11 +53,14 @@ public ClientActor(int id, String name) { * @param newState The MoveState to add. */ public void addMoveState(MoveState newState) { - synchronized (nextMoveStates) + if (nextState == null) + nextState = newState; + else { - if (nextState == null) - nextState = newState; - else nextMoveStates.add(newState); + synchronized (nextMoveStates) + { + nextMoveStates.add(newState); + } } } diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/ClientPlayer.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/ClientPlayer.java similarity index 96% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/ClientPlayer.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/ClientPlayer.java index f9fb6225..02c48202 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/ClientPlayer.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/ClientPlayer.java @@ -35,6 +35,8 @@ public class ClientPlayer extends SightedObject private boolean isNewAbsolute; /** Flags whether a new relative angle has been set. */ private boolean isNewRelative; + /** The Location before a step is taken. */ + private Location locationBeforeStep; /** * Construct an Actor with the given name. @@ -105,11 +107,19 @@ private void setRelativeAngle(double relativeAngle) { } @Override - public void update() { + public void setUp() { setAngles(); resetFlags(); - Location locationBeforeStep = getLocation(); + locationBeforeStep = getLocation(); + } + + @Override + public void update() { step(); + } + + @Override + public void reset() { // If we're in a new locations after stepping, update the visible array. if (locationBeforeStep != getLocation() || getVisibleLocations().isEmpty()) resetVisibleLocations(); @@ -187,7 +197,7 @@ private void saveState() { * Reset the player's state. * @param position The new position. * @param relativeAngle The relative angle. - * @param absoluteAngle The absolte angle. + * @param absoluteAngle The absolute angle. */ private void resetState(Vector2D position, double relativeAngle, double absoluteAngle) { setVector2D(position); diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/WorldClient.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/WorldClient.java similarity index 89% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/WorldClient.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/WorldClient.java index 83df6e57..90fc9cb3 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/WorldClient.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/WorldClient.java @@ -35,10 +35,10 @@ public class WorldClient extends AuthClient private World world; /** The actor representing the player controlled by this client. */ private ClientPlayer player; - /** Whether this client has reveived a world file checksum from the server. */ + /** Whether this client has received a world file checksum from the server. */ private boolean hasReceivedWorldFileChecksum; /** The world file checksum received from the server. */ - private byte[] serverWorldfileChecksum; + private byte[] serverWorldFileChecksum; /** The world file. */ private File worldFile; /** The world file reader for this client. */ @@ -79,8 +79,8 @@ public WorldClient(File worldFile, String address, int port, String username, St public boolean sendBlockingLoginRequest() { sendLoginRequest(); long startTime = System.currentTimeMillis(); - long timepast = System.currentTimeMillis() - startTime; - while (isWaitingForLoginResponse() && (timepast < TIMEOUT_MILLIS)) + long timePast = System.currentTimeMillis() - startTime; + while (!isLoggedIn() && isWaitingForLoginResponse() && (timePast < TIMEOUT_MILLIS)) { try { @@ -89,7 +89,7 @@ public boolean sendBlockingLoginRequest() { { LOGGER.log(Level.WARNING, "Interrupted while waiting for login response.", e); } - timepast = System.currentTimeMillis() - startTime; + timePast = System.currentTimeMillis() - startTime; } return isLoggedIn(); } @@ -105,8 +105,6 @@ public boolean sendBlockingLoginRequest() { * @param player The player to be controlled by this client. */ public void setPlayer(ClientPlayer player) { - if (this.player != null) - throw new IllegalStateException("Player already set!"); this.player = player; } @@ -134,10 +132,10 @@ public void setHasReceivedWorldFileChecksum(boolean hasReceivedWorldFileChecksum /** * Set the checksum received from the server. - * @param serverWorldfileChecksum The checksum received from the server. + * @param serverWorldFileChecksum The checksum received from the server. */ - public void setServerWorldfileChecksum(byte[] serverWorldfileChecksum) { - this.serverWorldfileChecksum = serverWorldfileChecksum; + public void setServerWorldFileChecksum(byte[] serverWorldFileChecksum) { + this.serverWorldFileChecksum = serverWorldFileChecksum; } /** @@ -166,7 +164,7 @@ public void setServerWorldFileBytes(byte[] serverWorldFileBytes) { * @throws org.xml.sax.SAXException If there's an error with the XML. */ public void sendBlockingWorldFileRequest() throws InterruptedException, NoSuchAlgorithmException, SAXException, TransformerException, ParserConfigurationException, IOException { - Message worldFileChecksumRequest = new Message("WorldChecksumRequest"); + Message worldFileChecksumRequest = new Message(this, "WorldChecksumRequest"); queueMessage(worldFileChecksumRequest); while (!hasReceivedWorldFileChecksum) @@ -174,9 +172,9 @@ public void sendBlockingWorldFileRequest() throws InterruptedException, NoSuchAl Thread.sleep(10); } - if (worldFileReader == null || !Arrays.equals(serverWorldfileChecksum, worldFileReader.getWorldFileChecksum())) + if (worldFileReader == null || !Arrays.equals(serverWorldFileChecksum, worldFileReader.getWorldFileChecksum())) { - queueMessage(new Message("WorldFileRequest")); + queueMessage(new Message(this, "WorldFileRequest")); while (!hasReceivedWorldFile) { Thread.sleep(10); @@ -197,7 +195,7 @@ public void sendBlockingWorldFileRequest() throws InterruptedException, NoSuchAl /** Send a LoginRequest to the server. */ private void sendLoginRequest() { - Message loginRequest = WorldClientMessageGenerator.generateLoginRequest(getUsername(), password); + Message loginRequest = WorldClientMessageGenerator.generateLoginRequest(this, getUsername(), password); setWaitingForLoginResponse(true); queueMessage(loginRequest); } @@ -207,13 +205,13 @@ private void sendLoginRequest() { * @param moveState The move state used to generate the request. */ protected void sendStateChangeRequest(MoveState moveState) { - Message stateChangeRequest = WorldClientMessageGenerator.generateStateChangeRequest(moveState); + Message stateChangeRequest = WorldClientMessageGenerator.generateStateChangeRequest(this, moveState); queueMessage(stateChangeRequest); } @Override protected void sendLogoutRequest() { - Message logoutRequest = new Message("WorldLogoutRequest"); + Message logoutRequest = new Message(this, "WorldLogoutRequest"); // Send the request, continue when response is received. setWaitingForLogoutResponse(true); diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/WorldClientUpdater.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/WorldClientUpdater.java similarity index 74% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/WorldClientUpdater.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/WorldClientUpdater.java index 979ce920..f2f0a745 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/WorldClientUpdater.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/WorldClientUpdater.java @@ -3,7 +3,6 @@ import com.jenjinstudios.world.state.MoveState; import java.util.LinkedList; -import java.util.TreeMap; /** * Responsible for updating the world. @@ -13,8 +12,6 @@ public class WorldClientUpdater implements Runnable { /** The client being updated by this runnable. */ private final WorldClient worldClient; - /** Actors other than the player. */ - private final TreeMap visibleObjects; /** The player being controlled by the world client. */ private final ClientPlayer player; @@ -25,18 +22,13 @@ public class WorldClientUpdater implements Runnable public WorldClientUpdater(WorldClient wc) { this.worldClient = wc; this.player = worldClient.getPlayer(); - this.visibleObjects = player.getVisibleObjects(); } @Override public void run() { - for (WorldObject currentObject : visibleObjects.values()) - { - currentObject.update(); - } + worldClient.getWorld().update(); if (player != null) { - player.update(); LinkedList newStates = player.getSavedStates(); while (!newStates.isEmpty()) worldClient.sendStateChangeRequest(newStates.remove()); diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableActorStepLengthMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableActorStepLengthMessage.java similarity index 100% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableActorStepLengthMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableActorStepLengthMessage.java diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableActorVisibleMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableActorVisibleMessage.java similarity index 68% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableActorVisibleMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableActorVisibleMessage.java index 84017935..f66e6e11 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableActorVisibleMessage.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableActorVisibleMessage.java @@ -2,21 +2,15 @@ import com.jenjinstudios.io.Message; import com.jenjinstudios.world.ClientActor; -import com.jenjinstudios.world.InvalidLocationException; import com.jenjinstudios.world.WorldClient; import com.jenjinstudios.world.state.MoveState; -import java.util.logging.Level; -import java.util.logging.Logger; - /** * Process an ActorVisibleMessage. * @author Caleb Brinkman */ public class ExecutableActorVisibleMessage extends WorldClientExecutableMessage { - /** The logger for this class. */ - private static final Logger LOGGER = Logger.getLogger(ExecutableActorVisibleMessage.class.getName()); /** The newly visible actor. */ ClientActor newlyVisible; @@ -31,13 +25,7 @@ public ExecutableActorVisibleMessage(WorldClient client, Message message) { @Override public void runSynced() { - try - { - getClient().getWorld().addObject(newlyVisible, newlyVisible.getId()); - } catch (InvalidLocationException e) - { - LOGGER.log(Level.INFO, "Tried to place newly visible actor in invalid location."); - } + getClient().getWorld().addObject(newlyVisible, newlyVisible.getId()); } @Override @@ -45,15 +33,17 @@ public void runASync() { Message message = getMessage(); String name = (String) message.getArgument("name"); int id = (int) message.getArgument("id"); - double xCoord = (double) message.getArgument("xCoordinate"); - double yCoord = (double) message.getArgument("yCoordinate"); + int resourceID = (int) message.getArgument("resourceID"); + double xCoordinate = (double) message.getArgument("xCoordinate"); + double yCoordinate = (double) message.getArgument("yCoordinate"); double direction = (double) message.getArgument("relativeAngle"); double angle = (double) message.getArgument("absoluteAngle"); int stepsFromLast = (int) message.getArgument("stepsTaken"); int stepsUntilChange = (int) message.getArgument("stepsUntilChange"); newlyVisible = new ClientActor(id, name); - newlyVisible.setVector2D(xCoord, yCoord); + newlyVisible.setResourceID(resourceID); + newlyVisible.setVector2D(xCoordinate, yCoordinate); MoveState state = new MoveState(direction, stepsUntilChange, angle); newlyVisible.setCurrentMoveState(state); newlyVisible.setStepsTaken(stepsFromLast); diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableForceStateMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableForceStateMessage.java similarity index 100% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableForceStateMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableForceStateMessage.java diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableObjectInvisibleMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableObjectInvisibleMessage.java similarity index 62% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableObjectInvisibleMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableObjectInvisibleMessage.java index 2e43a5fb..87953b2a 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableObjectInvisibleMessage.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableObjectInvisibleMessage.java @@ -3,12 +3,17 @@ import com.jenjinstudios.io.Message; import com.jenjinstudios.world.WorldClient; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * Handles processing an ActorInvisibleMessage. * @author Caleb Brinkman */ public class ExecutableObjectInvisibleMessage extends WorldClientExecutableMessage { + /** The logger for this class. */ + private static final Logger LOGGER = Logger.getLogger(ExecutableObjectInvisibleMessage.class.getName()); /** The ID of the object to be made invisible. */ private int id; @@ -23,7 +28,9 @@ public ExecutableObjectInvisibleMessage(WorldClient client, Message message) { @Override public void runSynced() { + LOGGER.log(Level.FINEST, "Before processing ObjectInvisibleMessage, world contains: {0}", getClient().getWorld().getObjectCount()); getClient().getWorld().removeObject(id); + LOGGER.log(Level.FINEST, "After processing ObjectInvisibleMessage, world contains: {0}", getClient().getWorld().getObjectCount()); } @Override diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableObjectVisibleMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableObjectVisibleMessage.java similarity index 58% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableObjectVisibleMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableObjectVisibleMessage.java index 89fa483e..74d41f6e 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableObjectVisibleMessage.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableObjectVisibleMessage.java @@ -1,21 +1,15 @@ package com.jenjinstudios.world.message; import com.jenjinstudios.io.Message; -import com.jenjinstudios.world.InvalidLocationException; import com.jenjinstudios.world.WorldClient; import com.jenjinstudios.world.WorldObject; -import java.util.logging.Level; -import java.util.logging.Logger; - /** * Process an ActorVisibleMessage. * @author Caleb Brinkman */ public class ExecutableObjectVisibleMessage extends WorldClientExecutableMessage { - /** The logger for this class. */ - private static final Logger LOGGER = Logger.getLogger(ExecutableObjectVisibleMessage.class.getName()); /** The newly visible actor. */ WorldObject newlyVisible; @@ -30,13 +24,7 @@ public ExecutableObjectVisibleMessage(WorldClient client, Message message) { @Override public void runSynced() { - try - { - getClient().getWorld().addObject(newlyVisible, newlyVisible.getId()); - } catch (InvalidLocationException e) - { - LOGGER.log(Level.INFO, "Tried to place newly visible actor in invalid location."); - } + getClient().getWorld().addObject(newlyVisible, newlyVisible.getId()); } @Override @@ -44,11 +32,13 @@ public void runASync() { Message message = getMessage(); String name = (String) message.getArgument("name"); int id = (int) message.getArgument("id"); - double xCoord = (double) message.getArgument("xCoordinate"); - double yCoord = (double) message.getArgument("yCoordinate"); + int resourceID = (int) message.getArgument("resourceID"); + double xCoordinate = (double) message.getArgument("xCoordinate"); + double yCoordinate = (double) message.getArgument("yCoordinate"); newlyVisible = new WorldObject(name); newlyVisible.setId(id); - newlyVisible.setVector2D(xCoord, yCoord); + newlyVisible.setResourceID(resourceID); + newlyVisible.setVector2D(xCoordinate, yCoordinate); } } diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableStateChangeMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableStateChangeMessage.java similarity index 100% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableStateChangeMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableStateChangeMessage.java diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldChecksumResponse.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldChecksumResponse.java similarity index 91% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldChecksumResponse.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldChecksumResponse.java index 27acda1c..1fae1a55 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldChecksumResponse.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldChecksumResponse.java @@ -24,7 +24,7 @@ public void runSynced() { @Override public void runASync() { - getClient().setServerWorldfileChecksum((byte[]) getMessage().getArgument("checksum")); + getClient().setServerWorldFileChecksum((byte[]) getMessage().getArgument("checksum")); getClient().setHasReceivedWorldFileChecksum(true); } } diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldFileResponse.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldFileResponse.java similarity index 100% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldFileResponse.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldFileResponse.java diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLoginResponse.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLoginResponse.java similarity index 67% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLoginResponse.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLoginResponse.java index 677faee6..9927e26c 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLoginResponse.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLoginResponse.java @@ -2,21 +2,15 @@ import com.jenjinstudios.io.Message; import com.jenjinstudios.world.ClientPlayer; -import com.jenjinstudios.world.InvalidLocationException; import com.jenjinstudios.world.WorldClient; import com.jenjinstudios.world.WorldClientUpdater; -import java.util.logging.Level; -import java.util.logging.Logger; - /** * Handles login responses from the server. * @author Caleb Brinkman */ public class ExecutableWorldLoginResponse extends WorldClientExecutableMessage { - /** The logger for this class. */ - private static final Logger LOGGER = Logger.getLogger(ExecutableWorldFileResponse.class.getName()); /** The player created as indicated by the world login response. */ private ClientPlayer player; @@ -41,13 +35,7 @@ public void runSynced() { client.setLoggedInTime((long) getMessage().getArgument("loginTime")); client.setName(client.getUsername()); client.setPlayer(player); - try - { - client.getWorld().addObject(player, player.getId()); - } catch (InvalidLocationException e) - { - LOGGER.log(Level.INFO, "Tried to place newly visible actor in invalid location."); - } + client.getWorld().addObject(player, player.getId()); client.addRepeatedTask(new WorldClientUpdater(client)); } @@ -55,9 +43,9 @@ public void runSynced() { @Override public void runASync() { int id = (int) getMessage().getArgument("id"); - double xCoord = (double) getMessage().getArgument("xCoordinate"); - double yCoord = (double) getMessage().getArgument("yCoordinate"); + double xCoordinate = (double) getMessage().getArgument("xCoordinate"); + double yCoordinate = (double) getMessage().getArgument("yCoordinate"); player = new ClientPlayer(id, getClient().getUsername()); - player.setVector2D(xCoord, yCoord); + player.setVector2D(xCoordinate, yCoordinate); } } diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLogoutResponse.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLogoutResponse.java similarity index 100% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLogoutResponse.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/ExecutableWorldLogoutResponse.java diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/message/WorldClientExecutableMessage.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/message/WorldClientExecutableMessage.java similarity index 100% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/message/WorldClientExecutableMessage.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/message/WorldClientExecutableMessage.java diff --git a/jenjin-client-world/src/main/java/com/jenjinstudios/world/util/WorldClientMessageGenerator.java b/jenjin-world-client/src/main/java/com/jenjinstudios/world/util/WorldClientMessageGenerator.java similarity index 65% rename from jenjin-client-world/src/main/java/com/jenjinstudios/world/util/WorldClientMessageGenerator.java rename to jenjin-world-client/src/main/java/com/jenjinstudios/world/util/WorldClientMessageGenerator.java index 7203c775..21b759fb 100644 --- a/jenjin-client-world/src/main/java/com/jenjinstudios/world/util/WorldClientMessageGenerator.java +++ b/jenjin-world-client/src/main/java/com/jenjinstudios/world/util/WorldClientMessageGenerator.java @@ -1,6 +1,7 @@ package com.jenjinstudios.world.util; import com.jenjinstudios.io.Message; +import com.jenjinstudios.net.Connection; import com.jenjinstudios.world.state.MoveState; /** @@ -11,11 +12,12 @@ public class WorldClientMessageGenerator { /** * Generate a state change request for the given move state. + * @param connection The connection generating this message. * @param moveState The state used to generate a state change request. * @return The generated message. */ - public static Message generateStateChangeRequest(MoveState moveState) { - Message stateChangeRequest = new Message("StateChangeRequest"); + public static Message generateStateChangeRequest(Connection connection, MoveState moveState) { + Message stateChangeRequest = new Message(connection, "StateChangeRequest"); stateChangeRequest.setArgument("relativeAngle", moveState.relativeAngle); stateChangeRequest.setArgument("absoluteAngle", moveState.absoluteAngle); stateChangeRequest.setArgument("stepsUntilChange", moveState.stepsUntilChange); @@ -24,12 +26,13 @@ public static Message generateStateChangeRequest(MoveState moveState) { /** * Generate a LoginRequest message. + * @param connection The connection generating this message. * @param username The username. * @param password The password. * @return The LoginRequest message. */ - public static Message generateLoginRequest(String username, String password) { - Message loginRequest = new Message("WorldLoginRequest"); + public static Message generateLoginRequest(Connection connection, String username, String password) { + Message loginRequest = new Message(connection, "WorldLoginRequest"); loginRequest.setArgument("username", username); loginRequest.setArgument("password", password); return loginRequest; diff --git a/jenjin-client-world/src/main/resources/com/jenjinstudios/world/Messages.xml b/jenjin-world-client/src/main/resources/com/jenjinstudios/world/Messages.xml similarity index 98% rename from jenjin-client-world/src/main/resources/com/jenjinstudios/world/Messages.xml rename to jenjin-world-client/src/main/resources/com/jenjinstudios/world/Messages.xml index 407f324f..833a3171 100644 --- a/jenjin-client-world/src/main/resources/com/jenjinstudios/world/Messages.xml +++ b/jenjin-world-client/src/main/resources/com/jenjinstudios/world/Messages.xml @@ -13,6 +13,7 @@ com.jenjinstudios.world.message.ExecutableObjectVisibleMessage + @@ -21,6 +22,7 @@ com.jenjinstudios.world.message.ExecutableActorVisibleMessage + diff --git a/jenjin-world-utils/buid.gradle b/jenjin-world-core/buid.gradle similarity index 100% rename from jenjin-world-utils/buid.gradle rename to jenjin-world-core/buid.gradle diff --git a/jenjin-world-core/src/main/java/com/jenjinstudios/world/Location.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/Location.java new file mode 100644 index 00000000..e87cb2ea --- /dev/null +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/Location.java @@ -0,0 +1,256 @@ +package com.jenjinstudios.world; + +import com.jenjinstudios.world.math.Vector2D; + +import java.util.*; + +/** + * Represents a location in the world's location grid. + * @author Caleb Brinkman + */ +public class Location +{ + /** The size, int units, of each location. */ + public static final int SIZE = 10; + /** The x coordinate of the location in it's zone's grid. */ + public final int X_COORDINATE; + /** The y coordinate of the location in it's zone's grid. */ + public final int Y_COORDINATE; + /** The objects residing in this location. */ + private final HashSet objects; + /** The locationProperties of this location. */ + private final LocationProperties locationProperties; + /** The locations visible from this one. */ + private LinkedList locationsVisibleFrom; + /** Flags whether the adjacent locations are set. */ + private boolean hasLocationsSet; + /** The location adjacent to the North. */ + private Location adjNorth; + /** The location adjacent to the South. */ + private Location adjSouth; + /** The location adjacent to the East. */ + private Location adjEast; + /** The location adjacent to the West. */ + private Location adjWest; + /** The location adjacent to the NorthEast. */ + private Location adjNorthEast; + /** The location adjacent to the NorthWest. */ + private Location adjNorthWest; + /** The location adjacent to the SouthEast. */ + private Location adjSouthEast; + /** The location adjacent to the SouthWest. */ + private Location adjSouthWest; + /** The locations adjacent to this one. */ + private final LinkedList adjacentLocations; + /** The locations adjacent to this one through which a path may be plotted. */ + private final LinkedList adjacentWalkableLocations; + /** The locations adjacent diagonally. */ + private final LinkedList diagonals; + /** The center of this Location. */ + private final Vector2D center; + + /** + * Construct a new location at the given position in a zone grid. + * @param x The x coordinate of the zone grid. + * @param y The y coordinate of the zone grid. + */ + public Location(int x, int y) { + this(x, y, new LocationProperties()); + } + + /** + * Construct a location with the given position and locationProperties. + * @param x The x coordinate. + * @param y The y coordinate. + * @param locationProperties1 The locationProperties. + */ + public Location(int x, int y, LocationProperties locationProperties1) { + diagonals = new LinkedList<>(); + adjacentLocations = new LinkedList<>(); + adjacentWalkableLocations = new LinkedList<>(); + X_COORDINATE = x; + Y_COORDINATE = y; + center = new Vector2D(X_COORDINATE * SIZE + SIZE / 2, Y_COORDINATE * SIZE + SIZE / 2); + this.locationProperties = locationProperties1; + objects = new HashSet<>(); + locationsVisibleFrom = new LinkedList<>(); + } + + /** + * Get the locationProperties of this location. + * @return The locationProperties of this location. + */ + public LocationProperties getLocationProperties() { return locationProperties; } + + /** + * Get the objects residing in this location, as an array. + * @return An array containing all objects residing in this location. + */ + public Collection getObjects() { return Collections.unmodifiableCollection(new ArrayList<>(objects)); } + + /** + * Add the object to this location's object map. + * @param object The object to add. + */ + public void addObject(WorldObject object) { objects.add(object); } + + /** + * Remove an object from this location's object map. + * @param object The object to remove. + */ + public void removeObject(WorldObject object) { objects.remove(object); } + + @Override + public String toString() { return "(" + X_COORDINATE + ", " + Y_COORDINATE + ")"; } + + /** + * Get the locations visible from this one. + * @return The locations visible from this one. + */ + public LinkedList getLocationsVisibleFrom() { return locationsVisibleFrom; } + + /** + * Set the locations visible from this location. + * @param visible The locations to be visible from this one. + */ + public void setLocationsVisibleFrom(List visible) { locationsVisibleFrom.addAll(visible); } + + /** + * The location adjacent to the North. + * @return The location adjacent to the north. + */ + public Location getAdjNorth() { return adjNorth; } + + /** + * The location adjacent to the South. + * @return The location adjacent to the South. + */ + public Location getAdjSouth() { return adjSouth; } + + /** + * The location adjacent to the East. + * @return The location adjacent to the East. + */ + public Location getAdjEast() { return adjEast; } + + /** + * The location adjacent to the West. + * @return The data adjacent to the west. + */ + public Location getAdjWest() { return adjWest; } + + /** + * Get a list of all adjacent locations. + * @return The list of adjacent locations. + */ + public List getAdjacentLocations() { return new LinkedList<>(adjacentLocations); } + + /** + * Set the locations adjacent to this one. + * @param zone The zone in which this location (or rather, the "adjacent" locations) lie. + */ + protected void setAdjacentLocations(Zone zone) { + if (hasLocationsSet) + { + throw new IllegalStateException("Cannot set adjacent locations after they have already been set!"); + } + hasLocationsSet = true; + + adjNorth = zone.getLocationOnGrid(X_COORDINATE, Y_COORDINATE + 1); + adjSouth = zone.getLocationOnGrid(X_COORDINATE, Y_COORDINATE - 1); + adjEast = zone.getLocationOnGrid(X_COORDINATE + 1, Y_COORDINATE); + adjWest = zone.getLocationOnGrid(X_COORDINATE - 1, Y_COORDINATE); + adjNorthEast = zone.getLocationOnGrid(X_COORDINATE + 1, Y_COORDINATE + 1); + adjNorthWest = zone.getLocationOnGrid(X_COORDINATE - 1, Y_COORDINATE + 1); + adjSouthEast = zone.getLocationOnGrid(X_COORDINATE + 1, Y_COORDINATE - 1); + adjSouthWest = zone.getLocationOnGrid(X_COORDINATE - 1, Y_COORDINATE - 1); + + if (adjNorth != null) + adjacentLocations.add(adjNorth); + if (adjSouth != null) + adjacentLocations.add(adjSouth); + if (adjEast != null) + adjacentLocations.add(adjEast); + if (adjWest != null) + adjacentLocations.add(adjWest); + if (adjNorthEast != null) + { + adjacentLocations.add(adjNorthEast); + diagonals.add(adjNorthEast); + } + if (adjNorthWest != null) + { + adjacentLocations.add(adjNorthWest); + diagonals.add(adjNorthWest); + } + if (adjSouthEast != null) + { + adjacentLocations.add(adjSouthEast); + diagonals.add(adjSouthEast); + } + if (adjSouthWest != null) + { + adjacentLocations.add(adjSouthWest); + diagonals.add(adjSouthWest); + } + } + + /** + * The location adjacent to the NorthEast. + * @return The location adjacent to the NorthEast. + */ + public Location getAdjNorthEast() { return adjNorthEast; } + + /** + * The location adjacent to the NorthWest. + * @return The location adjacent to the NorthWest. + */ + public Location getAdjNorthWest() { return adjNorthWest; } + + /** + * The location adjacent to the SouthEast. + * @return The location adjacent to the SouthEast. + */ + public Location getAdjSouthEast() { return adjSouthEast; } + + /** + * The location adjacent to the SouthWest. + * @return The location adjacent to the SouthWest. + */ + public Location getAdjSouthWest() { return adjSouthWest; } + + /** + * Get a list of locations adjacent to this one, all of which can be walked to. + * @return A list of adjacent, walkable locations. + */ + public List getAdjacentWalkableLocations() { + return new LinkedList<>(adjacentWalkableLocations); + } + + /** + * Get the Vector2D at the center of this location. + * @return The Vector2D at the center of this location. + */ + public Vector2D getCenter() { + return center; + } + + /** Set the locations adjacent to this one which can be moved to while finding a path. */ + protected void setAdjacentWalkableLocations() { + adjacentWalkableLocations.addAll(adjacentLocations); + for (Location walkable : adjacentLocations) + { + if ("false".equals(walkable.getLocationProperties().getProperty("walkable"))) + { + adjacentWalkableLocations.remove(walkable); + for (Location blocked : walkable.getAdjacentLocations()) + { + if (diagonals.contains(blocked)) + { + adjacentWalkableLocations.remove(blocked); + } + } + } + } + } +} diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/LocationProperties.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/LocationProperties.java similarity index 100% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/LocationProperties.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/LocationProperties.java diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/SightedObject.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/SightedObject.java similarity index 93% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/SightedObject.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/SightedObject.java index 56c1a603..1170f8da 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/SightedObject.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/SightedObject.java @@ -22,11 +22,6 @@ public class SightedObject extends WorldObject /** The list of newly invisible objects. */ private final ArrayList newlyInvisibleObjects; - /** Construct a new SightedObject. */ - public SightedObject() { - this(DEFAULT_NAME); - } - /** * Construct a new SightedObject. * @param name The name of this object. @@ -55,6 +50,35 @@ public void setWorld(World world) { resetVisibleLocations(); } + /** + * The container for visible objects. + * @return An ArrayList containing all objects visible to this actor. + */ + public TreeMap getVisibleObjects() { + synchronized (visibleObjects) + { + return new TreeMap<>(visibleObjects); + } + } + + /** + * Get newly visible objects. + * @return A list of all objects newly visible. + */ + public ArrayList getNewlyVisibleObjects() {return newlyVisibleObjects;} + + /** + * Get newly invisible objects. + * @return A list of all objects newly invisible. + */ + public ArrayList getNewlyInvisibleObjects() {return newlyInvisibleObjects;} + + /** + * Get the currently visible locations. + * @return The array list of currently visible locations. + */ + public ArrayList getVisibleLocations() { return visibleLocations; } + /** Resets the array of currently visible location. */ protected void resetVisibleLocations() { visibleLocations.clear(); @@ -87,33 +111,9 @@ protected void resetVisibleObjects() { newlyVisibleObjects.removeAll(visibleObjects.values()); visibleObjects.clear(); - for(WorldObject object : currentlyVisible) + for (WorldObject object : currentlyVisible) { visibleObjects.put(object.getId(), object); } } - - /** - * The container for visible objects. - * @return An ArrayList containing all objects visible to this actor. - */ - public TreeMap getVisibleObjects() {return visibleObjects;} - - /** - * Get newly visible objects. - * @return A list of all objects newly visible. - */ - public ArrayList getNewlyVisibleObjects() {return newlyVisibleObjects;} - - /** - * Get newly invisible objects. - * @return A list of all objects newly invisible. - */ - public ArrayList getNewlyInvisibleObjects() {return newlyInvisibleObjects;} - - /** - * Get the currently visible locations. - * @return The array list of currently visible locations. - */ - public ArrayList getVisibleLocations() { return visibleLocations; } } diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/World.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/World.java similarity index 65% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/World.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/World.java index 0e0c7e71..7c0466d5 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/World.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/World.java @@ -2,7 +2,7 @@ import com.jenjinstudios.world.math.Vector2D; -import java.util.ArrayList; +import java.util.*; /** * Contains all the Zones, Locations and GameObjects. @@ -10,20 +10,18 @@ */ public class World { - /** The size of the world's location grid. */ - public final int DEFAULT_SIZE = 50; /** The list of in-world Zones. */ private final Zone[] zones; /** The GameObjects contained in the world. */ - private final ArrayList worldObjects; - /** The number of objects currently in the world. */ - private int objectCount; + private final WorldObjectTree worldObjects; /** Construct a new World. */ public World() { zones = new Zone[1]; - zones[0] = new Zone(0, DEFAULT_SIZE, DEFAULT_SIZE); - worldObjects = new ArrayList<>(); + /* The default size of the world's location grid. */ + int DEFAULT_SIZE = 50; + zones[0] = new Zone(0, DEFAULT_SIZE, DEFAULT_SIZE, new Location[]{}); + worldObjects = new WorldObjectTree(); } /** @@ -32,15 +30,14 @@ public World() { */ public World(Zone[] zones) { this.zones = zones; - worldObjects = new ArrayList<>(); + worldObjects = new WorldObjectTree(); } /** * Add an object to the world. * @param object The object to add. - * @throws InvalidLocationException If an object is attempted to be added with an invalid location. */ - public void addObject(WorldObject object) throws InvalidLocationException { + public void addObject(WorldObject object) { this.addObject(object, worldObjects.size()); } @@ -48,30 +45,21 @@ public void addObject(WorldObject object) throws InvalidLocationException { * Add an object with the specified ID. * @param object The object to add. * @param id The id. - * @throws InvalidLocationException If the object is to be added in an invalid location. */ - public void addObject(WorldObject object, int id) throws InvalidLocationException { + public void addObject(WorldObject object, int id) { if (object == null) throw new IllegalArgumentException("addObject(WorldObject obj) argument 0 not allowed to be null!"); - worldObjects.ensureCapacity(id + 1); - - while(worldObjects.size() <= id) - { - worldObjects.add(null); - } - if (worldObjects.get(id) != null) - throw new IllegalArgumentException("addObject(WorldObject obj) argument 1 not allowed to be an occupied id!"); + throw new IllegalArgumentException("addObject(WorldObject obj) argument 1 not allowed to be an occupied id: " + id); object.setWorld(this); object.setVector2D(object.getVector2D()); synchronized (worldObjects) { object.setId(id); - worldObjects.add(id, object); + worldObjects.put(id, object); } - objectCount++; } /** @@ -79,19 +67,19 @@ public void addObject(WorldObject object, int id) throws InvalidLocationExceptio * @param object The object to remove. */ public void removeObject(WorldObject object) { - synchronized (worldObjects) - { - worldObjects.set(object.getId(), null); - object.getLocation().removeObject(object); - } - objectCount--; + removeObject(object.getId()); } /** * Remove the object with the specified id. * @param id The id. */ - public void removeObject(int id) { this.removeObject(worldObjects.get(id)); } + public void removeObject(int id) { + synchronized (worldObjects) + { + worldObjects.remove(id); + } + } /** * Get the location from the zone grid that contains the specified vector2D. @@ -102,7 +90,7 @@ public void removeObject(WorldObject object) { public Location getLocationForCoordinates(int zoneID, Vector2D vector2D) { if (!isValidLocation(zoneID, vector2D)) return null; - return zones[zoneID].getLocation(vector2D); + return zones[zoneID].getLocationForCoordinates(vector2D); } /** @@ -113,16 +101,23 @@ public Location getLocationForCoordinates(int zoneID, Vector2D vector2D) { */ public boolean isValidLocation(int zoneID, Vector2D vector2D) { Zone zone = zones[zoneID]; - return !(zone != null && zone.isValidLocation(vector2D)); + return zone == null || zone.isInvalidLocation(vector2D); } /** Update all objects in the world. */ public void update() { synchronized (worldObjects) { - for (WorldObject o : worldObjects) + Collection values = worldObjects.values(); + for (WorldObject o : values) + if (o != null) + o.setUp(); + for (WorldObject o : values) if (o != null) o.update(); + for (WorldObject o : values) + if (o != null) + o.reset(); } } @@ -143,7 +138,7 @@ public ArrayList getLocationArea(int zoneID, Vector2D center, int radi * Get the number of objects currently in the world. * @return The number of objects currently in the world. */ - public int getObjectCount() { return objectCount; } + public int getObjectCount() { return worldObjects.size(); } /** * Get an object by its id. @@ -153,10 +148,45 @@ public ArrayList getLocationArea(int zoneID, Vector2D center, int radi public WorldObject getObject(int id) { return worldObjects.get(id); } /** - * Get the zone array. - * @return The array of zone objects contained in this world. + * Get a list of all valid Zone IDs in this world. + * @return A List of all IDs which are linked to a zone. */ - public Zone[] getZones() { - return zones; + public List getZoneIDs() + { + LinkedList r = new LinkedList<>(); + synchronized (zones) + { + for(Zone z : zones) + { + r.add(z.id); + } + } + return r; + } + + /** + * Get the zone with the given id. + * @param id The id of the zone to retrieve. + * @return The zone with the given id. + */ + public Zone getZone(int id) { + Zone r = null; + synchronized (zones) + { + for (Zone z : zones) + { + if (z.id == id) + r = z; + } + } + return r; + } + + /** Reset the world to it's original state. */ + public void purgeObjects() { + synchronized (worldObjects) + { + worldObjects.clear(); + } } } diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/WorldObject.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/WorldObject.java similarity index 72% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/WorldObject.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/WorldObject.java index b1ef9479..1adad00b 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/WorldObject.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/WorldObject.java @@ -8,8 +8,6 @@ */ public class WorldObject { - /** The default name of this actor. */ - public static final String DEFAULT_NAME = "Object"; /** The name of this actor. */ private final String name; /** The zoneID in which this actor is located. */ @@ -24,11 +22,9 @@ public class WorldObject private World world; /** The location in which this object is residing. */ private Location location; + /** The resource ID number for this object. */ + private int resourceID; - /** Construct a new WorldObject. */ - public WorldObject() { - this(DEFAULT_NAME); - } /** * Construct a new WorldObject. @@ -39,16 +35,6 @@ public WorldObject(String name) { this.name = name; } - /** - * Create a World Object with the specified id. - * @param name The name of the object. - * @param id The id of the object. - */ - public WorldObject(String name, int id) { - this(name); - setId(id); - } - /** * Get the relativeAngle in which this object is facing, in radians. * @return The relativeAngle in which this object is facing. @@ -73,21 +59,26 @@ public WorldObject(String name, int id) { */ public void setVector2D(Vector2D vector2D) { this.vector2D = new Vector2D(vector2D); - Location oldLocation = location; + if (world != null) { - location = world.getLocationForCoordinates(this.zoneID, this.vector2D); - if (oldLocation != location && location != null) - { - if (oldLocation != null) - { - oldLocation.removeObject(this); - } - location.addObject(this); - } + Location newLocation = world.getLocationForCoordinates(this.zoneID, this.vector2D); + setLocation(newLocation); } } + /** + * Get the resourceID for this object. + * @return The resourceID for this object. + */ + public int getResourceID() { return resourceID; } + + /** + * Set the resourceID for this object. + * @param resourceID The resourceID for this object. + */ + public void setResourceID(int resourceID) { this.resourceID = resourceID; } + /** * Get this object's ID number. * @return This object's ID number. @@ -106,6 +97,23 @@ public void setVector2D(Vector2D vector2D) { */ public Location getLocation() { return location; } + /** + * Set this objects new location. + * @param newLocation The new location. + */ + protected void setLocation(Location newLocation) { + Location oldLocation = location; + location = newLocation; + if (oldLocation != location && oldLocation != null) + { + oldLocation.removeObject(this); + } + if (location != null) + { + location.addObject(this); + } + } + /** * Get the world in which this object is located. * @return the world in which this object is located. @@ -120,10 +128,7 @@ public void setWorld(World world) { if (this.world != null) throw new IllegalArgumentException("The world has already been set for this object."); this.world = world; - location = world.getLocationForCoordinates(this.zoneID, this.vector2D); - - if (location != null) - location.addObject(this); + setLocation(world.getLocationForCoordinates(this.zoneID, this.vector2D)); } /** @@ -132,10 +137,16 @@ public void setWorld(World world) { */ public String getName() { return name; } + /** Set up this WorldObject before updating. */ + public void setUp() { } + /** Update this WorldObject. */ public void update() { } - public String toString() { return name + ": " + id + " @ " + vector2D + " in " + location; } + /** Reset this WorldObject after updating. */ + public void reset() { } + + public String toString() { return name + ": " + id; } /** * Get the id number of the zone in which this player is located. @@ -151,8 +162,10 @@ public void update() { } /** * Set the object's vector based on coordinates. - * @param xCoord The x coordinate. - * @param yCoord The y coordinate. + * @param xCoordinate The x coordinate. + * @param yCoordinate The y coordinate. */ - public void setVector2D(double xCoord, double yCoord) { this.setVector2D(new Vector2D(xCoord, yCoord)); } + public void setVector2D(double xCoordinate, double yCoordinate) { + this.setVector2D(new Vector2D(xCoordinate, yCoordinate)); + } } diff --git a/jenjin-world-core/src/main/java/com/jenjinstudios/world/WorldObjectTree.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/WorldObjectTree.java new file mode 100644 index 00000000..2576a809 --- /dev/null +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/WorldObjectTree.java @@ -0,0 +1,29 @@ +package com.jenjinstudios.world; + +import java.util.TreeMap; + +/** + * Used to store WorldObjects. + * @author Caleb Brinkman + */ +public class WorldObjectTree extends TreeMap +{ + @Override + public WorldObject remove(Object key) { + WorldObject r = super.remove(key); + if (r != null) + { + r.setLocation(null); + } + return r; + } + + @Override + public void clear() { + Object[] set = keySet().toArray(); + for (Object i : set) + { + remove(i); + } + } +} diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/Zone.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/Zone.java similarity index 69% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/Zone.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/Zone.java index f4d6d300..71935c93 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/Zone.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/Zone.java @@ -6,14 +6,18 @@ import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; +import java.util.logging.Level; +import java.util.logging.Logger; /** * The {@code Zone} class represents a grid of {@code Location} objects within the {@code World}. Zones cannot be - * accessed from other Zones. Suuport for this feature is planned in a future release. + * accessed from other Zones. Support for this feature is planned in a future release. * @author Caleb Brinkman */ public class Zone { + /** The Logger for this class. */ + private static final Logger LOGGER = Logger.getLogger(Zone.class.getName()); /** The number assigned to this Zone by the world on initialization. */ public final int id; /** The number of {@code Location} objects in the x-axis. */ @@ -23,16 +27,6 @@ public class Zone /** The grid of {@code Location} objects. */ private final Location[][] locationGrid; - /** - * Construct a new zone with the given ID and size. - * @param id The id number of the zone. - * @param xSize The x length of the zone. - * @param ySize The y length of zone. - */ - public Zone(int id, int xSize, int ySize) { - this(id, xSize, ySize, null); - } - /** * Construct a new zone with the given ID and size. * @param id The id number of the zone. @@ -46,8 +40,21 @@ public Zone(int id, int xSize, int ySize, Location[] specialLocations) { this.ySize = ySize; locationGrid = new Location[xSize][ySize]; - initLocations(); + LOGGER.log(Level.FINEST, "Constructing Locations."); + constructLocations(); + LOGGER.log(Level.FINEST, "Adding Special Locations."); + addSpecialLocations(specialLocations); + LOGGER.log(Level.FINEST, "Calculating Location Visibility."); + setLocationVisibility(); + LOGGER.log(Level.FINEST, "Calculating Location Adjacency."); + setAdjacentLocations(); + } + /** + * Replace empty locations with the specified locations. + * @param specialLocations The locations to be placed in the grid. + */ + private void addSpecialLocations(Location[] specialLocations) { if (specialLocations != null) { for (Location l : specialLocations) @@ -55,30 +62,28 @@ public Zone(int id, int xSize, int ySize, Location[] specialLocations) { locationGrid[l.X_COORDINATE][l.Y_COORDINATE] = l; } } - - setLocationVisibility(); } /** - * Determine if the coordinates of the vector are within this Zones boundries. + * Determine if the coordinates of the vector are within this Zones boundaries. * @param vector2D The coordinates to check. - * @return Whether the coordinates of the vector are within this Zones boundries. + * @return Whether the coordinates of the vector are within this Zones boundaries. */ - public boolean isValidLocation(Vector2D vector2D) { + public boolean isInvalidLocation(Vector2D vector2D) { double x = vector2D.getXCoordinate(); double y = vector2D.getYCoordinate(); - return (x < 0 || y < 0 || x / Location.SIZE >= xSize || y / Location.SIZE >= ySize); + return !(x < 0 || y < 0 || x / Location.SIZE >= xSize || y / Location.SIZE >= ySize); } /** * Get an area of location objects. - * @param centerCoords The center of the area to return. + * @param centerCoordinates The center of the area to return. * @param radius The radius of the area. * @return An ArrayList containing all valid locations in the specified area. */ - public ArrayList getLocationArea(Vector2D centerCoords, int radius) { + public ArrayList getLocationArea(Vector2D centerCoordinates, int radius) { ArrayList areaGrid = new ArrayList<>(); - Location center = getLocation(centerCoords); + Location center = getLocationForCoordinates(centerCoordinates); int xStart = Math.max(center.X_COORDINATE - (radius - 1), 0); int yStart = Math.max(center.Y_COORDINATE - (radius - 1), 0); int xEnd = Math.min(center.X_COORDINATE + (radius - 1), locationGrid.length - 1); @@ -94,11 +99,11 @@ public ArrayList getLocationArea(Vector2D centerCoords, int radius) { /** * Get the location at the specified coordinates. - * @param centerCoords The coodinates. + * @param centerCoordinates The coordinates. * @return The location at the specified coordinates. */ - public Location getLocation(Vector2D centerCoords) { - return getLocation(centerCoords.getXCoordinate(), centerCoords.getYCoordinate()); + public Location getLocationForCoordinates(Vector2D centerCoordinates) { + return getLocationForCoordinates(centerCoordinates.getXCoordinate(), centerCoordinates.getYCoordinate()); } /** @@ -107,7 +112,7 @@ public Location getLocation(Vector2D centerCoords) { * @param y The y coordinate. * @return The location at the specified coordinates. */ - public Location getLocation(double x, double y) { + public Location getLocationForCoordinates(double x, double y) { return locationGrid[(int) x / Location.SIZE][(int) y / Location.SIZE]; } @@ -161,7 +166,7 @@ public LinkedList castVisibilityCircle(Location center, int radius) { double distance = new Vector2D(centerX, centerY).getDistanceToVector(new Vector2D(location.X_COORDINATE, location.Y_COORDINATE)); if (distance <= radius) { - LinkedList visibleRay = castVisibleRay(centerX, centerY, location.X_COORDINATE, location.Y_COORDINATE); + LinkedList visibleRay = castVisibilityRay(centerX, centerY, location.X_COORDINATE, location.Y_COORDINATE); visibleLocations.addAll(visibleRay); } } @@ -181,7 +186,7 @@ public LinkedList castVisibilityCircle(Location center, int radius) { * @return The ray cast from the given starting points to the given end points. */ @SuppressWarnings("SuspiciousNameCombination") - public LinkedList castVisibleRay(int x1, int y1, int x2, int y2) { + public LinkedList castVisibilityRay(int x1, int y1, int x2, int y2) { LinkedList visibleRay = new LinkedList<>(); int i; // loop counter int yStep, xStep; // the step on y and x axis @@ -226,43 +231,17 @@ public LinkedList castVisibleRay(int x1, int y1, int x2, int y2) { // three cases (octant == right->right-top for directions below): if (error + previousError < ddx) // bottom square also { - Location location = getLocationOnGrid(y - yStep, x); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if (!addLocationToVisibilityRay(y - yStep, x, visibleRay)) break; } else if (error + previousError > ddx) // left square also { - Location location = getLocationOnGrid(y, x - xStep); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if (!addLocationToVisibilityRay(y, x - xStep, visibleRay)) break; } else { // corner: bottom and left squares also - Location location = getLocationOnGrid(y - yStep, x); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); - location = getLocationOnGrid(y, x - xStep); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); - visibleRay.add(location); + if (!addLocationToVisibilityRay(y - yStep, x, visibleRay)) break; + if (!addLocationToVisibilityRay(y, x - xStep, visibleRay)) break; } } - Location location = getLocationOnGrid(y, x); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if (!addLocationToVisibilityRay(y, x, visibleRay)) break; previousError = error; } } else @@ -278,51 +257,45 @@ public LinkedList castVisibleRay(int x1, int y1, int x2, int y2) { error -= ddy; if (error + previousError < ddy) { - Location location = getLocationOnGrid(y, x - xStep); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if(!addLocationToVisibilityRay(y, x - xStep, visibleRay)) break; } else { if (error + previousError > ddy) { - Location location = getLocationOnGrid(y - yStep, x); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if(!addLocationToVisibilityRay(y - yStep, x, visibleRay)) break; } else { - Location location = getLocationOnGrid(y, x - xStep); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); - location = getLocationOnGrid(y - yStep, x); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if(!addLocationToVisibilityRay(y, x - xStep, visibleRay)) break; + if(!addLocationToVisibilityRay(y - yStep, x, visibleRay)) break; } } } - Location location = getLocationOnGrid(y, x); - if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) - { - break; - } - visibleRay.add(location); + if(!addLocationToVisibilityRay(y, x, visibleRay)) break; previousError = error; } } return visibleRay; } + /** + * Add the location at the given coordinates to the specified ray, returning true if the location was added, false if + * not. + * @param x The x coordinate. + * @param y The y coordinate. + * @param ray The ray. + * @return true if the location was added. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean addLocationToVisibilityRay(int x, int y, LinkedList ray) { + Location location = getLocationOnGrid(x, y); + if (location == null || "true".equals(location.getLocationProperties().getProperty("blocksVision"))) + { + return false; + } + ray.add(location); + return true; + } + /** Add visible locations to initiated locations. */ private void setLocationVisibility() { for (int x = 0; x < xSize; x++) @@ -336,9 +309,21 @@ private void setLocationVisibility() { } /** Initialize the locations in the zone. */ - private void initLocations() { + private void constructLocations() { for (int x = 0; x < xSize; x++) for (int y = 0; y < ySize; y++) locationGrid[x][y] = new Location(x, y); } + + /** + * Establish the locations adjacent to one another. + */ + private void setAdjacentLocations() { + for (int x = 0; x < xSize; x++) + for (int y = 0; y < ySize; y++) + locationGrid[x][y].setAdjacentLocations(this); + for (int x = 0; x < xSize; x++) + for (int y = 0; y < ySize; y++) + locationGrid[x][y].setAdjacentWalkableLocations(); + } } diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/ChecksumUtil.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/io/ChecksumUtil.java similarity index 86% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/ChecksumUtil.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/io/ChecksumUtil.java index d2a359a1..798b4f6c 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/ChecksumUtil.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/io/ChecksumUtil.java @@ -13,7 +13,7 @@ public class ChecksumUtil * Get the MD5 Checksum for the given byte array. * @param bytes The byte array. * @return The MD5 checksum. - * @throws NoSuchAlgorithmException If the MD5 algorith cannot be found. + * @throws NoSuchAlgorithmException If the MD5 algorithm cannot be found. */ public static byte[] getMD5Checksum(byte[] bytes) throws NoSuchAlgorithmException { return MessageDigest.getInstance("MD5").digest(bytes); diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldFileReader.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldFileReader.java similarity index 100% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldFileReader.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldFileReader.java diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldFileWriter.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldFileWriter.java similarity index 100% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldFileWriter.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldFileWriter.java diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldXMLBuilder.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldXmlBuilder.java similarity index 82% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldXMLBuilder.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldXmlBuilder.java index 93abfd49..b916c53d 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/io/WorldXMLBuilder.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/io/WorldXmlBuilder.java @@ -10,30 +10,31 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import java.util.List; import java.util.TreeMap; /** - * Used to create XML representations of World obejcts. + * Used to create XML representations of World objects. * @author Caleb Brinkman */ public class WorldXmlBuilder { /** The tag name for World objects. */ - public static final String WORLD_TAG_NAME = "world"; + private static final String WORLD_TAG_NAME = "world"; /** The tag name for zone objects. */ - public static final String ZONE_TAG_NAME = "zone"; + private static final String ZONE_TAG_NAME = "zone"; /** The name of the zone ID attribute. */ - public static final String ZONE_ID_ATTR = "id"; + private static final String ZONE_ID_ATTR = "id"; /** The name of the zone xSize attribute. */ - public static final String ZONE_XSIZE_ATTR = "xSize"; + private static final String ZONE_X_SIZE_ATTR = "xSize"; /** The name of the zone ySize attribute. */ - public static final String ZONE_YSIZE_ATTR = "ySize"; + private static final String ZONE_Y_SIZE_ATTR = "ySize"; /** The zone of the location element. */ - public static final String LOC_TAG_NAME = "location"; + private static final String LOC_TAG_NAME = "location"; /** The name of the location x attribute. */ - public static final String LOC_X_ATTR = "x"; + private static final String LOC_X_ATTR = "x"; /** The name of the location y attribute. */ - public static final String LOC_Y_ATTR = "y"; + private static final String LOC_Y_ATTR = "y"; /** * Create an XML document from the given world. @@ -50,9 +51,10 @@ public static Document createWorldDocument(World world) throws ParserConfigurati Element rootElement = doc.createElement(WORLD_TAG_NAME); doc.appendChild(rootElement); - Zone[] zones = world.getZones(); - for (Zone zone : zones) + List zoneIDs = world.getZoneIDs(); + for (int id : zoneIDs) { + Zone zone = world.getZone(id); Element zoneElement = createZoneElement(doc, zone); addLocationNodes(doc, zone, zoneElement); rootElement.appendChild(zoneElement); @@ -70,8 +72,8 @@ public static Document createWorldDocument(World world) throws ParserConfigurati private static Element createZoneElement(Document doc, Zone zone) { Element zoneElement = doc.createElement(ZONE_TAG_NAME); zoneElement.setAttribute(ZONE_ID_ATTR, String.valueOf(zone.id)); - zoneElement.setAttribute(ZONE_XSIZE_ATTR, String.valueOf(zone.xSize)); - zoneElement.setAttribute(ZONE_YSIZE_ATTR, String.valueOf(zone.ySize)); + zoneElement.setAttribute(ZONE_X_SIZE_ATTR, String.valueOf(zone.xSize)); + zoneElement.setAttribute(ZONE_Y_SIZE_ATTR, String.valueOf(zone.ySize)); return zoneElement; } diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/math/MathUtil.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/math/MathUtil.java similarity index 100% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/math/MathUtil.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/math/MathUtil.java diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/math/Vector2D.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/math/Vector2D.java similarity index 95% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/math/Vector2D.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/math/Vector2D.java index 8ae99cf0..2906eca1 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/math/Vector2D.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/math/Vector2D.java @@ -23,6 +23,16 @@ public Vector2D(Vector2D vector2D) { this(vector2D.getXCoordinate(), vector2D.getYCoordinate()); } + /** + * Construct a new set of coordinates. + * @param x The x coordinate. + * @param y The y coordinate. + */ + public Vector2D(double x, double y) { + xCoordinate = x; + yCoordinate = y; + } + /** * Get the y coordinate. * @return The y coordinate. @@ -55,16 +65,6 @@ public void setXCoordinate(double xCoordinate) { this.xCoordinate = xCoordinate; } - /** - * Construct a new set of coordinates. - * @param x The x coordinate. - * @param y The y coordinate. - */ - public Vector2D(double x, double y) { - xCoordinate = x; - yCoordinate = y; - } - @Override public boolean equals(Object obj) { if (!(obj instanceof Vector2D)) @@ -99,6 +99,11 @@ public Vector2D getVectorInDirection(double distance, double angle) { * @return The angle to the supplied vector. */ public double getAngleToVector(Vector2D vector2D) { + if (vector2D.equals(this)) + { + // Negative infinity specifies that the vectors are the same (can't get an angle). + return Double.NEGATIVE_INFINITY; + } double xDist = vector2D.getXCoordinate() - xCoordinate; double yDist = vector2D.getYCoordinate() - yCoordinate; return java.lang.Math.atan2(yDist, xDist); diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/state/MoveState.java b/jenjin-world-core/src/main/java/com/jenjinstudios/world/state/MoveState.java similarity index 70% rename from jenjin-world-utils/src/main/java/com/jenjinstudios/world/state/MoveState.java rename to jenjin-world-core/src/main/java/com/jenjinstudios/world/state/MoveState.java index 4190dba4..7f25c515 100644 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/state/MoveState.java +++ b/jenjin-world-core/src/main/java/com/jenjinstudios/world/state/MoveState.java @@ -54,6 +54,33 @@ public MoveState(double relativeAngle, int stepsUntilChange, double absoluteAngl @Override public String toString() { - return "[" + relativeAngle + "\u00B0, " + " " + absoluteAngle + "\u00B0] : " + stepsUntilChange; + return "RelAng: " + relativeAngleString() + ", AbsAng: " + absoluteAngle + " Steps: " + stepsUntilChange; + } + + /** + * Get a string representation of the relative angle. Used to report cardinal directions instead of numbers. + * @return The string representation of the relative angle. + */ + private String relativeAngleString() + { + String cheese = String.valueOf(relativeAngle); + if(relativeAngle == FRONT) { + cheese = "FRONT"; + }else if(relativeAngle == FRONT_RIGHT) { + cheese = "FRONT_RIGHT"; + }else if (relativeAngle == FRONT_LEFT) { + cheese = "FRONT_LEFT"; + }else if (relativeAngle == BACK) { + cheese = "BACK"; + }else if(relativeAngle == BACK_LEFT) { + cheese = "BACK_LEFT"; + }else if(relativeAngle == BACK_RIGHT) { + cheese = "BACK_RIGHT"; + }else if(relativeAngle == LEFT) { + cheese = "LEFT"; + }else if(relativeAngle == RIGHT) { + cheese = "RIGHT"; + } + return cheese; } } diff --git a/jenjin-world-server/build.gradle b/jenjin-world-server/build.gradle new file mode 100644 index 00000000..3c539e52 --- /dev/null +++ b/jenjin-world-server/build.gradle @@ -0,0 +1,7 @@ +description = '' + +dependencies { + compile project(':jenjin-core-server') + compile project(':jenjin-world-core') + compile project(':jenjin-world-client') +} diff --git a/jenjin-server-world/src/main/java/com/jenjinstudios/world/Actor.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/Actor.java similarity index 76% rename from jenjin-server-world/src/main/java/com/jenjinstudios/world/Actor.java rename to jenjin-world-server/src/main/java/com/jenjinstudios/world/Actor.java index 164c6633..1dcfec6d 100644 --- a/jenjin-server-world/src/main/java/com/jenjinstudios/world/Actor.java +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/Actor.java @@ -41,9 +41,8 @@ public class Actor extends SightedObject private MoveState nextState; /** Flags whether the state of this actor was forced during this update. */ private boolean forcedState; - - /** Construct a new Actor. */ - public Actor() { this(DEFAULT_NAME); } + /** The Location before a step is taken. */ + private Location locationBeforeStep; /** * Construct an Actor with the given name. @@ -55,18 +54,6 @@ public Actor(String name) { nextMoveStates = new LinkedList<>(); } - /** - * Construct a new Actor with the given ID and name. - * @param name The name. - * @param id The ID. - */ - public Actor(String name, int id) - { - super(name, id); - currentMoveState = new MoveState(IDLE, 0, 0); - nextMoveStates = new LinkedList<>(); - } - /** * Add a new MoveState to the actor's queue. * @param newState The MoveState to add. @@ -77,10 +64,16 @@ public void addMoveState(MoveState newState) { } @Override - public void update() { + public void setUp() { resetFlags(); - Location locationBeforeStep = getLocation(); - step(); + locationBeforeStep = getLocation(); + } + + @Override + public void update() { step(); } + + @Override + public void reset() { // If we're in a new locations after stepping, update the visible array. if (locationBeforeStep != getLocation() || getVisibleLocations().isEmpty()) resetVisibleLocations(); @@ -90,21 +83,17 @@ public void update() { /** Take a step, changing state and correcting steps if necessary. */ public void step() { - int overstepped = getOverstepped(); - MoveState idleState = new MoveState(IDLE, stepsTaken, currentMoveState.absoluteAngle); - if (overstepped < MAX_CORRECT) + MoveState idleState = new MoveState(IDLE, getStepsTaken(), getCurrentMoveState().absoluteAngle); + int stepsToTake = getNextState() != null ? getNextState().stepsUntilChange - getStepsTaken() : -1; + if (stepsToTake <= 0) { - boolean stepCorrectionSuccess = (overstepped < 0) || (correctOverSteps(overstepped)); - if (!stepCorrectionSuccess || !stepForward()) - { - setForcedState(idleState); - } - } else + resetState(); + } + if (!stepForward()) { - setForcedState(currentMoveState); - stepForward(); + setForcedState(idleState); } - stepsTaken++; + incrementStepCounter(); } /** @@ -115,7 +104,7 @@ public boolean stepForward() { if (currentMoveState.relativeAngle == IDLE) { return true; } Vector2D newVector = getVector2D().getVectorInDirection(STEP_LENGTH, currentMoveState.stepAngle); Location newLocation = getWorld().getLocationForCoordinates(getZoneID(), newVector); - if(newLocation == null) { return false; } + if (newLocation == null) { return false; } boolean walkable = !"false".equals(newLocation.getLocationProperties().getProperty("walkable")); if (walkable) { @@ -127,43 +116,6 @@ public boolean stepForward() { } } - /** - * Correct the given number of steps at the specified angles. - * @param overstepped The number of steps over. - * @return Whether correcting the state was successful. - */ - private boolean correctOverSteps(int overstepped) { - double stepAmount = STEP_LENGTH * overstepped; - Vector2D backVector = getVector2D().getVectorInDirection(stepAmount, currentMoveState.stepAngle - Math.PI); - Vector2D newVector = backVector.getVectorInDirection(stepAmount, nextState.stepAngle); - Location newLocation = getWorld().getLocationForCoordinates(getZoneID(), newVector); - boolean success = newLocation != null && !"false".equals(newLocation.getLocationProperties().getProperty("walkable")); - resetState(); - if (success) - { - stepsTaken = overstepped; - setVector2D(newVector); - } - return success; - } - - /** Reset the move state, relativeAngle, and newState flag when changing the move state. */ - private void resetState() { - if (nextState == null) { return; } - stepsTaken = 0; - currentMoveState = nextState; - nextState = nextMoveStates.poll(); - newState = true; - setDirection(currentMoveState.absoluteAngle); - } - - /** - * Determine if a state change is necessary. - * @return The number of steps needed to "correct" to set the actor to the correct state. A negative number means no - * state change is necessary. - */ - private int getOverstepped() { return (nextState != null) ? stepsTaken - nextState.stepsUntilChange : -1; } - /** Reset the flags used by this actor. */ public void resetFlags() { newState = false; @@ -188,6 +140,12 @@ public void resetFlags() { */ public int getStepsTaken() { return stepsTaken; } + /** + * Set the steps taken. + * @param stepsTaken The new number of steps taken. + */ + protected void setStepsTaken(int stepsTaken) { this.stepsTaken = stepsTaken; } + /** * Get the actor's current move state. * @return The actor's current move state. @@ -212,6 +170,12 @@ public void setForcedState(MoveState forced) { resetState(); } + /** + * Get the relative angle of movement of this actor. + * @return The relative angle of movement of this actor. + */ + public double getMoveDirection() { return currentMoveState.relativeAngle; } + /** * Step the actor back to a valid location. * @param stepAngle The angle the actor is moving. @@ -230,9 +194,28 @@ private void stepBackToValid(double stepAngle) { setVector2D(current); } + /** Increment the stepsTaken counter. */ + protected void incrementStepCounter() {stepsTaken++;} + + /** Reset the move state, relativeAngle, and newState flag when changing the move state. */ + protected void resetState() { + if (nextState == null) { return; } + stepsTaken = 0; + currentMoveState = nextState; + nextState = nextMoveStates.poll(); + newState = true; + setDirection(currentMoveState.absoluteAngle); + } + /** - * Get the relative angle of movement of this actor. - * @return The relative angle of movement of this actor. + * Get the next state for this actor to follow. + * @return The next state. */ - public double getMoveDirection() { return currentMoveState.relativeAngle; } + protected MoveState getNextState() { return nextState; } + + /** Clear all upcoming move states. */ + protected void clearMoveStates() { + nextState = null; + nextMoveStates.clear(); + } } diff --git a/jenjin-world-server/src/main/java/com/jenjinstudios/world/NPC.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/NPC.java new file mode 100644 index 00000000..73f8da47 --- /dev/null +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/NPC.java @@ -0,0 +1,185 @@ +package com.jenjinstudios.world; + +import com.jenjinstudios.world.ai.Pathfinder; +import com.jenjinstudios.world.math.MathUtil; +import com.jenjinstudios.world.math.Vector2D; +import com.jenjinstudios.world.state.MoveState; + +import java.util.LinkedList; +import java.util.TreeMap; + +/** + * This class represents a Non-Player Character. + * @author Caleb Brinkman + */ +public class NPC extends Actor +{ + /** The list of targets to which a wandering NPC will move. */ + private final LinkedList wanderTargets; + /** The behavior flags associated with this NPC. */ + private TreeMap behaviorFlags; + /** The Location at which the NPC began following a player. */ + private Location startLocation; + /** The location toward which this NPC is to move. */ + private Location targetLocation; + /** The player currently being targeted. */ + private Player targetPlayer; + /** The index of the current wander target. */ + private int wanderTargetIndex; + + /** + * Construct an NPC with the given name. + * @param name The name. + */ + public NPC(String name) { + this(name, new TreeMap()); + } + + /** + * Construct an NPC with the given name and behavior flags. + * @param name The name of the player. + * @param behaviorFlags The behavior flags. + */ + public NPC(String name, TreeMap behaviorFlags) { + super(name); + this.behaviorFlags = behaviorFlags; + wanderTargets = new LinkedList<>(); + } + + @Override + public void setUp() { + super.setUp(); + if (behaviorFlags.get("aggressive") != null && behaviorFlags.get("aggressive")) + { + doAggressiveBehavior(); + } + if (behaviorFlags.get("wanders") != null && behaviorFlags.get("wanders")) + { + doWandersBehavior(); + } + + } + + /** + * Plot a path to the given Location, and begin following it immediately. + * @param target The target location. + */ + public void plotPath(Location target) { + if (target == null) + { + return; + } + clearMoveStates(); + LinkedList path = Pathfinder.findPath(getLocation(), target); + // Start will be current location. + if(path.isEmpty()) + { + return; + } + Location prev = path.pop(); + Vector2D start = getVector2D(); + Vector2D prevCenter = prev.getCenter(); + double currentAngle = getVector2D().getAngleToVector(prevCenter); + // Create a move state to start moving forward, right now, toward the center of the current location. + MoveState moveState = new MoveState(MoveState.FRONT, getStepsTaken(), currentAngle); + addMoveState(moveState); + int stepsToTake; + + while (!path.isEmpty()) + { + // TODO save angle and refrain from resetting state if angle is the same. Save bandwidth. + Vector2D nextCenter = path.pop().getCenter(); + currentAngle = prevCenter.getAngleToVector(nextCenter); + stepsToTake = (int) MathUtil.round(start.getDistanceToVector(prevCenter) / Actor.STEP_LENGTH, 0); + + MoveState nextState = new MoveState(MoveState.FRONT, stepsToTake, currentAngle); + addMoveState(nextState); + + start = prevCenter; + prevCenter = nextCenter; + + if (path.isEmpty()) + { + stepsToTake = (int) MathUtil.round(start.getDistanceToVector(prevCenter) / Actor.STEP_LENGTH, 0); + MoveState idleState = new MoveState(MoveState.IDLE, stepsToTake, getCurrentMoveState().absoluteAngle); + addMoveState(idleState); + } + } + } + + /** + * Add the specified Location to the list of possible wandering targets. + * @param newTarget The Location to add to the target. + */ + public void addWanderTarget(Location newTarget) { + synchronized (wanderTargets) + { + wanderTargets.add(newTarget); + } + } + + /** Perform the behavior of an NPC that "wanders". */ + private void doWandersBehavior() { + if (targetPlayer == null && (targetLocation == getLocation() || targetLocation == null) && !wanderTargets.isEmpty()) + { + if (getCurrentMoveState().relativeAngle == MoveState.IDLE && getNextState() == null) + { + /* The amount of steps for which the NPC should idle in between reaching targets. */ + int idleTimeBetweenTargets = 100; + if (getStepsTaken() >= idleTimeBetweenTargets) + { + targetLocation = wanderTargets.get(wanderTargetIndex); + plotPath(targetLocation); + if (++wanderTargetIndex >= wanderTargets.size()) + { + wanderTargetIndex = 0; + } + } + } + } + } + + /** Perform the behavior signature of an NPC that is "aggressive". */ + private void doAggressiveBehavior() { + if (targetPlayer == null && (targetLocation == null || targetLocation != startLocation)) + { + targetPlayer = findPlayer(); + targetLocation = targetPlayer != null ? targetPlayer.getLocation() : startLocation; + startLocation = targetLocation != null ? getLocation() : null; + plotPath(targetLocation); + } else if (getLocation() == targetLocation && targetPlayer != null) + { + if (!targetLocation.getObjects().contains(targetPlayer)) + { + if (getVisibleObjects().get(targetPlayer.getId()) != null) + { + targetLocation = targetPlayer.getLocation(); + plotPath(targetLocation); + } else + { + targetLocation = startLocation; + targetPlayer = null; + plotPath(targetLocation); + } + } else + { + targetPlayer = null; + targetLocation = startLocation; + plotPath(targetLocation); + } + } else if (getLocation() == startLocation && targetLocation == startLocation) + { + targetLocation = null; + } + } + + /** + * Get a player from the map of visible objects. + * @return The player, or null if none is found. + */ + private Player findPlayer() { + for (WorldObject object : getVisibleObjects().values()) + if (object instanceof Player) return (Player) object; + return null; + } +} diff --git a/jenjin-world-server/src/main/java/com/jenjinstudios/world/Player.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/Player.java new file mode 100644 index 00000000..82568bf9 --- /dev/null +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/Player.java @@ -0,0 +1,69 @@ +package com.jenjinstudios.world; + +import com.jenjinstudios.world.math.Vector2D; +import com.jenjinstudios.world.state.MoveState; + +import static com.jenjinstudios.world.state.MoveState.IDLE; + +/** + * The player class represents a player in the server-side world. + * @author Caleb Brinkman + */ +public class Player extends Actor +{ + /** + * Construct a player with the given username. + * @param username The username. + */ + public Player(String username) { + super(username); + } + + /** Take a step, changing state and correcting steps if necessary. */ + public void step() { + int overstepped = getOverstepped(); + MoveState idleState = new MoveState(IDLE, getStepsTaken(), getCurrentMoveState().absoluteAngle); + if (overstepped < MAX_CORRECT) + { + boolean stepCorrectionSuccess = (overstepped < 0) || (correctOverSteps(overstepped)); + if (!stepCorrectionSuccess || !stepForward()) + { + setForcedState(idleState); + } + } else + { + setForcedState(getCurrentMoveState()); + stepForward(); + } + incrementStepCounter(); + } + + /** + * Determine if a state change is necessary. + * @return The number of steps needed to "correct" to set the actor to the correct state. A negative number means no + * state change is necessary. + */ + private int getOverstepped() { + return (getNextState() != null) ? getStepsTaken() - getNextState().stepsUntilChange : -1; + } + + /** + * Correct the given number of steps at the specified angles. + * @param overstepped The number of steps over. + * @return Whether correcting the state was successful. + */ + private boolean correctOverSteps(int overstepped) { + double stepAmount = STEP_LENGTH * overstepped; + Vector2D backVector = getVector2D().getVectorInDirection(stepAmount, getCurrentMoveState().stepAngle - Math.PI); + Vector2D newVector = backVector.getVectorInDirection(stepAmount, getNextState().stepAngle); + Location newLocation = getWorld().getLocationForCoordinates(getZoneID(), newVector); + boolean success = newLocation != null && !"false".equals(newLocation.getLocationProperties().getProperty("walkable")); + resetState(); + if (success) + { + setStepsTaken(overstepped); + setVector2D(newVector); + } + return success; + } +} diff --git a/jenjin-server-world/src/main/java/com/jenjinstudios/world/WorldClientHandler.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/WorldClientHandler.java similarity index 72% rename from jenjin-server-world/src/main/java/com/jenjinstudios/world/WorldClientHandler.java rename to jenjin-world-server/src/main/java/com/jenjinstudios/world/WorldClientHandler.java index 8a0c8df3..f8dd3b6e 100644 --- a/jenjin-server-world/src/main/java/com/jenjinstudios/world/WorldClientHandler.java +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/WorldClientHandler.java @@ -1,6 +1,7 @@ package com.jenjinstudios.world; import com.jenjinstudios.io.Message; +import com.jenjinstudios.io.MessageRegistry; import com.jenjinstudios.net.ClientHandler; import com.jenjinstudios.world.util.WorldServerMessageGenerator; @@ -15,40 +16,33 @@ public class WorldClientHandler extends ClientHandler { /** The WorldServer owning this handler. */ private final WorldServer server; - /** The ID of the player controlled by this clienthandler. */ + /** The ID of the player controlled by this client handler. */ private long playerID = -1; /** The Actor managed by this handler. */ - private Actor actor; + private Player player; /** * Construct a new Client Handler using the given socket. When constructing a new ClientHandler, it is necessary to * send the client a FirstConnectResponse message with the server's UPS * @param s The server for which this handler works. * @param sk The socket used to communicate with the client. + * @param messageRegistry The MessageRegistry for this ClientHandler. * @throws java.io.IOException If the socket is unable to connect. */ - public WorldClientHandler(WorldServer s, Socket sk) throws IOException { - super(s, sk); + public WorldClientHandler(WorldServer s, Socket sk, MessageRegistry messageRegistry) throws IOException { + super(s, sk, messageRegistry); server = s; - queueMessage(WorldServerMessageGenerator.generateActorStepLengthMessage()); - } - - /** - * Get the actor of this client handler. - * @return The actor controlled by this client handler. - */ - public Actor getActor() { - return actor; + queueMessage(WorldServerMessageGenerator.generateActorStepLengthMessage(this)); } /** * Set the Actor managed by this handler. - * @param actor The actor to be managed by this handler. + * @param player The player to be managed by this handler. */ - public void setActor(Actor actor) { - this.actor = actor; - setUsername(actor.getName()); - setPlayerID(actor.getId()); + public void setPlayer(Player player) { + this.player = player; + setUsername(player.getName()); + setPlayerID(player.getId()); } /** @@ -64,7 +58,7 @@ public void setPlayerID(long id) { public void update() { super.update(); - if (actor == null) + if (player == null) return; queueForcesStateMessage(); @@ -80,35 +74,35 @@ public void update() { * Get the player associated with this client handler. * @return The player associated with this client handler. */ - public Actor getPlayer() { return actor; } + public Player getPlayer() { return player; } /** Generate and queue messages for newly visible objects. */ private void queueNewlyVisibleMessages() { - for (WorldObject object : actor.getNewlyVisibleObjects()) + for (WorldObject object : player.getNewlyVisibleObjects()) { Message newlyVisibleMessage; - newlyVisibleMessage = WorldServerMessageGenerator.generateNewlyVisibleMessage(object); + newlyVisibleMessage = WorldServerMessageGenerator.generateNewlyVisibleMessage(this, object); queueMessage(newlyVisibleMessage); } } /** Generate and queue messages for newly invisible objects. */ private void queueNewlyInvisibleMessages() { - for (WorldObject object : actor.getNewlyInvisibleObjects()) + for (WorldObject object : player.getNewlyInvisibleObjects()) { - Message newlyInvisibleMessage = WorldServerMessageGenerator.generateNewlyInvisibleMessage(object); + Message newlyInvisibleMessage = WorldServerMessageGenerator.generateNewlyInvisibleMessage(this, object); queueMessage(newlyInvisibleMessage); } } /** Generate and queue messages for actors with changed states. */ private void queueStateChangeMessages() { - for (WorldObject object : actor.getVisibleObjects().values()) + for (WorldObject object : player.getVisibleObjects().values()) { Actor changedActor; if (object instanceof Actor && (changedActor = (Actor) object).isNewState()) { - Message newState = WorldServerMessageGenerator.generateChangeStateMessage(changedActor); + Message newState = WorldServerMessageGenerator.generateChangeStateMessage(this, changedActor); queueMessage(newState); } } @@ -116,7 +110,7 @@ private void queueStateChangeMessages() { /** Generate and queue a ForcedStateMessage if necessary. */ private void queueForcesStateMessage() { - if (actor.isForcedState()) - queueMessage(WorldServerMessageGenerator.generateForcedStateMessage(actor, server)); + if (player.isForcedState()) + queueMessage(WorldServerMessageGenerator.generateForcedStateMessage(this, player, server)); } } diff --git a/jenjin-server-world/src/main/java/com/jenjinstudios/world/WorldServer.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/WorldServer.java similarity index 88% rename from jenjin-server-world/src/main/java/com/jenjinstudios/world/WorldServer.java rename to jenjin-world-server/src/main/java/com/jenjinstudios/world/WorldServer.java index e66b4e76..3a96994d 100644 --- a/jenjin-server-world/src/main/java/com/jenjinstudios/world/WorldServer.java +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/WorldServer.java @@ -3,7 +3,9 @@ import com.jenjinstudios.net.AuthServer; import com.jenjinstudios.world.io.WorldFileReader; import com.jenjinstudios.world.sql.WorldSQLHandler; +import org.xml.sax.SAXException; +import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; /** @@ -32,9 +34,11 @@ public class WorldServer extends AuthServer * @param sqlHandler The WorldSqlHandler used to communicate with the MySql Database. * @throws java.io.IOException If there is an IO Error when initializing the server. * @throws NoSuchMethodException If there is no appropriate constructor for the specified ClientHandler constructor. + * @throws javax.xml.parsers.ParserConfigurationException If there is an error parsing XML files. + * @throws org.xml.sax.SAXException If there is an error parsing XML files. */ public WorldServer(WorldFileReader worldFileReader, int ups, int port, Class wchClass, - WorldSQLHandler sqlHandler) throws IOException, NoSuchMethodException + WorldSQLHandler sqlHandler) throws IOException, NoSuchMethodException, ParserConfigurationException, SAXException { super(ups, port, wchClass, sqlHandler); this.world = worldFileReader.read(); diff --git a/jenjin-world-server/src/main/java/com/jenjinstudios/world/ai/Pathfinder.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/ai/Pathfinder.java new file mode 100644 index 00000000..d848faf5 --- /dev/null +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/ai/Pathfinder.java @@ -0,0 +1,133 @@ +package com.jenjinstudios.world.ai; + +import com.jenjinstudios.world.Location; + +import java.util.LinkedList; +import java.util.Stack; + +/** + * This class contains helper methods used to determine a list of Locations to follow to get from point to point around + * obstacles. + * @author Caleb Brinkman + */ +public class Pathfinder +{ + /** The maximum number of nodes to check before giving up and assuming the path cannot be found. */ + public static final int NODE_LIMIT = 1000; + + /** + * Find a path between the two locations. + * @param start The start location. + * @param end The end location. + * @return The Locations necessary to traverse in order to travel from A to B. + */ + public static LinkedList findPath(Location start, Location end) { + LinkedList path = new LinkedList<>(); + + LinkedList openList = new LinkedList<>(); + LinkedList closedList = new LinkedList<>(); + Node selectedNode = new Node(start, end); + openList.add(selectedNode); + + while (selectedNode.location != end && !openList.isEmpty() && openList.size() < NODE_LIMIT) + { + int lowestF = Integer.MAX_VALUE; + selectedNode = openList.peek(); + for (Node node : openList) + { + if (node.F < lowestF) + { + lowestF = node.F; + selectedNode = node; + } + } + openList.remove(selectedNode); + closedList.add(selectedNode); + for (Location adjacentLocation : selectedNode.location.getAdjacentWalkableLocations()) + { + Node adjacentNode = new Node(selectedNode, adjacentLocation, end); + if (closedList.contains(adjacentNode)) + { + continue; + } + if (openList.contains(adjacentNode)) + { + int indexOfOldNode = openList.indexOf(adjacentNode); + Node oldNode = openList.get(indexOfOldNode); + if (adjacentNode.G < oldNode.G) + { + oldNode.parent = selectedNode; + } + } else + { + openList.add(adjacentNode); + } + } + } + + if(selectedNode.location == end) + { + Stack reversePath = new Stack<>(); + while(selectedNode.location != start) + { + reversePath.push(selectedNode.location); + selectedNode = selectedNode.parent; + } + reversePath.push(selectedNode.location); + while(!reversePath.isEmpty()) + { + path.add(reversePath.pop()); + } + } + + return path; + } + + /** Used to represent a path finding node. */ + private static class Node + { + /** The x coordinate of this node. */ + public final int x; + /** The y coordinate of this node. */ + public final int y; + /** The location represented in this node. */ + public final Location location; + /** The G-Score of this node. */ + public final int G; + /** The H-Score of this node. */ + public final int H; + /** The F-Score of this node. */ + public final int F; + /** The parent of this node. */ + public Node parent; + + /** + * Construct a new node with the given parent and representing the given location. + * @param parent The parent node. + * @param location The location. + * @param target The target location. + */ + public Node(Node parent, Location location, Location target) { + this.parent = parent; + this.location = location; + x = location.X_COORDINATE; + y = location.Y_COORDINATE; + G = this.parent == null ? 0 : parent.G + (parent.y == y || parent.x == x ? 10 : 14); + H = 10 * (Math.abs(x - target.X_COORDINATE) + Math.abs(y - target.Y_COORDINATE)); + F = G + H; + } + + /** + * Construct a new, parent less node. + * @param location The location represented by this node. + * @param target The target location. + */ + public Node(Location location, Location target) { + this(null, location, target); + } + + public boolean equals(Object o) { + return o != null && o instanceof Node && ((Node) o).location == location; + } + } +} diff --git a/jenjin-world-server/src/main/java/com/jenjinstudios/world/io/CompoundFileReader.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/io/CompoundFileReader.java new file mode 100644 index 00000000..343de28e --- /dev/null +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/io/CompoundFileReader.java @@ -0,0 +1,85 @@ +package com.jenjinstudios.world.io; + +import com.jenjinstudios.world.NPC; +import com.jenjinstudios.world.World; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import java.io.*; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +/** + * This class is used to read a World file which contains both World structure information and NPC information. + * @author Caleb Brinkman + */ +public class CompoundFileReader +{ + /** The world being built from the XML. */ + private final World world; + /** The NPCFileReader. */ + private NPCFileReader npcFileReader; + + /** + * Construct a new NPCFileReader for the given file. + * @param npcFile The file containing the NPC info. + * @throws java.io.IOException If there's an error reading the file. + * @throws javax.xml.parsers.ParserConfigurationException If there's an error parsing the XML. + * @throws org.xml.sax.SAXException If there's an error validating the XML. + * @throws java.security.NoSuchAlgorithmException If transform algorithms cannot be found. + * @throws javax.xml.transform.TransformerException If there is a Transformer Exception. + */ + public CompoundFileReader(File npcFile) throws IOException, ParserConfigurationException, SAXException, TransformerException, NoSuchAlgorithmException { + this(new FileInputStream(npcFile)); + } + + /** + * Construct a new NPCFileReader for the given input stream. + * @param inputStream The stream containing the NPC XML. + * @throws javax.xml.parsers.ParserConfigurationException If there's an error parsing the XML. + * @throws java.io.IOException If there's an error reading the stream. + * @throws org.xml.sax.SAXException If there's an error validating the XML. + * @throws java.security.NoSuchAlgorithmException If transform algorithms cannot be found. + * @throws javax.xml.transform.TransformerException If there is a Transformer Exception. + */ + public CompoundFileReader(InputStream inputStream) throws ParserConfigurationException, IOException, SAXException, TransformerException, NoSuchAlgorithmException { + InputStream worldFileStream = new NonClosableBufferedInputStream(inputStream); + worldFileStream.mark(Integer.MAX_VALUE); + WorldFileReader worldFileReader = new WorldFileReader(worldFileStream); + world = worldFileReader.read(); + worldFileStream.reset(); + npcFileReader = new NPCFileReader(world, worldFileStream); + } + + /** + * Read the XML document and return the correctly structured World containing all NPCs. + * @return The World represented by the XML with the NPCs already added. + */ + public World read() { + List npcs = npcFileReader.read(); + for (NPC npc : npcs) + { + world.addObject(npc); + } + return world; + } + + /** A quick and dirty reusable input stream so the file can be read twice. */ + private class NonClosableBufferedInputStream extends BufferedInputStream + { + /** + * Construct a new NonClosableInputStream. + * @param in The input stream used to build this one. + */ + public NonClosableBufferedInputStream(InputStream in) { + super(in); + super.mark(Integer.MAX_VALUE); + } + + @Override + public void close() throws IOException { + super.reset(); + } + } +} diff --git a/jenjin-world-server/src/main/java/com/jenjinstudios/world/io/NpcFileReader.java b/jenjin-world-server/src/main/java/com/jenjinstudios/world/io/NpcFileReader.java new file mode 100644 index 00000000..6420f9f3 --- /dev/null +++ b/jenjin-world-server/src/main/java/com/jenjinstudios/world/io/NpcFileReader.java @@ -0,0 +1,139 @@ +package com.jenjinstudios.world.io; + +import com.jenjinstudios.world.Location; +import com.jenjinstudios.world.NPC; +import com.jenjinstudios.world.World; +import com.jenjinstudios.world.Zone; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeMap; + +/** + * The class responsible for reading NPCs from an xml file. + * @author Caleb Brinkman + */ +public class NPCFileReader +{ + /** The tag name for the root "zone" tags. */ + public static final String NPC_TAG_NAME = "npc"; + /** The XML document storing the NPC data. */ + private final Document npcDocument; + /** The world in which to look for locations. */ + private final World world; + + /** + * Construct a new NPCFileReader for the given file. + * @param world The world which will be used to retrieve location references; this object is not modified, only read. + * @param npcFile The file containing the NPC info. + * @throws IOException If there's an error reading the file. + * @throws ParserConfigurationException If there's an error parsing the XML. + * @throws SAXException If there's an error validating the XML. + */ + public NPCFileReader(World world, File npcFile) throws IOException, ParserConfigurationException, SAXException { + this(world, new FileInputStream(npcFile)); + } + + /** + * Construct a new NPCFileReader for the given input stream. + * @param inputStream The stream containing the NPC XML. + * @param world The world which will be used to retrieve location references; this object is not modified, only read. + * @throws ParserConfigurationException If there's an error parsing the XML. + * @throws IOException If there's an error reading the stream. + * @throws SAXException If there's an error validating the XML. + */ + public NPCFileReader(World world, InputStream inputStream) throws ParserConfigurationException, IOException, SAXException { + this.world = world; + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + npcDocument = builder.parse(inputStream); + npcDocument.getDocumentElement().normalize(); + } + + /** + * Consume the document and return all NPCs read from it. + * @return A list of NPCs parsed from the XML input. + */ + public List read() { + LinkedList r = new LinkedList<>(); + NodeList npcNodes = npcDocument.getElementsByTagName(NPC_TAG_NAME); + for(int i=0; i behaviors = parseBehaviorElements(npcElement.getElementsByTagName("behaviors")); + + NPC currentNPC = new NPC(name, behaviors); + currentNPC.setVector2D(xCoordinate, yCoordinate); + currentNPC.setZoneID(zoneID); + + List wanderTargets = parseWanderTargets(zoneID, npcElement.getElementsByTagName("wander_targets")); + for(Location location : wanderTargets) + { + currentNPC.addWanderTarget(location); + } + r.add(currentNPC); + } + return r; + } + + /** + * Parse a list of "wander_targets" elements. + * @param zoneID The id of the zone in which the NPC is being created. + * @param wanderTargetsLists The NodeList containing the "wander_targets" elements. + * @return The list of parsed target locations. + */ + private List parseWanderTargets(int zoneID, NodeList wanderTargetsLists) { + LinkedList targetList = new LinkedList<>(); + Zone targetZone = world.getZone(zoneID); + for(int i=0; i parseBehaviorElements(NodeList behaviorsElements) { + TreeMap behaviors = new TreeMap<>(); + for(int i=0; i foundPath = Pathfinder.findPath(start, end); + + + LinkedList correctPath = new LinkedList<>(); + correctPath.add(testZone.getLocationOnGrid(3,3)); + correctPath.add(testZone.getLocationOnGrid(4,4)); + correctPath.add(testZone.getLocationOnGrid(4,5)); + correctPath.add(testZone.getLocationOnGrid(5,5)); + correctPath.add(testZone.getLocationOnGrid(6,5)); + correctPath.add(testZone.getLocationOnGrid(7,4)); + correctPath.add(testZone.getLocationOnGrid(7,3)); + + + Assert.assertEquals(correctPath, foundPath); + } +} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldObjectTest.java b/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldObjectTest.java similarity index 93% rename from jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldObjectTest.java rename to jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldObjectTest.java index 92b6c203..900e9100 100644 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/WorldObjectTest.java +++ b/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldObjectTest.java @@ -3,9 +3,8 @@ import com.jenjinstudios.world.math.Vector2D; import com.jenjinstudios.world.World; import com.jenjinstudios.world.WorldObject; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.testng.Assert; +import org.testng.annotations.*; /** * Test WorldObject class. @@ -32,9 +31,9 @@ public class WorldObjectTest * Set up before each test. * @throws Exception If there is an exception. */ - @Before + @BeforeMethod public void setUp() throws Exception { - worldObject = new WorldObject(); + worldObject = new WorldObject("Test Object"); direction = 2.15f; xCoordinate = 5.20f; yCoordinate = 7.23f; @@ -85,7 +84,7 @@ public void testGetLocation() throws Exception { */ @Test public void testSetId() throws Exception { - WorldObject worldObject1 = new WorldObject(); + WorldObject worldObject1 = new WorldObject("Test Object"); worldObject1.setId(id); Assert.assertEquals(id, worldObject1.getId()); } diff --git a/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldServerTest.java b/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldServerTest.java new file mode 100644 index 00000000..c86b1127 --- /dev/null +++ b/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldServerTest.java @@ -0,0 +1,391 @@ +package test.jenjinstudios.world; + +import com.jenjinstudios.util.FileUtil; +import com.jenjinstudios.world.*; +import com.jenjinstudios.world.io.WorldFileReader; +import com.jenjinstudios.world.math.MathUtil; +import com.jenjinstudios.world.math.Vector2D; +import com.jenjinstudios.world.sql.WorldSQLHandler; +import com.jenjinstudios.world.state.MoveState; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.InputStream; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Test the world server. + * @author Caleb Brinkman + */ +public class WorldServerTest +{ + /** The Logger for this class. */ + private static final Logger LOGGER = Logger.getLogger(WorldServerTest.class.getName()); + /** The current test account being used. */ + private static int testAccountNumber = 0; + /** The port used to listen and connect. */ + private static int port = WorldServer.DEFAULT_PORT; + + // Server fields + /** The world server used to test. */ + private WorldServer worldServer; + /** The world used for testing. */ + private World world; + /** The server-side actor representing the player. */ + private Actor serverPlayer; + + // Client fields + /** The world client used to test. */ + private WorldClient worldClient; + /** The client-side player used for testing. */ + private ClientPlayer clientPlayer; + + /** + * Construct the test. + * @throws Exception If there's an Exception. + */ + @BeforeClass + public static void construct() throws Exception { + InputStream configFile = WorldServerTest.class.getResourceAsStream("/test/jenjinstudios/logger.properties"); + LogManager.getLogManager().readConfiguration(configFile); + } + + /** + * Make the client player stay idle for the given number of steps. + * @param i The number of steps. + * @param clientPlayer The client player. + * @throws InterruptedException If there's an issue waiting for the player to be idle for the given number of steps. + */ + private static void idleClientPlayer(int i, ClientPlayer clientPlayer) throws InterruptedException { + clientPlayer.setNewRelativeAngle(MoveState.IDLE); + while (clientPlayer.getRelativeAngle() != MoveState.IDLE || clientPlayer.getStepsTaken() < i) + { + Thread.sleep(2); + } + } + + /** + * Move the specified actor to within one STEP_LENGTH of the specified vector. + * @param serverActor The actor. + * @param newVector The target vector. + * @throws InterruptedException If there is an error blocking until the target is reached. + */ + private static void moveServerActorToVector(Actor serverActor, Vector2D newVector) throws InterruptedException { + int stepsTaken = serverActor.getStepsTaken(); + double newAngle = serverActor.getVector2D().getAngleToVector(newVector); + MoveState newState = new MoveState(newAngle, stepsTaken, 0); + serverActor.addMoveState(newState); + double distanceToNewVector = serverActor.getVector2D().getDistanceToVector(newVector); + while (distanceToNewVector > Actor.STEP_LENGTH && !serverActor.isForcedState()) + { + Thread.sleep(10); + distanceToNewVector = serverActor.getVector2D().getDistanceToVector(newVector); + } + MoveState idleState = new MoveState(MoveState.IDLE, serverActor.getStepsTaken(), 0); + serverActor.addMoveState(idleState); + Thread.sleep(10); + } + + /** + * Move the client and server player to the given vector, by initiating the move client-side. Also sends a ping to the + * server with each sleep cycle. + * @param newVector The vector to which to move. + * @param client The client. + * @param serverPlayer The server player. + * @throws InterruptedException If there's an exception. + */ + private static void moveClientPlayerTowardVector(Vector2D newVector, WorldClient client, Actor serverPlayer) throws InterruptedException { + ClientPlayer clientPlayer = client.getPlayer(); + // Make sure not to send multiple states during the same update. + idleClientPlayer(1, clientPlayer); + double newAngle = clientPlayer.getVector2D().getAngleToVector(newVector); + clientPlayer.setNewRelativeAngle(newAngle); + double targetDistance = clientPlayer.getVector2D().getDistanceToVector(newVector); + while (targetDistance >= Actor.STEP_LENGTH && !clientPlayer.isForcedState()) + { + client.sendPing(); + Thread.sleep(2); + targetDistance = clientPlayer.getVector2D().getDistanceToVector(newVector); + } + int stepsToIdle = Math.abs(clientPlayer.getStepsTaken() - serverPlayer.getStepsTaken()) * 5; + idleClientPlayer(stepsToIdle, clientPlayer); + double playersDistance = clientPlayer.getVector2D().getDistanceToVector(serverPlayer.getVector2D()); + Assert.assertEquals(0, playersDistance, .001); + } + + /** + * Set up the client and server. + * @throws Exception If there's an exception. + */ + @BeforeMethod + public void setUp() throws Exception { + testAccountNumber++; + port++; + initWorldServer(); + initWorldClient(); + } + + /** + * Tear down the client and server. + * @throws Exception If there's an exception. + */ + @AfterMethod + public void tearDown() throws Exception { + serverPlayer.setVector2D(new Vector2D(0, 0)); + worldClient.sendBlockingLogoutRequest(); + LOGGER.log(Level.INFO, "Shutting down WorldClient. Avg. ping was {0}", worldClient.getAveragePingTime()); + worldClient.shutdown(); + + worldServer.shutdown(); + + File resourcesDir = new File("resources/"); + FileUtil.deleteRecursively(resourcesDir); + } + + /** + * Test the actor visibility after player and actor movement. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testActorVisibility() throws Exception { + double visibilityEdge = Location.SIZE * (SightedObject.VIEW_RADIUS + 1); + Vector2D serverActorStartPosition = new Vector2D(0, visibilityEdge + 1); + Vector2D serverActorTargetPosition = new Vector2D(0, visibilityEdge - 1); + Actor serverActor = new Actor("TestActor"); + serverActor.setVector2D(serverActorStartPosition); + world.addObject(serverActor); + + LOGGER.log(Level.INFO, "Moving serverActor to {0}", serverActorTargetPosition); + moveServerActorToVector(serverActor, serverActorTargetPosition); + + LOGGER.log(Level.INFO, "Asserting that clientPlayer can see serverPlayer"); + WorldObject clientActor = worldClient.getPlayer().getVisibleObjects().get(serverActor.getId()); + Assert.assertEquals(1, worldClient.getPlayer().getVisibleObjects().size()); + Assert.assertNotNull(clientActor); + Thread.sleep(50); + Assert.assertEquals(serverActor.getVector2D(), clientActor.getVector2D()); + + LOGGER.log(Level.INFO, "Moving serverActor out of visible range."); + moveServerActorToVector(serverActor, serverActorStartPosition); + Assert.assertEquals(0, worldClient.getPlayer().getVisibleObjects().size()); + + LOGGER.log(Level.INFO, "Moving clientPlayer into visible range."); + moveClientPlayerTowardVector(new Vector2D(0, Location.SIZE + 1), worldClient, serverPlayer); + Assert.assertEquals(1, worldClient.getPlayer().getVisibleObjects().size()); + clientActor = worldClient.getPlayer().getVisibleObjects().get(serverActor.getId()); + Assert.assertEquals(serverActor.getVector2D(), clientActor.getVector2D()); + + LOGGER.log(Level.INFO, "Moving clientPlayer back to origin."); + moveClientPlayerTowardVector(Vector2D.ORIGIN, worldClient, serverPlayer); + Assert.assertEquals(0, worldClient.getPlayer().getVisibleObjects().size()); + } + + /** + * Test the state-forcing functionality. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testForcedStateFromEdge() throws Exception { + LOGGER.log(Level.INFO, "Attempting to move clientPlayer off edge of world."); + moveClientPlayerTowardVector(new Vector2D(-1.0, 0), worldClient, serverPlayer); + //moveClientPlayerTowardVector(new Vector2D(1, 0), worldClient, serverPlayer); + Assert.assertFalse(clientPlayer.isForcedState()); + Assert.assertEquals(serverPlayer.getVector2D(), clientPlayer.getVector2D()); + } + + /** + * Test the state forcing functionality. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testForcedState() throws Exception { + moveClientPlayerTowardVector(new Vector2D(0.0, 0.2), worldClient, serverPlayer); + moveClientPlayerTowardVector(new Vector2D(0.0, -0.4), worldClient, serverPlayer); + idleClientPlayer(5, clientPlayer); + Assert.assertEquals(clientPlayer.getVector2D(), serverPlayer.getVector2D()); + } + + /** + * Test basic movement. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testMovement() throws Exception { + Vector2D targetVector = new Vector2D(3.956, 3.7468); + moveClientPlayerTowardVector(targetVector, worldClient, serverPlayer); + } + + /** + * Test repeatedly forcing client. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testRepeatedForcedState() throws Exception { + moveClientPlayerTowardVector(new Vector2D(.5, .5), worldClient, serverPlayer); + moveClientPlayerTowardVector(new Vector2D(-1, -1), worldClient, serverPlayer); + moveClientPlayerTowardVector(new Vector2D(.5, .5), worldClient, serverPlayer); + moveClientPlayerTowardVector(new Vector2D(-1, -1), worldClient, serverPlayer); + moveClientPlayerTowardVector(new Vector2D(.5, .5), worldClient, serverPlayer); + } + + /** + * Test movement to various random vectors. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testRandomMovement() throws Exception { + idleClientPlayer(1, clientPlayer); + int maxCoordinate = 3; + for (int i = 0; i < 3; i++) + { + double randomX = MathUtil.round(java.lang.Math.random() * maxCoordinate, 4); + double randomY = MathUtil.round(java.lang.Math.random() * maxCoordinate, 4); + Vector2D random = new Vector2D(randomX, randomY); + moveClientPlayerTowardVector(random, worldClient, serverPlayer); + double distance = clientPlayer.getVector2D().getDistanceToVector(serverPlayer.getVector2D()); + Assert.assertEquals(0, distance, .001); + } + } + + /** + * Test attempting to walk into a "blocked" location. + * @throws Exception If there's an Exception. + */ + @Test(timeOut = 10000) + public void testAttemptBlockedLocation() throws Exception { + Vector2D vector1 = new Vector2D(15, 0); + Vector2D attemptedVector2 = new Vector2D(15, 15); + Vector2D actualVector2 = new Vector2D(15, 9.8); + Vector2D vector3 = new Vector2D(15, 9); + Vector2D vector4 = new Vector2D(9, 9); + Vector2D vector5 = new Vector2D(9, 11); + Vector2D attemptedVector6 = new Vector2D(15, 11); + Vector2D actualVector6 = new Vector2D(9.8, 11); + Vector2D attemptedVector7 = new Vector2D(15, 15); + Vector2D actualVector7 = new Vector2D(9.8, 11); + + // Move to (35, 0) + moveClientPlayerTowardVector(vector1, worldClient, serverPlayer); + Assert.assertEquals(vector1, clientPlayer.getVector2D()); + + // Attempt to move to (35, 35) + // This attempt should be forced to stop one step away from + moveClientPlayerTowardVector(attemptedVector2, worldClient, serverPlayer); + Assert.assertEquals(actualVector2, clientPlayer.getVector2D()); + + moveClientPlayerTowardVector(vector3, worldClient, serverPlayer); + Assert.assertEquals(vector3, clientPlayer.getVector2D()); + + moveClientPlayerTowardVector(vector4, worldClient, serverPlayer); + Assert.assertEquals(vector4, clientPlayer.getVector2D()); + + moveClientPlayerTowardVector(vector5, worldClient, serverPlayer); + Assert.assertEquals(vector5, clientPlayer.getVector2D()); + + moveClientPlayerTowardVector(vector5, worldClient, serverPlayer); + Assert.assertEquals(vector5, clientPlayer.getVector2D()); + + moveClientPlayerTowardVector(attemptedVector6, worldClient, serverPlayer); + Assert.assertEquals(actualVector6, clientPlayer.getVector2D()); + + moveClientPlayerTowardVector(attemptedVector7, worldClient, serverPlayer); + Assert.assertEquals(actualVector7, clientPlayer.getVector2D()); + } + + /** + * Test NPC movement. + * @throws Exception If there's an exception. + */ + @Test(timeOut = 10000) + public void testNPCMovement() throws Exception { + NPC testNPC = new NPC("TestNPC"); + Location target = world.getZone(0).getLocationOnGrid(0, 0); + testNPC.setVector2D(20, 5); + world.addObject(testNPC); + testNPC.plotPath(target); + double distance = testNPC.getVector2D().getDistanceToVector(target.getCenter()); + while (distance >= Actor.STEP_LENGTH - .001) + { + distance = testNPC.getVector2D().getDistanceToVector(target.getCenter()); + Thread.sleep(10); + } + WorldObject clientNPC = clientPlayer.getVisibleObjects().get(testNPC.getId()); + distance = testNPC.getVector2D().getDistanceToVector(clientNPC.getVector2D()); + Assert.assertEquals(distance, 0, Actor.STEP_LENGTH); + + // Make sure the NPC is in the same place. + Thread.sleep(100); + Assert.assertEquals(testNPC.getVector2D(), clientNPC.getVector2D()); + } + + /** + * Test logging the player into and out of the world, including updating coordinates. + * @throws Exception If there's an exception. + */ + @Test + public void testLoginLogout() throws Exception { + testAccountNumber++; + WorldSQLHandler worldSQLHandler = new WorldSQLHandler("localhost", "jenjin_test", "jenjin_user", + "jenjin_password"); + + Assert.assertTrue(worldSQLHandler.isConnected()); + + Actor player = worldSQLHandler.logInPlayer("TestAccount" + testAccountNumber, "testPassword"); + Vector2D origin = player.getVector2D(); + Vector2D secondVector = new Vector2D(50, 50); + + Assert.assertEquals(origin, player.getVector2D()); + + player.setVector2D(secondVector); + Assert.assertTrue(worldSQLHandler.logOutPlayer(player)); + + player = worldSQLHandler.logInPlayer("TestAccount" + testAccountNumber, "testPassword"); + Assert.assertEquals(secondVector, player.getVector2D()); + + player.setVector2D(origin); + Assert.assertTrue(worldSQLHandler.logOutPlayer(player)); + + player = worldSQLHandler.logInPlayer("TestAccount" + testAccountNumber, "testPassword"); + Assert.assertEquals(origin, player.getVector2D()); + + Assert.assertTrue(worldSQLHandler.logOutPlayer(player)); + } + + /** + * Initialize and log the client in. + * @throws Exception If there's an exception. + */ + private void initWorldClient() throws Exception { + String user = "TestAccount" + testAccountNumber; + LOGGER.log(Level.INFO, "Logging into account {0}", user); + worldClient = new WorldClient(new File("resources/WorldTestFile.xml"), "localhost", port, user, "testPassword"); + worldClient.blockingStart(); + worldClient.sendBlockingWorldFileRequest(); + worldClient.sendBlockingLoginRequest(); + + /* The WorldClientHandler used to test. */ + WorldClientHandler worldClientHandler = worldServer.getClientHandlerByUsername(worldClient.getUsername()); + clientPlayer = worldClient.getPlayer(); + serverPlayer = worldClientHandler.getPlayer(); + } + + /** + * Initialize the world and world server. + * @throws Exception If there's an exception. + */ + private void initWorldServer() throws Exception { + /* The world SQL handler used to test. */ + WorldSQLHandler worldSQLHandler = new WorldSQLHandler("localhost", "jenjin_test", "jenjin_user", "jenjin_password"); + worldServer = new WorldServer(new WorldFileReader(getClass().getResourceAsStream("/test/jenjinstudios/world/WorldFile01.xml")), + WorldServer.DEFAULT_UPS, port, WorldClientHandler.class, worldSQLHandler); + world = worldServer.getWorld(); + worldServer.blockingStart(); + } + +} diff --git a/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldTest.java b/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldTest.java new file mode 100644 index 00000000..9bf9cf40 --- /dev/null +++ b/jenjin-world-server/src/test/java/test/jenjinstudios/world/WorldTest.java @@ -0,0 +1,55 @@ +package test.jenjinstudios.world; + +import com.jenjinstudios.world.WorldObject; +import com.jenjinstudios.world.math.Vector2D; +import com.jenjinstudios.world.Location; +import com.jenjinstudios.world.World; +import org.testng.Assert; +import org.testng.annotations.Test; + + +import java.util.ArrayList; + +/** + * Tests the World class. + * @author Caleb Brinkman + */ +public class WorldTest +{ + /** + * Test the getLocationArea() method. + * @throws Exception If there's an exception. + */ + @Test + public void testGetLocationArea() throws Exception { + World testWorld = new World(); + ArrayList testGrid = testWorld.getLocationArea(0, new Vector2D(0, 0), 3); + Assert.assertEquals(9, testGrid.size()); + + testGrid = testWorld.getLocationArea(0, new Vector2D(50, 50), 3); + Assert.assertEquals(25, testGrid.size()); + + testGrid = testWorld.getLocationArea(0, new Vector2D(50, 50), 4); + Assert.assertEquals(49, testGrid.size()); + } + + /** + * Test the object count and purge features of the World class. + * @throws Exception If there's an exception. + */ + @Test + public void testPurgeObjects() throws Exception { + World testWorld = new World(); + int random = (int) (Math.random() * 100); + for(int i=0; i npcList = npcFileReader.read(); + for(NPC npc : npcList) + { + testWorld.addObject(npc); + } + Assert.assertEquals(1, testWorld.getObjectCount(), "World object count after reading NPCFile01.xml"); + } +} diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/io/WorldFileReaderTest.java b/jenjin-world-server/src/test/java/test/jenjinstudios/world/io/WorldFileReaderTest.java similarity index 86% rename from jenjin-server-world/src/test/java/test/jenjinstudios/world/io/WorldFileReaderTest.java rename to jenjin-world-server/src/test/java/test/jenjinstudios/world/io/WorldFileReaderTest.java index 62d9283e..94f4aad4 100644 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/io/WorldFileReaderTest.java +++ b/jenjin-world-server/src/test/java/test/jenjinstudios/world/io/WorldFileReaderTest.java @@ -5,8 +5,8 @@ import com.jenjinstudios.world.World; import com.jenjinstudios.world.io.WorldFileReader; import com.jenjinstudios.world.math.Vector2D; -import org.junit.Assert; -import org.junit.Test; +import org.testng.Assert; +import org.testng.annotations.*; import java.io.InputStream; @@ -21,10 +21,10 @@ public class WorldFileReaderTest */ @Test public void testRead() throws Exception { - InputStream resourceAsStream = getClass().getResourceAsStream("/WorldFile01.xml"); + InputStream resourceAsStream = getClass().getResourceAsStream("/test/jenjinstudios/world/WorldFile01.xml"); WorldFileReader testReader = new WorldFileReader(resourceAsStream); World world = testReader.read(); - Location testLocation = world.getLocationForCoordinates(0, new Vector2D(Location.SIZE * 3, Location.SIZE * 3)); + Location testLocation = world.getLocationForCoordinates(0, new Vector2D(Location.SIZE, Location.SIZE)); LocationProperties testProperties = testLocation.getLocationProperties(); Assert.assertTrue("false".equals(testProperties.getProperty("walkable"))); } diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/io/WorldFileWriterTest.java b/jenjin-world-server/src/test/java/test/jenjinstudios/world/io/WorldFileWriterTest.java similarity index 94% rename from jenjin-server-world/src/test/java/test/jenjinstudios/world/io/WorldFileWriterTest.java rename to jenjin-world-server/src/test/java/test/jenjinstudios/world/io/WorldFileWriterTest.java index e845bd26..7795008c 100644 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/io/WorldFileWriterTest.java +++ b/jenjin-world-server/src/test/java/test/jenjinstudios/world/io/WorldFileWriterTest.java @@ -7,10 +7,8 @@ import com.jenjinstudios.world.io.WorldFileReader; import com.jenjinstudios.world.io.WorldFileWriter; import com.jenjinstudios.world.math.Vector2D; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.testng.annotations.*; +import org.testng.Assert; import java.io.File; import java.util.TreeMap; @@ -28,7 +26,7 @@ public class WorldFileWriterTest * Set up the tests. * @throws Exception If there's an exception. */ - @Before + @BeforeMethod public void setUp() throws Exception { worldFile = new File("WorldFileWriterTest.xml"); if(worldFile.exists() && worldFile.delete()) @@ -41,7 +39,7 @@ public void setUp() throws Exception { * Tear down the test. * @throws Exception If there's an exception. */ - @After + @AfterMethod public void tearDown() throws Exception { if(worldFile.exists() && worldFile.delete()) { diff --git a/jenjin-server-world/src/test/java/test/jenjinstudios/world/math/Vector2DTest.java b/jenjin-world-server/src/test/java/test/jenjinstudios/world/math/Vector2DTest.java similarity index 97% rename from jenjin-server-world/src/test/java/test/jenjinstudios/world/math/Vector2DTest.java rename to jenjin-world-server/src/test/java/test/jenjinstudios/world/math/Vector2DTest.java index cfe50126..17ca9cf8 100644 --- a/jenjin-server-world/src/test/java/test/jenjinstudios/world/math/Vector2DTest.java +++ b/jenjin-world-server/src/test/java/test/jenjinstudios/world/math/Vector2DTest.java @@ -1,8 +1,8 @@ package test.jenjinstudios.world.math; import com.jenjinstudios.world.math.Vector2D; -import org.junit.Assert; -import org.junit.Test; +import org.testng.Assert; +import org.testng.annotations.*; /** * Test the coordinates class. diff --git a/jenjin-world-server/src/test/resources/test/jenjinstudios/logger.properties b/jenjin-world-server/src/test/resources/test/jenjinstudios/logger.properties new file mode 100644 index 00000000..950c0c51 --- /dev/null +++ b/jenjin-world-server/src/test/resources/test/jenjinstudios/logger.properties @@ -0,0 +1,19 @@ +# suppress inspection "UnusedProperty" for whole file +# Logging +handlers = java.util.logging.ConsoleHandler + +com.jenjinstudios.handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler +com.jenjinstudios.level = ALL +com.jenjinstudios.useParentHandlers = FALSE + +test.jenjinstudios.handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler +test.jenjinstudios.level = ALL +test.jenjinstudios.useParentHandlers = FALSE + +# File Logging +java.util.logging.FileHandler.pattern = jenjin.log +java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.FileHandler.level = ALL + +# Console Logging +java.util.logging.ConsoleHandler.level = ALL \ No newline at end of file diff --git a/jenjin-world-server/src/test/resources/test/jenjinstudios/world/CompoundFile01.xml b/jenjin-world-server/src/test/resources/test/jenjinstudios/world/CompoundFile01.xml new file mode 100644 index 00000000..02b68707 --- /dev/null +++ b/jenjin-world-server/src/test/resources/test/jenjinstudios/world/CompoundFile01.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jenjin-world-server/src/test/resources/test/jenjinstudios/world/NPCFile01.xml b/jenjin-world-server/src/test/resources/test/jenjinstudios/world/NPCFile01.xml new file mode 100644 index 00000000..07608ae5 --- /dev/null +++ b/jenjin-world-server/src/test/resources/test/jenjinstudios/world/NPCFile01.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/jenjin-server-world/src/test/resources/WorldFile01.xml b/jenjin-world-server/src/test/resources/test/jenjinstudios/world/WorldFile01.xml similarity index 58% rename from jenjin-server-world/src/test/resources/WorldFile01.xml rename to jenjin-world-server/src/test/resources/test/jenjinstudios/world/WorldFile01.xml index 9d0c146d..196adccb 100644 --- a/jenjin-server-world/src/test/resources/WorldFile01.xml +++ b/jenjin-world-server/src/test/resources/test/jenjinstudios/world/WorldFile01.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/InvalidLocationException.java b/jenjin-world-utils/src/main/java/com/jenjinstudios/world/InvalidLocationException.java deleted file mode 100644 index 7d547f2d..00000000 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/InvalidLocationException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.jenjinstudios.world; - -import com.jenjinstudios.world.math.Vector2D; - -/** - * An {@code InvalidLocationException} is used to indicate that the supplied coordinates specify an invalid location. - * @author Caleb Brinkman - */ -public class InvalidLocationException extends Exception -{ - /** - * Construct a new InvalidLocationException for the given coordinates. - * @param coordinates The coordinates of the invalid location. - */ - public InvalidLocationException(Vector2D coordinates) { - super("Location does not exist at: " + coordinates); - } -} diff --git a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/Location.java b/jenjin-world-utils/src/main/java/com/jenjinstudios/world/Location.java deleted file mode 100644 index f3001446..00000000 --- a/jenjin-world-utils/src/main/java/com/jenjinstudios/world/Location.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.jenjinstudios.world; - -import java.util.*; - -/** - * Represents a location in the world's location grid. - * @author Caleb Brinkman - */ -public class Location -{ - /** The size, int units, of each location. */ - public static final int SIZE = 10; - /** The x coordinate of the location in it's zone's grid. */ - public final int X_COORDINATE; - /** The y coordinate of the location in it's zone's grid. */ - public final int Y_COORDINATE; - /** The objects residing in this location. */ - private final HashSet objects; - /** The locationProperties of this location. */ - private final LocationProperties locationProperties; - /** The locations visible from this one. */ - private LinkedList locationsVisibleFrom; - - /** - * Construct a new location at the given position in a zone grid. - * @param x The x coordinate of the zone grid. - * @param y The y coordinate of the zone grid. - */ - public Location(int x, int y) { - this(x, y, new LocationProperties()); - } - - /** - * Construct a location with the given position and locationProperties. - * @param x The x coordinate. - * @param y The y coordinate. - * @param locationProperties1 The locationProperties. - */ - public Location(int x, int y, LocationProperties locationProperties1) - { - X_COORDINATE = x; - Y_COORDINATE = y; - this.locationProperties = locationProperties1; - objects = new HashSet<>(); - locationsVisibleFrom = new LinkedList<>(); - } - - /** - * Get the locationProperties of this location. - * @return The locationProperties of this location. - */ - public LocationProperties getLocationProperties() { - return locationProperties; - } - - /** - * Get the objects residing in this location, as an array. - * @return An array containing all objects residing in this location. - */ - public Collection getObjects() { - return Collections.unmodifiableCollection(new ArrayList<>(objects)); - } - - /** - * Add the object to this location's object map. - * @param object The object to add. - */ - public void addObject(WorldObject object) { - objects.add(object); - } - - /** - * Remove an object from this location's object map. - * @param object The object to remove. - */ - public void removeObject(WorldObject object) { - objects.remove(object); - } - - @Override - public String toString() { - return "(" + X_COORDINATE + ", " + Y_COORDINATE + ")"; - } - - /** - * Set the locations visible from this location. - * @param visible The locations to be visible from this one. - */ - public void setLocationsVisibleFrom(List visible) - { - locationsVisibleFrom.addAll(visible); - } - - /** - * Get the locations visible from this one. - * @return The locations visible from this one. - */ - public LinkedList getLocationsVisibleFrom() { - return locationsVisibleFrom; - } -} diff --git a/settings.gradle b/settings.gradle index 57181e15..81a1ad6d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,9 @@ rootProject.name = 'Jenjin' -include ':jenjin-server', ':jenjin-client', ':jenjin-server-world', ':jenjin-client-world', ':jenjin-io', ':jenjin-world-utils' +include ':jenjin-core-server', ':jenjin-core-client', ':jenjin-world-server', ':jenjin-world-client', ':jenjin-core', ':jenjin-world-core' -project(':jenjin-server').projectDir = "$rootDir/jenjin-server" as File -project(':jenjin-client').projectDir = "$rootDir/jenjin-client" as File -project(':jenjin-server-world').projectDir = "$rootDir/jenjin-server-world" as File -project(':jenjin-client-world').projectDir = "$rootDir/jenjin-client-world" as File -project(':jenjin-io').projectDir = "$rootDir/jenjin-io" as File -project(':jenjin-world-utils').projectDir = "$rootDir/jenjin-world-utils" as File \ No newline at end of file +project(':jenjin-core-server').projectDir = "$rootDir/jenjin-core-server" as File +project(':jenjin-core-client').projectDir = "$rootDir/jenjin-core-client" as File +project(':jenjin-world-server').projectDir = "$rootDir/jenjin-world-server" as File +project(':jenjin-world-client').projectDir = "$rootDir/jenjin-world-client" as File +project(':jenjin-core').projectDir = "$rootDir/jenjin-core" as File +project(':jenjin-world-core').projectDir = "$rootDir/jenjin-world-core" as File \ No newline at end of file