diff --git a/.classpath b/.classpath
index 2ebdd55..cc65c5f 100644
--- a/.classpath
+++ b/.classpath
@@ -1,7 +1,7 @@
-
+
diff --git a/.gitignore b/.gitignore
index 548dc14..ee1c6e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,13 @@
.settings
-.gradle
+
+# IDE-based
+PSWGCommon.iml
+out
+.idea
+
+# Gradle
bin
build
+.gradle
+gradle
+gradlew*
diff --git a/build.gradle b/build.gradle
index 85fa01f..b6bade1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,12 @@
-apply plugin: 'java'
+plugins {
+ id 'java'
+ id 'idea'
+}
sourceSets {
main {
java {
- srcDirs = ['src']
- }
- resources {
- srcDirs = ['src']
+ srcDir 'src'
}
}
}
@@ -15,3 +15,7 @@ jar {
from sourceSets.main.allSource
archiveName = "PSWGCommon.jar"
}
+
+task wrapper(type: Wrapper) {
+ gradleVersion = "4.4"
+}
diff --git a/module-info.java b/module-info.java
new file mode 100644
index 0000000..759f9c9
--- /dev/null
+++ b/module-info.java
@@ -0,0 +1,3 @@
+module com.projectswg.launcher {
+ requires java.base;
+}
\ No newline at end of file
diff --git a/src/com/projectswg/common/control/IntentManager.java b/src/com/projectswg/common/control/IntentManager.java
index 008978c..d643d99 100644
--- a/src/com/projectswg/common/control/IntentManager.java
+++ b/src/com/projectswg/common/control/IntentManager.java
@@ -27,6 +27,9 @@
***********************************************************************************/
package com.projectswg.common.control;
+import com.projectswg.common.concurrency.PswgThreadPool;
+import com.projectswg.common.debug.Log;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -39,9 +42,6 @@ import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
-import com.projectswg.common.concurrency.PswgThreadPool;
-import com.projectswg.common.debug.Log;
-
public class IntentManager {
private static final AtomicReference INSTANCE = new AtomicReference<>(null);
@@ -70,6 +70,7 @@ public class IntentManager {
if (!initialized.getAndSet(false))
return;
processThreads.stop(true);
+ processThreads.awaitTermination(1000);
}
public int getIntentCount() {
diff --git a/src/com/projectswg/common/control/Manager.java b/src/com/projectswg/common/control/Manager.java
index 653abac..f4eede4 100644
--- a/src/com/projectswg/common/control/Manager.java
+++ b/src/com/projectswg/common/control/Manager.java
@@ -160,7 +160,9 @@ public abstract class Manager extends Service {
return;
}
children.add(s);
- s.setIntentManager(getIntentManager());
+ IntentManager manager = getIntentManager();
+ if (manager != null)
+ s.setIntentManager(manager);
}
}
diff --git a/src/com/projectswg/common/control/PrimaryManager.java b/src/com/projectswg/common/control/PrimaryManager.java
new file mode 100644
index 0000000..5ee4a3e
--- /dev/null
+++ b/src/com/projectswg/common/control/PrimaryManager.java
@@ -0,0 +1,73 @@
+/***********************************************************************************
+ * Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
+ * *
+ * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
+ * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
+ * Our goal is to create an emulator which will provide a server for players to *
+ * continue playing a game similar to the one they used to play. We are basing *
+ * it on the final publish of the game prior to end-game events. *
+ * *
+ * This file is part of Holocore. *
+ * *
+ * --------------------------------------------------------------------------------*
+ * *
+ * Holocore is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation, either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * Holocore is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Affero General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Affero General Public License *
+ * along with Holocore. If not, see . *
+ * *
+ ***********************************************************************************/
+package com.projectswg.common.control;
+
+public class PrimaryManager {
+
+ private final Manager manager;
+
+ public PrimaryManager() {
+ this(new DefaultManager());
+ }
+
+ public PrimaryManager(Manager manager) {
+ this.manager = manager;
+ }
+
+ public void addChildService(Service service) {
+ manager.addChildService(service);
+ }
+
+ public boolean initialize() {
+ return manager.initialize();
+ }
+
+ public boolean start() {
+ boolean ret = manager.start();
+ if (!ret)
+ terminate();
+ return ret;
+ }
+
+ public boolean isOperational() {
+ return manager.isOperational();
+ }
+
+ public boolean stop() {
+ return manager.stop();
+ }
+
+ public boolean terminate() {
+ return manager.terminate();
+ }
+
+ private static class DefaultManager extends Manager {
+
+ }
+
+}
diff --git a/src/com/projectswg/common/control/Service.java b/src/com/projectswg/common/control/Service.java
index 9c04568..0d15dc3 100644
--- a/src/com/projectswg/common/control/Service.java
+++ b/src/com/projectswg/common/control/Service.java
@@ -74,7 +74,9 @@ public abstract class Service {
* @return TRUE if termination was successful, FALSE otherwise
*/
public boolean terminate() {
- IntentManager.getInstance().terminate();
+ IntentManager im = IntentManager.getInstance();
+ if (im != null)
+ im.terminate();
return true;
}
diff --git a/src/com/projectswg/common/data/CRC.java b/src/com/projectswg/common/data/CRC.java
index d42c231..d93a355 100644
--- a/src/com/projectswg/common/data/CRC.java
+++ b/src/com/projectswg/common/data/CRC.java
@@ -1,39 +1,39 @@
/***********************************************************************************
-* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
-* *
-* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
-* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
-* Our goal is to create an emulator which will provide a server for players to *
-* continue playing a game similar to the one they used to play. We are basing *
-* it on the final publish of the game prior to end-game events. *
-* *
-* This file is part of Holocore. *
-* *
-* -------------------------------------------------------------------------------- *
-* *
-* Holocore is free software: you can redistribute it and/or modify *
-* it under the terms of the GNU Affero General Public License as *
-* published by the Free Software Foundation, either version 3 of the *
-* License, or (at your option) any later version. *
-* *
-* Holocore is distributed in the hope that it will be useful, *
-* but WITHOUT ANY WARRANTY; without even the implied warranty of *
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
-* GNU Affero General Public License for more details. *
-* *
-* You should have received a copy of the GNU Affero General Public License *
-* along with Holocore. If not, see . *
-* *
-***********************************************************************************/
+ * Copyright (c) 2017 /// Project SWG /// www.projectswg.com *
+ * *
+ * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
+ * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
+ * Our goal is to create an emulator which will provide a server for players to *
+ * continue playing a game similar to the one they used to play. We are basing *
+ * it on the final publish of the game prior to end-game events. *
+ * *
+ * This file is part of Holocore. *
+ * *
+ * --------------------------------------------------------------------------------*
+ * *
+ * Holocore is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation, either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * Holocore is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Affero General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Affero General Public License *
+ * along with Holocore. If not, see . *
+ * *
+ ***********************************************************************************/
package com.projectswg.common.data;
-import java.nio.charset.StandardCharsets;
-
import com.projectswg.common.encoding.Encodable;
import com.projectswg.common.network.NetBuffer;
import com.projectswg.common.network.NetBufferStream;
import com.projectswg.common.persistable.Persistable;
+import java.nio.charset.StandardCharsets;
+
public class CRC implements Encodable, Persistable {
private static final int CRC_TABLE[] = {
@@ -101,7 +101,7 @@ public class CRC implements Encodable, Persistable {
}
@Override
- public byte [] encode() {
+ public byte[] encode() {
NetBuffer buffer = NetBuffer.allocate(4);
buffer.addInt(crc);
return buffer.array();
@@ -127,45 +127,35 @@ public class CRC implements Encodable, Persistable {
public void read(NetBufferStream stream) {
crc = stream.getInt();
}
-
+
@Override
public String toString() {
return str;
}
-
+
@Override
public int hashCode() {
return this.crc;
}
-
+
@Override
public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (!(obj instanceof CRC)) {
- return false;
- }
-
- return crc == ((CRC) obj).crc;
- }
-
- public static int getCrc(String input) {
- return getCrc(input.getBytes(StandardCharsets.UTF_8));
- }
-
- public static int getCrc(byte [] data) {
- int crc = 0xFFFFFFFF;
- for (int i = 0; i < data.length; i++)
- crc = CRC_TABLE[((crc>>>24) ^ data[i]) & 0xFF] ^ (crc << 8);
- return ~crc;
+ return this == obj || (obj instanceof CRC && crc == ((CRC) obj).crc);
}
public static String getString(int crc) {
return CrcDatabase.getInstance().getString(crc);
}
+ public static int getCrc(String input) {
+ return getCrc(input.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public static int getCrc(byte[] data) {
+ int crc = 0xFFFFFFFF;
+ for (byte b : data)
+ crc = CRC_TABLE[((crc >>> 24) ^ b) & 0xFF] ^ (crc << 8);
+ return ~crc;
+ }
+
}
diff --git a/src/com/projectswg/common/data/Pair.java b/src/com/projectswg/common/data/Pair.java
new file mode 100644
index 0000000..87ffe15
--- /dev/null
+++ b/src/com/projectswg/common/data/Pair.java
@@ -0,0 +1,192 @@
+package com.projectswg.common.data;
+
+import java.util.Objects;
+
+import com.projectswg.common.debug.Assert;
+import com.projectswg.common.debug.Log;
+import com.projectswg.common.encoding.CachedEncode;
+import com.projectswg.common.encoding.Encodable;
+import com.projectswg.common.encoding.Encoder;
+import com.projectswg.common.encoding.StringType;
+import com.projectswg.common.network.NetBuffer;
+import com.projectswg.common.network.NetBufferStream;
+import com.projectswg.common.persistable.Persistable;
+
+public class Pair implements Encodable, Persistable {
+
+ private final StringType leftType;
+ private final StringType rightType;
+ private final Class leftClass;
+ private final Class rightClass;
+ private final CachedEncode cache;
+
+ private T left;
+ private S right;
+
+ private Pair(T left, S right, Class leftClass, Class rightClass, StringType leftType, StringType rightType) {
+ // Final checks - has to be either one or the other. These ensure other assumptions in the class will succeed
+ Assert.test((leftType == null && !(left instanceof String) && !leftClass.equals(String.class)) ^ (leftType != null && (left instanceof String) && leftClass.equals(String.class)));
+ Assert.test((rightType == null && !(right instanceof String) && !rightClass.equals(String.class)) ^ (rightType != null && (right instanceof String) && rightClass.equals(String.class)));
+ this.leftClass = leftClass;
+ this.rightClass = rightClass;
+ this.leftType = leftType;
+ this.rightType = rightType;
+ this.cache = new CachedEncode(this::encodeImpl);
+ setLeft(left);
+ setRight(right);
+ }
+
+ public T getLeft() {
+ return left;
+ }
+
+ public S getRight() {
+ return right;
+ }
+
+ public void setLeft(T val1) {
+ this.left = val1;
+ cache.clearCached();
+ }
+
+ public void setRight(S val2) {
+ this.right = val2;
+ cache.clearCached();
+ }
+
+ @Override
+ public void decode(NetBuffer data) {
+ setLeft(smartDecode(data, leftClass, leftType));
+ setRight(smartDecode(data, rightClass, rightType));
+ }
+
+ @Override
+ public byte[] encode() {
+ Objects.requireNonNull(left, "left is null in encode()");
+ Objects.requireNonNull(right, "right is null in encode()");
+ return cache.encode();
+ }
+
+ @Override
+ public int getLength() {
+ return cache.encode().length;
+ }
+
+ @Override
+ public void save(NetBufferStream stream) {
+ smartSave(stream, left, leftType);
+ smartSave(stream, right, rightType);
+ }
+
+ @Override
+ public void read(NetBufferStream stream) {
+ smartRead(stream, left, leftClass, leftType);
+ smartRead(stream, right, rightClass, rightType);
+ }
+
+ private byte [] encodeImpl() {
+ if (left == null || right == null)
+ return new byte[0];
+
+ byte [] left = smartEncode(this.left, leftType);
+ byte [] right = smartEncode(this.right, rightType);
+ byte [] combined = new byte[left.length + right.length];
+ System.arraycopy(left, 0, combined, 0, left.length);
+ System.arraycopy(right, 0, combined, left.length, right.length);
+ return combined;
+ }
+
+ @SuppressWarnings("unchecked") // should succeed based on checks in constructor
+ private static U smartDecode(NetBuffer data, Class klass, StringType strType) {
+ if (strType != null)
+ return (U) data.getString(strType);
+ return (U) data.getGeneric(klass);
+ }
+
+ private static byte [] smartEncode(Object obj, StringType strType) {
+ if (strType != null)
+ return Encoder.encode(obj, strType);
+ return Encoder.encode(obj);
+ }
+
+ @SuppressWarnings("unchecked") // should succeed based on checks in constructor
+ private static U smartRead(NetBufferStream data, U obj, Class klass, StringType strType) {
+ if (strType != null)
+ return (U) data.getString(strType);
+
+ // Try making our own persistable
+ if (obj == null && Persistable.class.isAssignableFrom(klass)) {
+ try {
+ obj = klass.getConstructor().newInstance();
+ } catch (Exception e) {
+ Log.e(e);
+ }
+ }
+
+ // If we succeeded, read it - if not, try grabbing some generic
+ if (obj instanceof Persistable) {
+ ((Persistable) obj).read(data);
+ } else {
+ obj = (U) data.getGeneric(klass);
+ }
+ return obj;
+ }
+
+ private static void smartSave(NetBufferStream data, Object obj, StringType strType) {
+ if (strType != null) {
+ data.addString((String) obj, strType);
+ } else if (obj instanceof Persistable) {
+ ((Persistable) obj).save(data);
+ } else {
+ data.write(Encoder.encode(obj));
+ }
+ }
+
+ @SuppressWarnings("unchecked") // it's pretty obvious T.getClass() should be Class
+ public static Pair createPair(T left, S right) {
+ Objects.requireNonNull(left, "Left cannot be null in this method! Instead call createPair(left, right, leftClass, rightClass)");
+ Objects.requireNonNull(right, "Right cannot be null in this method! Instead call createPair(left, right, leftClass, rightClass)");
+ return createPair(left, right, (Class) left.getClass(), (Class) right.getClass());
+ }
+
+ public static Pair createPair(T left, S right, Class leftClass, Class rightClass) {
+ Assert.test(!(left instanceof String));
+ Assert.test(!(right instanceof String));
+ return new Pair<>(left, right, leftClass, rightClass, null, null);
+ }
+
+ @SuppressWarnings("unchecked") // it's pretty obvious T.getClass() should be Class
+ public static Pair createPair(String left, T right, StringType type) {
+ Objects.requireNonNull(right, "Right cannot be null in this method! Instead call createPair(left, right, type, rightClass)");
+ return createPair(left, right, type, (Class) right.getClass());
+ }
+
+ public static Pair createPair(String left, T right, StringType type, Class rightClass) {
+ Assert.test(!(right instanceof String));
+ return new Pair<>(left, right, String.class, rightClass, type, null);
+ }
+
+ @SuppressWarnings("unchecked") // it's pretty obvious T.getClass() should be Class
+ public static Pair createPair(T left, String right, StringType type) {
+ Objects.requireNonNull(left, "Left cannot be null in this method! Instead call createPair(left, right, leftClass, type)");
+ return createPair(left, right, (Class) left.getClass(), type);
+ }
+
+ public static Pair createPair(T left, String right, Class leftClass, StringType type) {
+ Assert.test(!(left instanceof String));
+ return new Pair<>(left, right, leftClass, String.class, null, type);
+ }
+
+ /**
+ * Creates a pair with two strings
+ * @param left the left string, can be null
+ * @param right the right string, can be null
+ * @param leftType the type of the left string: ASCII/UNICODE
+ * @param rightType the type of the right string: ASCII/UNICODE
+ * @return the corresponding pair
+ */
+ public static Pair createPair(String left, String right, StringType leftType, StringType rightType) {
+ return new Pair<>(left, right, String.class, String.class, leftType, rightType);
+ }
+
+}
diff --git a/src/com/projectswg/common/data/radial/RadialItem.java b/src/com/projectswg/common/data/radial/RadialItem.java
index 6445ad2..7225fce 100644
--- a/src/com/projectswg/common/data/radial/RadialItem.java
+++ b/src/com/projectswg/common/data/radial/RadialItem.java
@@ -349,7 +349,8 @@ public enum RadialItem {
BIO_LINK (""),
LANDMINE_DISARM (""),
LANDMINE_REVERSE_TRIGGER (""),
- REWARD_TRADE_IN ("");
+ REWARD_TRADE_IN (""),
+ TRANSFER_CREDITS_TO_BANK_ACCOUNT ("@space/space_loot:use_credit_chip");
private static final EnumLookup LOOKUP = new EnumLookup<>(RadialItem.class, RadialItem::getId);
diff --git a/src/com/projectswg/common/debug/ThreadPrinter.java b/src/com/projectswg/common/debug/ThreadPrinter.java
new file mode 100644
index 0000000..c7397ff
--- /dev/null
+++ b/src/com/projectswg/common/debug/ThreadPrinter.java
@@ -0,0 +1,61 @@
+/***********************************************************************************
+ * Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
+ * *
+ * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
+ * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
+ * Our goal is to create an emulator which will provide a server for players to *
+ * continue playing a game similar to the one they used to play. We are basing *
+ * it on the final publish of the game prior to end-game events. *
+ * *
+ * This file is part of Holocore. *
+ * *
+ * --------------------------------------------------------------------------------*
+ * *
+ * Holocore is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation, either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * Holocore is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Affero General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Affero General Public License *
+ * along with Holocore. If not, see . *
+ * *
+ ***********************************************************************************/
+package com.projectswg.common.debug;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ThreadPrinter {
+
+ public static void printActiveThreads() {
+ ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
+ int threadCount = threadGroup.activeCount();
+ Thread[] threadsRaw = new Thread[threadCount];
+ threadCount = threadGroup.enumerate(threadsRaw);
+ List threads = Arrays.stream(threadsRaw, 0, threadCount).filter(t -> t.getState() != Thread.State.TERMINATED).collect(Collectors.toList());
+ int maxLength = threads.stream().mapToInt(t -> t.getName().length()).max().orElse(4);
+ if (maxLength < 4)
+ maxLength = 4;
+
+ Log.w("Active Threads: %d", threads.size());
+ Log.w("+-%s---%s-+", createRepeatingDash(maxLength), createRepeatingDash(13));
+ Log.w("| %-" + maxLength + "s | %-13s |", "Name", "State");
+ Log.w("+-%s-+-%s-+", createRepeatingDash(maxLength), createRepeatingDash(13));
+ for (Thread t : threads) {
+ Log.w("| %-" + maxLength + "s | %-13s |", t.getName(), t.getState());
+ }
+ Log.w("+-%s---%s-+", createRepeatingDash(maxLength), createRepeatingDash(13));
+ }
+
+ private static String createRepeatingDash(int count) {
+ return String.join("", Collections.nCopies(count, "-"));
+ }
+
+}
diff --git a/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java b/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java
index 2869a5b..d2dfdaf 100644
--- a/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java
+++ b/src/com/projectswg/common/debug/log_wrapper/FileLogWrapper.java
@@ -18,11 +18,6 @@ public class FileLogWrapper implements LogWrapper {
writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8));
}
- @Override
- protected void finalize() throws IOException {
- writer.close();
- }
-
@Override
public void onLog(LogLevel level, String str) {
try {
diff --git a/src/com/projectswg/common/network/NetBuffer.java b/src/com/projectswg/common/network/NetBuffer.java
index 39c30ee..aa0d102 100644
--- a/src/com/projectswg/common/network/NetBuffer.java
+++ b/src/com/projectswg/common/network/NetBuffer.java
@@ -27,6 +27,7 @@
***********************************************************************************/
package com.projectswg.common.network;
+import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
@@ -208,7 +209,7 @@ public class NetBuffer {
}
public boolean getBoolean() {
- return getByte() == 1 ? true : false;
+ return getByte() == 1;
}
public String getAscii() {
@@ -298,16 +299,16 @@ public class NetBuffer {
return booleans;
}
- public Object getGeneric(Class type) {
+ public Object getGeneric(Class> type) {
if (Encodable.class.isAssignableFrom(type)) {
- T instance = null;
+ Object instance = null;
try {
- instance = type.newInstance();
+ instance = type.getConstructor().newInstance();
((Encodable) instance).decode(this);
- } catch (InstantiationException | IllegalAccessException e) {
+ } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
Log.e(e);
}
-
+
return instance;
} else if (Integer.class.isAssignableFrom(type) || Integer.TYPE.isAssignableFrom(type))
return getInt();
@@ -336,11 +337,11 @@ public class NetBuffer {
try {
for (int i = 0; i < size; i++) {
- T instance = type.newInstance();
+ T instance = type.getConstructor().newInstance();
instance.decode(this);
list.add(instance);
}
- } catch (InstantiationException | IllegalAccessException e) {
+ } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
Log.e(e);
}
@@ -383,9 +384,9 @@ public class NetBuffer {
public T getEncodable(Class type) {
T instance = null;
try {
- instance = type.newInstance();
+ instance = type.getConstructor().newInstance();
instance.decode(this);
- } catch (InstantiationException | IllegalAccessException e) {
+ } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
Log.e(e);
}
diff --git a/src/com/projectswg/common/network/NetBufferStream.java b/src/com/projectswg/common/network/NetBufferStream.java
index 376a1a8..d35b0f1 100644
--- a/src/com/projectswg/common/network/NetBufferStream.java
+++ b/src/com/projectswg/common/network/NetBufferStream.java
@@ -34,6 +34,8 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import com.projectswg.common.encoding.StringType;
+
public class NetBufferStream extends OutputStream {
private final Object expansionMutex;
@@ -189,6 +191,23 @@ public class NetBufferStream extends OutputStream {
}
}
+ public Object getGeneric(Class> type) {
+ synchronized (bufferMutex) {
+ return buffer.getGeneric(type);
+ }
+ }
+
+ public String getString(StringType type) {
+ switch (type) {
+ case ASCII:
+ return getAscii();
+ case UNICODE:
+ return getUnicode();
+ default:
+ throw new IllegalArgumentException("Unknown string type: " + type);
+ }
+ }
+
public String getAscii() {
synchronized (bufferMutex) {
return buffer.getAscii();
@@ -261,6 +280,12 @@ public class NetBufferStream extends OutputStream {
}
}
+ public byte[] getArrayLarge() {
+ synchronized (bufferMutex) {
+ return buffer.getArrayLarge();
+ }
+ }
+
public byte[] getArray(int size) {
synchronized (bufferMutex) {
return buffer.getArray(size);
@@ -281,6 +306,19 @@ public class NetBufferStream extends OutputStream {
}
}
+ public void addString(String s, StringType type) {
+ switch (type) {
+ case ASCII:
+ addAscii(s);
+ break;
+ case UNICODE:
+ addUnicode(s);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown string type: " + type);
+ }
+ }
+
public void addAscii(String s) {
ensureCapacity(size+2+s.length());
synchronized (bufferMutex) {
diff --git a/src/com/projectswg/common/network/SecureSocketFactory.java b/src/com/projectswg/common/network/SecureSocketFactory.java
new file mode 100644
index 0000000..5cdfd71
--- /dev/null
+++ b/src/com/projectswg/common/network/SecureSocketFactory.java
@@ -0,0 +1,140 @@
+package com.projectswg.common.network;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketImpl;
+import java.net.SocketImplFactory;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+public class SecureSocketFactory extends SSLSocketFactory implements SocketImplFactory {
+
+ private final AtomicBoolean loaded;
+ private SSLSocketFactory sslSocketFactory;
+
+ public SecureSocketFactory() {
+ this.loaded = new AtomicBoolean(false);
+ this.sslSocketFactory = null;
+ }
+
+ @Override
+ public Socket createSocket(Socket arg0, InputStream arg1, boolean arg2) throws IOException {
+ return sslSocketFactory.createSocket(arg0, arg1, arg2);
+ }
+
+ @Override
+ public Socket createSocket(Socket arg0, String arg1, int arg2, boolean arg3) throws IOException {
+ return sslSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+
+ @Override
+ public Socket createSocket(String arg0, int arg1) throws IOException, UnknownHostException {
+ return sslSocketFactory.createSocket(arg0, arg1);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress arg0, int arg1) throws IOException {
+ return sslSocketFactory.createSocket(arg0, arg1);
+ }
+
+ @Override
+ public Socket createSocket(String arg0, int arg1, InetAddress arg2, int arg3) throws IOException, UnknownHostException {
+ return sslSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress arg0, int arg1, InetAddress arg2, int arg3) throws IOException {
+ return sslSocketFactory.createSocket(arg0, arg1, arg2, arg3);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return sslSocketFactory.equals(obj);
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return sslSocketFactory.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return sslSocketFactory.getSupportedCipherSuites();
+ }
+
+ @Override
+ public int hashCode() {
+ return sslSocketFactory.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return sslSocketFactory.toString();
+ }
+
+ @Override
+ public SocketImpl createSocketImpl() {
+ return null;
+ }
+
+ /**
+ * Loads the encryption mechanism
+ * @param keystoreFile the keystore file
+ * @param password the password for the keystore
+ * @throws KeyStoreException if KeyManagerFactory.init or TrustManagerFactory.init fails
+ * @throws NoSuchAlgorithmException if the algorithm for the keystore or key manager could not be found
+ * @throws CertificateException if any of the certificates in the keystore could not be loaded
+ * @throws FileNotFoundException if the keystore file does not exist
+ * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given, or if the given password was incorrect. If the error is due to a wrong password, the cause of the IOException should be an UnrecoverableKeyException
+ * @throws KeyManagementException if SSLContext.init fails
+ * @throws UnrecoverableKeyException if the key cannot be recovered (e.g. the given password is wrong).
+ */
+ public void load(File keystoreFile, char [] password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, KeyManagementException, UnrecoverableKeyException {
+ // First initialize the key and trust material
+ KeyStore ksKeys = KeyStore.getInstance("JKS");
+ ksKeys.load(new FileInputStream(keystoreFile), password);
+
+ // KeyManagers decide which key material to use
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+ kmf.init(ksKeys, password);
+
+ // TrustManagers decide whether to allow connections
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
+ tmf.init(ksKeys);
+
+ // Used to create the
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
+
+ this.sslSocketFactory = sslContext.getSocketFactory();
+ loaded.set(true);
+ }
+
+ public boolean isLoaded() {
+ return loaded.get();
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ if (!loaded.get())
+ throw new IllegalStateException("SecureSocketFactory hasn't been loaded yet!");
+ return sslSocketFactory.createSocket();
+ }
+
+}
diff --git a/src/com/projectswg/common/network/TCPSecureSocket.java b/src/com/projectswg/common/network/TCPSecureSocket.java
new file mode 100644
index 0000000..1c816e8
--- /dev/null
+++ b/src/com/projectswg/common/network/TCPSecureSocket.java
@@ -0,0 +1,44 @@
+package com.projectswg.common.network;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+
+public class TCPSecureSocket extends TCPSocket {
+
+ private final SecureSocketFactory socketFactory;
+
+ public TCPSecureSocket(InetSocketAddress address, int bufferSize) {
+ super(address, bufferSize);
+ this.socketFactory = new SecureSocketFactory();
+ }
+
+ /**
+ * Sets up the encryption mechanism
+ * @param keystoreFile the keystore file
+ * @param password the password for the keystore
+ * @throws KeyStoreException if KeyManagerFactory.init or TrustManagerFactory.init fails
+ * @throws NoSuchAlgorithmException if the algorithm for the keystore or key manager could not be found
+ * @throws CertificateException if any of the certificates in the keystore could not be loaded
+ * @throws FileNotFoundException if the keystore file does not exist
+ * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given, or if the given password was incorrect. If the error is due to a wrong password, the cause of the IOException should be an UnrecoverableKeyException
+ * @throws KeyManagementException if SSLContext.init fails
+ * @throws UnrecoverableKeyException if the key cannot be recovered (e.g. the given password is wrong).
+ */
+ public void setupEncryption(File keystoreFile, char [] password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, KeyManagementException, UnrecoverableKeyException {
+ socketFactory.load(keystoreFile, password);
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return socketFactory.createSocket();
+ }
+
+}
diff --git a/src/com/projectswg/common/network/TCPServer.java b/src/com/projectswg/common/network/TCPServer.java
index 304b4a8..53f1091 100644
--- a/src/com/projectswg/common/network/TCPServer.java
+++ b/src/com/projectswg/common/network/TCPServer.java
@@ -1,77 +1,82 @@
/***********************************************************************************
-* Copyright (c) 2015 /// Project SWG /// www.projectswg.com *
-* *
-* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
-* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
-* Our goal is to create an emulator which will provide a server for players to *
-* continue playing a game similar to the one they used to play. We are basing *
-* it on the final publish of the game prior to end-game events. *
-* *
-* This file is part of Holocore. *
-* *
-* -------------------------------------------------------------------------------- *
-* *
-* Holocore is free software: you can redistribute it and/or modify *
-* it under the terms of the GNU Affero General Public License as *
-* published by the Free Software Foundation, either version 3 of the *
-* License, or (at your option) any later version. *
-* *
-* Holocore is distributed in the hope that it will be useful, *
-* but WITHOUT ANY WARRANTY; without even the implied warranty of *
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
-* GNU Affero General Public License for more details. *
-* *
-* You should have received a copy of the GNU Affero General Public License *
-* along with Holocore. If not, see . *
-* *
-***********************************************************************************/
+ * Copyright (c) 2017 /// Project SWG /// www.projectswg.com *
+ * *
+ * ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
+ * July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
+ * Our goal is to create an emulator which will provide a server for players to *
+ * continue playing a game similar to the one they used to play. We are basing *
+ * it on the final publish of the game prior to end-game events. *
+ * *
+ * This file is part of Holocore. *
+ * *
+ * --------------------------------------------------------------------------------*
+ * *
+ * Holocore is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU Affero General Public License as *
+ * published by the Free Software Foundation, either version 3 of the *
+ * License, or (at your option) any later version. *
+ * *
+ * Holocore is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU Affero General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU Affero General Public License *
+ * along with Holocore. If not, see . *
+ * *
+ ***********************************************************************************/
package com.projectswg.common.network;
+import com.projectswg.common.concurrency.PswgBasicThread;
+import com.projectswg.common.concurrency.PswgThreadPool;
+import com.projectswg.common.debug.Log;
+
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
-import java.nio.channels.ClosedByInterruptException;
-import java.nio.channels.ClosedChannelException;
-import java.nio.channels.SelectableChannel;
-import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
-import java.nio.channels.ServerSocketChannel;
-import java.nio.channels.SocketChannel;
-import java.util.HashMap;
-import java.util.Locale;
+import java.nio.channels.*;
import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
-import com.projectswg.common.callback.CallbackManager;
-import com.projectswg.common.debug.Assert;
-import com.projectswg.common.debug.Log;
-
-public class TCPServer {
+public class TCPServer {
- private final CallbackManager callbackManager;
- private final Map sockets;
+ private final PswgThreadPool callbackThread;
+ private final Map channels;
+ private final Map sessionIdToChannel;
+
+ private final PswgBasicThread listener;
private final AtomicBoolean running;
- private final InetAddress addr;
- private final int port;
- private final int bufferSize;
- private final TCPServerListener listener;
+ private final InetSocketAddress addr;
+ private final Function sessionCreator;
private ServerSocketChannel channel;
- public TCPServer(int port, int bufferSize) {
- this(null, port, bufferSize);
+ private final ByteBuffer buffer;
+ private final ByteArrayOutputStream bufferStream;
+ private final WritableByteChannel byteBufferChannel;
+
+ public TCPServer(int port, int bufferSize, Function sessionCreator) {
+ this(new InetSocketAddress((InetAddress) null, port), bufferSize, sessionCreator);
}
- public TCPServer(InetAddress addr, int port, int bufferSize) {
- this.callbackManager = new CallbackManager<>("tcpserver-"+port, 1);
- this.sockets = new HashMap<>();
+ public TCPServer(InetSocketAddress addr, int bufferSize, Function sessionCreator) {
+ this.callbackThread = new PswgThreadPool(false, 1, "tcpserver-" + addr.getPort());
+ this.channels = new ConcurrentHashMap<>();
+ this.sessionIdToChannel = new ConcurrentHashMap<>();
+ this.listener = new PswgBasicThread("tcpserver-listener-" + addr.getPort(), this::runListener);
this.running = new AtomicBoolean(false);
this.addr = addr;
- this.port = port;
- this.bufferSize = bufferSize;
this.channel = null;
- listener = new TCPServerListener();
+ this.sessionCreator = sessionCreator;
+ this.buffer = ByteBuffer.allocateDirect(bufferSize);
+ this.bufferStream = new ByteArrayOutputStream(bufferSize);
+ this.byteBufferChannel = Channels.newChannel(bufferStream);
}
public int getPort() {
@@ -79,190 +84,222 @@ public class TCPServer {
}
public void bind() throws IOException {
- if (running.getAndSet(true)) {
- Assert.fail();
+ assert !running.get() : "TCPServer is already running";
+ if (running.getAndSet(true))
return;
- }
- callbackManager.start();
+ callbackThread.start();
channel = ServerSocketChannel.open();
- channel.socket().bind(new InetSocketAddress(addr, port));
+ channel.bind(addr, 50);
channel.configureBlocking(false);
listener.start();
}
- public boolean disconnect(SocketAddress sock) {
- Assert.notNull(sock);
- synchronized (sockets) {
- SocketChannel sc = sockets.get(sock);
- if (sc == null)
- return true;
- sockets.remove(sock);
- try {
- sc.close();
- callbackManager.callOnEach((callback) -> callback.onConnectionDisconnect(sc, sock));
- return true;
- } catch (IOException e) {
- Log.e(e);
- return false;
+ public void disconnect(long sessionId) {
+ T session = sessionIdToChannel.remove(sessionId);
+ if (session == null) {
+ Log.w("TCPServer - unknown session id in disconnect: %d", sessionId);
+ return;
+ }
+
+ disconnect(session.getChannel());
+ }
+
+ public void disconnect(T session) {
+ disconnect(Objects.requireNonNull(session, "session").getChannel());
+ }
+
+ public void disconnect(SocketChannel sc) {
+ T session = channels.remove(sc);
+ if (session == null) {
+ Log.w("TCPServer - unknown channel in disconnect: %d", sc);
+ return;
+ }
+ sessionIdToChannel.remove(session.getSessionId());
+
+ session.close();
+ callbackThread.execute(session::onDisconnected);
+ }
+
+ public T getSession(long sessionId) {
+ return sessionIdToChannel.get(sessionId);
+ }
+
+ public T getSession(SocketChannel sc) {
+ return channels.get(sc);
+ }
+
+ public void close() {
+ assert running.get() : "TCPServer isn't running";
+ if (!running.getAndSet(false))
+ return;
+ callbackThread.stop(false);
+ listener.stop(true);
+ safeClose(channel);
+ }
+
+ private void runListener() {
+ try (Selector selector = Selector.open()) {
+ channel.register(selector, SelectionKey.OP_ACCEPT);
+ while (running.get()) {
+ selector.select();
+ accept(selector);
+ selector.selectedKeys().forEach(this::read);
}
- }
- }
-
- private boolean disconnect(SocketChannel sc) {
- try {
- return disconnect(sc.getRemoteAddress());
- } catch (IOException e) {
- return false;
- }
- }
-
- public boolean close() {
- if (!running.getAndSet(false)) {
- Assert.fail();
- return false;
- }
- callbackManager.stop();
- listener.stop();
- try {
- channel.close();
- return true;
} catch (IOException e) {
Log.e(e);
}
- return false;
}
- public SocketChannel getChannel(SocketAddress sock) {
- synchronized (sockets) {
- return sockets.get(sock);
- }
- }
-
- public void setCallback(TCPCallback callback) {
- callbackManager.setCallback(callback);
- }
-
- public interface TCPCallback {
- void onIncomingConnection(SocketChannel s, SocketAddress addr);
- void onConnectionDisconnect(SocketChannel s, SocketAddress addr);
- void onIncomingData(SocketChannel s, SocketAddress addr, byte [] data);
- }
-
- private class TCPServerListener implements Runnable {
-
- private final ByteBuffer buffer;
- private Thread thread;
- private boolean running;
-
- public TCPServerListener() {
- buffer = ByteBuffer.allocateDirect(bufferSize);
- running = false;
- thread = null;
- }
-
- public void start() {
- running = true;
- thread = new Thread(this, "TCPServerListener-" + channel.socket().getLocalPort());
- thread.start();
- }
-
- public void stop() {
- running = false;
- if (thread != null)
- thread.interrupt();
- thread = null;
- }
-
- @Override
- public void run() {
- try (Selector selector = setupSelector()) {
- while (running) {
- try {
- selector.select();
- processSelectionKeys(selector);
- } catch (Exception e) {
- Log.e(e);
- try {
- Thread.sleep(100);
- } catch (InterruptedException e1) {
- break;
- }
- }
- }
- } catch (IOException e) {
- Log.e(e);
+ private void accept(Selector selector) {
+ try {
+ while (channel.isOpen()) {
+ SocketChannel sc = channel.accept();
+ if (sc == null)
+ return;
+ sc.configureBlocking(false);
+ sc.register(selector, SelectionKey.OP_READ);
+ acceptConnection(sc);
}
+ } catch (ClosedChannelException e) {
+ // Ignored
+ } catch (Throwable t) {
+ Log.w("TCPServer - IOException in accept(): %s", t.getMessage());
}
-
- private Selector setupSelector() throws IOException {
- Selector selector = Selector.open();
- channel.register(selector, SelectionKey.OP_ACCEPT);
- return selector;
+ }
+
+ private void acceptConnection(SocketChannel sc) {
+ T session = sessionCreator.apply(sc);
+ if (session == null) {
+ Log.w("Session creator for TCPServer-%d created a null session!", addr.getPort());
+ safeClose(sc);
+ return;
}
-
- private void processSelectionKeys(Selector selector) throws ClosedChannelException {
- for (SelectionKey key : selector.selectedKeys()) {
- if (!key.isValid())
- continue;
- if (key.isAcceptable()) {
- accept(selector);
- } else if (key.isReadable()) {
- SelectableChannel selectable = key.channel();
- if (selectable instanceof SocketChannel) {
- boolean canRead = true;
- while (canRead)
- canRead = read(key, (SocketChannel) selectable);
- }
- }
- }
+ if (session.getChannel() != sc) {
+ Log.w("Session creator for TCPServer-%d created a session with an invalid channel!", addr.getPort());
+ safeClose(sc);
+ return;
}
-
- private void accept(Selector selector) {
- try {
- while (channel.isOpen()) {
- SocketChannel sc = channel.accept();
- if (sc == null)
- break;
- SocketChannel old = sockets.get(sc.getRemoteAddress());
- if (old != null)
- disconnect(old);
- sockets.put(sc.getRemoteAddress(), sc);
- sc.configureBlocking(false);
- sc.register(selector, SelectionKey.OP_READ);
- SocketAddress addr = sc.getRemoteAddress();
- callbackManager.callOnEach((callback) -> callback.onIncomingConnection(sc, addr));
- }
- } catch (IOException e) {
- Log.e(e);
- }
+ channels.put(sc, session);
+ sessionIdToChannel.put(session.getSessionId(), session);
+ callbackThread.execute(session::onConnected);
+ }
+
+ private void read(SelectionKey key) {
+ SelectableChannel selectableChannel = key.channel();
+ if (selectableChannel == channel)
+ return;
+ SocketChannel sc = (SocketChannel) selectableChannel;
+ T session = getSession(sc);
+ if (session == null || !sc.isConnected()) {
+ invalidate(sc, key);
+ return;
}
-
- private boolean read(SelectionKey key, SocketChannel s) {
- try {
- buffer.position(0);
- buffer.limit(bufferSize);
- int n = s.read(buffer);
+ try {
+ bufferStream.reset();
+ int n = 1;
+ while (n > 0) {
+ buffer.clear();
+ n = sc.read(buffer);
buffer.flip();
- if (n < 0) {
- key.cancel();
- disconnect(s);
- } else if (n > 0) {
- ByteBuffer smaller = ByteBuffer.allocate(n);
- smaller.put(buffer);
- SocketAddress addr = s.getRemoteAddress();
- callbackManager.callOnEach((callback) -> callback.onIncomingData(s, addr, smaller.array()));
- return true;
- }
- } catch (IOException e) {
- if (e.getMessage() != null && e.getMessage().toLowerCase(Locale.US).contains("connection reset"))
- Log.e("Connection Reset with %s", s.socket().getRemoteSocketAddress());
- else if (!(e instanceof ClosedByInterruptException))
- Log.e(e);
- key.cancel();
- disconnect(s);
+ byteBufferChannel.write(buffer);
}
- return false;
+ if (bufferStream.size() > 0) {
+ byte[] data = bufferStream.toByteArray();
+ callbackThread.execute(() -> session.onIncomingData(data));
+ }
+ if (n < 0) {
+ invalidate(sc, key);
+ }
+ } catch (ClosedChannelException e) {
+ // Ignored
+ } catch (Throwable t) {
+ Log.w("TCPServer - IOException in read(): %s", t.getMessage());
+ invalidate(sc, key);
}
}
+ private void invalidate(SocketChannel sc, SelectionKey key) {
+ key.cancel();
+ disconnect(sc);
+ }
+
+ private static void safeClose(Channel c) {
+ try {
+ c.close();
+ } catch (IOException e) {
+ // Ignored - as long as it's closed
+ }
+ }
+
+ public abstract static class TCPSession {
+
+ private static final AtomicLong GLOBAL_SESSION_ID = new AtomicLong(0);
+
+ private final SocketChannel sc;
+ private final SocketAddress addr;
+ private final long sessionId;
+
+ protected TCPSession(SocketChannel sc) {
+ this.sc = Objects.requireNonNull(sc, "socket");
+ this.sessionId = GLOBAL_SESSION_ID.incrementAndGet();
+
+ SocketAddress addr;
+ try {
+ addr = sc.getRemoteAddress();
+ } catch (IOException e) {
+ addr = null;
+ }
+ this.addr = addr;
+ }
+
+ protected void onConnected() {
+
+ }
+
+ protected void onDisconnected() {
+
+ }
+
+ /**
+ * Returns a globally unique session id for this particular connection
+ * @return the unique session id
+ */
+ protected final long getSessionId() {
+ return sessionId;
+ }
+
+ /**
+ * Returns the socket channel associated with this session
+ * @return the socket channel
+ */
+ protected final SocketChannel getChannel() {
+ return sc;
+ }
+
+ /**
+ * Returns the remote address that this socket is/was connected to
+ * @return the remote socket address
+ */
+ @SuppressWarnings("unused") // open for subclass to use
+ protected final SocketAddress getRemoteAddress() {
+ return addr;
+ }
+
+ @SuppressWarnings("unused") // open for subclass to use
+ protected void writeToChannel(ByteBuffer data) throws IOException {
+ sc.write(data);
+ }
+
+ @SuppressWarnings("unused") // open for subclass to use
+ protected void writeToChannel(byte [] data) throws IOException {
+ sc.write(ByteBuffer.wrap(data));
+ }
+
+ protected void close() {
+ safeClose(sc);
+ }
+
+ protected abstract void onIncomingData(byte[] data);
+ }
+
}
diff --git a/src/com/projectswg/common/network/TCPSocket.java b/src/com/projectswg/common/network/TCPSocket.java
index 51e8bfa..359021e 100644
--- a/src/com/projectswg/common/network/TCPSocket.java
+++ b/src/com/projectswg/common/network/TCPSocket.java
@@ -29,10 +29,13 @@ package com.projectswg.common.network;
import java.io.EOFException;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.net.InetSocketAddress;
+import java.net.Socket;
import java.nio.ByteBuffer;
-import java.nio.channels.SocketChannel;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import com.projectswg.common.callback.CallbackManager;
import com.projectswg.common.concurrency.Delay;
@@ -43,16 +46,33 @@ public class TCPSocket {
private final CallbackManager callbackManager;
private final TCPSocketListener listener;
- private final InetSocketAddress address;
+ private final AtomicReference address;
+ private final AtomicReference state;
+ private final Object stateMutex;
private final int bufferSize;
- private SocketChannel socket;
+ private Socket socket;
+ private InputStream socketInputStream;
+ private OutputStream socketOutputStream;
public TCPSocket(InetSocketAddress address, int bufferSize) {
this.callbackManager = new CallbackManager<>("tcpsocket-"+address, 1);
this.listener = new TCPSocketListener();
- this.address = address;
+ this.address = new AtomicReference<>(address);
+ this.state = new AtomicReference<>(SocketState.CLOSED);
+ this.stateMutex = new Object();
this.bufferSize = bufferSize;
+
this.socket = null;
+ this.socketInputStream = null;
+ this.socketOutputStream = null;
+ }
+
+ public TCPSocket(int bufferSize) {
+ this(null, bufferSize);
+ }
+
+ public TCPSocket() {
+ this(1024);
}
public int getBufferSize() {
@@ -60,19 +80,27 @@ public class TCPSocket {
}
public InetSocketAddress getRemoteAddress() {
- return address;
+ return address.get();
}
- public SocketChannel getSocket() {
+ public void setRemoteAddress(InetSocketAddress address) {
+ this.address.set(address);
+ }
+
+ public Socket getSocket() {
return socket;
}
public boolean isAlive() {
- return socket != null && listener.isAlive();
+ synchronized (stateLock()) {
+ return socket != null && listener.isAlive();
+ }
}
public boolean isConnected() {
- return socket != null && socket.isConnected();
+ synchronized (stateLock()) {
+ return socket != null && socket.isConnected();
+ }
}
public void setCallback(TCPSocketCallback callback) {
@@ -83,57 +111,114 @@ public class TCPSocket {
callbackManager.clearCallbacks();
}
- public boolean connect() {
- Assert.isNull(socket, "Socket must be null! Cannot connect twice!");
- Assert.test(!listener.isAlive(), "Listener must not be alive! Cannot connect twice!");
- try {
- callbackManager.start();
- socket = SocketChannel.open(address);
- if (socket.finishConnect()) {
- listener.start();
- callbackManager.callOnEach((callback) -> callback.onConnected(this));
- return true;
- }
- } catch (IOException e) {
- Log.e(e);
+ public void createConnection() throws IOException {
+ synchronized (stateLock()) {
+ checkAndSetState(SocketState.CLOSED, SocketState.CREATED);
+ socket = createSocket();
}
- socket = null;
- callbackManager.stop();
- return false;
+ }
+
+ public void startConnection() throws IOException {
+ synchronized (stateLock()) {
+ try {
+ checkAndSetState(SocketState.CREATED, SocketState.CONNECTING);
+ socket.connect(getRemoteAddress());
+ socketInputStream = socket.getInputStream();
+ socketOutputStream = socket.getOutputStream();
+ } catch (IOException e) {
+ checkAndSetState(SocketState.CONNECTING, SocketState.CLOSED);
+ socket = null;
+ socketInputStream = null;
+ socketOutputStream = null;
+ throw e;
+ }
+
+ callbackManager.start();
+ listener.start();
+ checkAndSetState(SocketState.CONNECTING, SocketState.CONNECTED);
+ callbackManager.callOnEach((callback) -> callback.onConnected(this));
+ }
+ }
+
+ public void connect() throws IOException {
+ createConnection();
+ startConnection();
}
public boolean disconnect() {
- if (socket == null)
- return true;
- try {
- socket.close();
- if (listener.isAlive()) {
- listener.stop();
- listener.awaitTermination();
+ synchronized (stateLock()) {
+ if (socket == null)
+ return true;
+ try {
+ checkAndSetState(SocketState.CONNECTED, SocketState.CLOSED);
+ socket.close();
+ socket = null;
+ socketInputStream = null;
+ socketOutputStream = null;
+
+ if (listener.isAlive()) {
+ listener.stop();
+ listener.awaitTermination();
+ }
+
+ if (callbackManager.isRunning()) {
+ callbackManager.callOnEach((callback) -> callback.onDisconnected(this));
+ callbackManager.stop();
+ }
+ return true;
+ } catch (IOException e) {
+ Log.e(e);
}
- if (callbackManager.isRunning()) {
- callbackManager.callOnEach((callback) -> callback.onDisconnected(this));
- callbackManager.stop();
- }
- socket = null;
- return true;
- } catch (IOException e) {
- Log.e(e);
+ return false;
}
- return false;
+ }
+
+ public boolean send(NetBuffer data) {
+ return send(data.array(), data.position(), data.remaining());
}
public boolean send(ByteBuffer data) {
- try {
- while (data.hasRemaining()) {
- if (socket == null || socket.write(data) <= 0)
+ return send(data.array(), data.position(), data.remaining());
+ }
+
+ public boolean send(byte [] data) {
+ return send(data, 0, data.length);
+ }
+
+ public boolean send(byte [] data, int offset, int length) {
+ synchronized (stateLock()) {
+ try {
+ if (socket == null)
return false;
+
+ if (length > 0)
+ socketOutputStream.write(data, offset, length);
+
+ return true;
+ } catch (IOException e) {
+ Log.e(e);
}
- return true;
- } catch (IOException e) {
- Log.e(e);
+ return false;
}
- return false;
+ }
+
+ protected Socket createSocket() throws IOException {
+ return new Socket();
+ }
+
+ protected final Object stateLock() {
+ return stateMutex;
+ }
+
+ /**
+ * Checks the current state to see if it matches the expected, and if so, changes it to the new state. If not, it fails the assertion
+ * @param expected the expected state
+ * @param state the new state
+ */
+ private void checkAndSetState(SocketState expected, SocketState state) {
+ Assert.notNull(expected, "Expected state cannot be null!");
+ Assert.notNull(state, "New state cannot be null!");
+ Assert.test(this.state.compareAndSet(expected, state), "Failed to set state! Was: " + this.state.get() + " Expected: " + expected + " Update: " + state);
}
public interface TCPSocketCallback {
@@ -142,6 +227,13 @@ public class TCPSocket {
void onIncomingData(TCPSocket socket, byte [] data);
}
+ private enum SocketState {
+ CLOSED,
+ CREATED,
+ CONNECTING,
+ CONNECTED
+ }
+
private class TCPSocketListener implements Runnable {
private final AtomicBoolean running;
@@ -158,7 +250,7 @@ public class TCPSocket {
public void start() {
Assert.test(!running.get(), "Cannot start listener! Already started!");
Assert.isNull(thread, "Cannot start listener! Already started!");
- thread = new Thread(this);
+ thread = new Thread(this, "TCPServer Port#" + getRemoteAddress().getPort());
running.set(true);
thread.start();
}
@@ -187,13 +279,10 @@ public class TCPSocket {
public void run() {
try {
alive.set(true);
- SocketChannel sc = TCPSocket.this.socket;
- int bufferSize = TCPSocket.this.bufferSize;
- Assert.notNull(sc, "SocketChannel is null at start of listener run()!");
- Assert.test(bufferSize > 0, "Buffer size is <= 0 at start of listener run()!");
- ByteBuffer buf = ByteBuffer.allocateDirect(bufferSize);
+ InputStream input = TCPSocket.this.socketInputStream;
+ byte [] buffer = new byte[TCPSocket.this.bufferSize];
while (running.get()) {
- waitIncoming(sc, buf, bufferSize);
+ waitIncoming(input, buffer);
}
} catch (Throwable t) {
@@ -205,18 +294,14 @@ public class TCPSocket {
}
}
- private void waitIncoming(SocketChannel sc, ByteBuffer buf, int bufferSize) throws IOException {
- buf.position(0);
- buf.limit(bufferSize);
- int n = sc.read(buf);
- buf.flip();
+ private void waitIncoming(InputStream input, byte [] buffer) throws IOException {
+ int n = input.read(buffer);
if (n == 0)
return;
if (n < 0)
throw new EOFException();
byte [] data = new byte[n];
- buf.position(0);
- buf.get(data, 0, n);
+ System.arraycopy(buffer, 0, data, 0, n);
callbackManager.callOnEach((callback) -> callback.onIncomingData(TCPSocket.this, data));
}
diff --git a/src/com/projectswg/common/network/packets/PacketType.java b/src/com/projectswg/common/network/packets/PacketType.java
index 00148eb..6d86423 100644
--- a/src/com/projectswg/common/network/packets/PacketType.java
+++ b/src/com/projectswg/common/network/packets/PacketType.java
@@ -370,7 +370,7 @@ public enum PacketType {
return null;
Class extends SWGPacket> c = type.c;
try {
- return c.newInstance();
+ return c.getConstructor().newInstance();
} catch (Exception e) {
Log.e("Packet: [%08X] %s", crc, c.getName());
Log.e(e);
diff --git a/src/com/projectswg/common/process/ArgumentParser.java b/src/com/projectswg/common/process/ArgumentParser.java
new file mode 100644
index 0000000..21928d7
--- /dev/null
+++ b/src/com/projectswg/common/process/ArgumentParser.java
@@ -0,0 +1,94 @@
+package com.projectswg.common.process;
+
+import com.projectswg.common.debug.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+public class ArgumentParser {
+
+ public static Map parseArguments(String [] args, Function renameArgument, PredicateexpectsArgument) {
+ Map arguments = new HashMap<>();
+ for (int i = 0; i < args.length; i++) {
+ String arg = args[i];
+ int equalIndex = arg.indexOf('=');
+
+ String key = arg;
+ String value = null;
+ if (equalIndex != -1)
+ key = arg.substring(0, equalIndex);
+ key = renameArgument.apply(key);
+
+ if (expectsArgument.test(key)) {
+ if (equalIndex == -1) { // seek ahead for argument
+ if (i + 1 < args.length)
+ value = args[i+1];
+ } else { // parse out argument
+ value = arg.substring(equalIndex+1);
+ }
+ }
+ arguments.put(key, value);
+ }
+ return arguments;
+ }
+
+ public static void requireValue(Map arguments, String key) {
+ if (!arguments.containsKey(key) || arguments.get(key) == null) {
+ throw new IllegalArgumentException(key + " - no argument");
+ }
+ }
+
+ public static void requireShort(Map arguments, String key) {
+ requireValue(arguments, key);
+ try {
+ Short.parseShort(arguments.get(key));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(key + " - invalid argument");
+ }
+ }
+
+ public static void requireInt(Map arguments, String key) {
+ requireValue(arguments, key);
+ try {
+ Integer.parseInt(arguments.get(key));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(key + " - invalid argument");
+ }
+ }
+
+ public static void requireLong(Map arguments, String key) {
+ requireValue(arguments, key);
+ try {
+ Long.parseLong(arguments.get(key));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(key + " - invalid argument");
+ }
+ }
+
+ public static int parseShort(String str, int defaultValue) {
+ try {
+ return Short.parseShort(str);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ public static int parseInt(String str, int defaultValue) {
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ public static long parseLong(String str, long defaultValue) {
+ try {
+ return Long.parseLong(str);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+}
diff --git a/src/com/projectswg/common/process/JarProcessBuilder.java b/src/com/projectswg/common/process/JarProcessBuilder.java
new file mode 100644
index 0000000..1cbcf23
--- /dev/null
+++ b/src/com/projectswg/common/process/JarProcessBuilder.java
@@ -0,0 +1,261 @@
+package com.projectswg.common.process;
+
+import com.projectswg.common.debug.Assert;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class JarProcessBuilder {
+
+ private final ProcessBuilder builder;
+ private final String javaPath;
+ private final String jarPath;
+ private final List vmArguments;
+ private final List arguments;
+ private long minMemory;
+ private long maxMemory;
+
+ public JarProcessBuilder(File java, File jar) {
+ this.builder = new ProcessBuilder();
+ String javaPath, jarPath;
+ try {
+ javaPath = java.getCanonicalPath();
+ jarPath = jar.getCanonicalPath();
+ } catch (IOException e) {
+ javaPath = java.getAbsolutePath();
+ jarPath = jar.getAbsolutePath();
+ }
+ this.javaPath = javaPath;
+ this.jarPath = jarPath;
+ this.vmArguments = new CopyOnWriteArrayList<>();
+ this.arguments = new CopyOnWriteArrayList<>();
+ this.minMemory = Long.MIN_VALUE;
+ this.maxMemory = Long.MIN_VALUE;
+ }
+
+ public JarProcessBuilder setMinMemory(long minMemory, MemoryUnit unit) {
+ Assert.test(unit == MemoryUnit.KILOBYTES || unit == MemoryUnit.MEGABYTES || unit == MemoryUnit.GIGABYTES, "Unsupported memory unit!");
+ this.minMemory = unit.getBytes(minMemory);
+ return this;
+ }
+
+ public JarProcessBuilder setMaxMemory(long maxMemory, MemoryUnit unit) {
+ Assert.test(unit == MemoryUnit.KILOBYTES || unit == MemoryUnit.MEGABYTES || unit == MemoryUnit.GIGABYTES, "Unsupported memory unit!");
+ this.maxMemory = unit.getBytes(maxMemory);
+ return this;
+ }
+
+ public JarProcessBuilder setMemory(long minMemory, long maxMemory, MemoryUnit unit) {
+ Assert.test(unit == MemoryUnit.KILOBYTES || unit == MemoryUnit.MEGABYTES || unit == MemoryUnit.GIGABYTES, "Unsupported memory unit!");
+ this.minMemory = unit.getBytes(minMemory);
+ this.maxMemory = unit.getBytes(maxMemory);
+ return this;
+ }
+
+ public JarProcessBuilder setVMArguments(String ... args) {
+ return setVMArguments(Arrays.asList(args));
+ }
+
+ public JarProcessBuilder setVMArguments(List args) {
+ this.vmArguments.clear();
+ this.vmArguments.addAll(args);
+ return this;
+ }
+
+ public JarProcessBuilder setArguments(String ... args) {
+ return setArguments(Arrays.asList(args));
+ }
+
+ public JarProcessBuilder setArguments(List args) {
+ this.arguments.clear();
+ this.arguments.addAll(args);
+ return this;
+ }
+
+ public List command() {
+ return builder.command();
+ }
+
+ public Map environment() {
+ return builder.environment();
+ }
+
+ public File directory() {
+ return builder.directory();
+ }
+
+ public JarProcessBuilder directory(File directory) {
+ builder.directory(directory);
+ return this;
+ }
+
+ public JarProcessBuilder redirectInput(Redirect source) {
+ builder.redirectInput(source);
+ return this;
+ }
+
+ public JarProcessBuilder redirectOutput(Redirect destination) {
+ builder.redirectOutput(destination);
+ return this;
+ }
+
+ public JarProcessBuilder redirectError(Redirect destination) {
+ builder.redirectError(destination);
+ return this;
+ }
+
+ public JarProcessBuilder redirectInput(File file) {
+ builder.redirectInput(file);
+ return this;
+ }
+
+ public JarProcessBuilder redirectOutput(File file) {
+ builder.redirectOutput(file);
+ return this;
+ }
+
+ public JarProcessBuilder redirectError(File file) {
+ builder.redirectError(file);
+ return this;
+ }
+
+ public Redirect redirectInput() {
+ return builder.redirectInput();
+ }
+
+ public Redirect redirectOutput() {
+ return builder.redirectOutput();
+ }
+
+ public Redirect redirectError() {
+ return builder.redirectError();
+ }
+
+ public JarProcessBuilder inheritIO() {
+ builder.inheritIO();
+ return this;
+ }
+
+ public boolean redirectErrorStream() {
+ return builder.redirectErrorStream();
+ }
+
+ public JarProcessBuilder redirectErrorStream(boolean redirectErrorStream) {
+ builder.redirectErrorStream(redirectErrorStream);
+ return this;
+ }
+
+ public Process start() throws IOException {
+ return builder.command(createCommand()).start();
+ }
+
+ @Override
+ public int hashCode() {
+ return builder.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return builder.equals(obj);
+ }
+
+ @Override
+ public String toString() {
+ return builder.toString();
+ }
+
+ private List createCommand() {
+ List command = new ArrayList<>();
+ command.add(javaPath);
+ if (minMemory > 0) {
+ MemoryUnit unit = MemoryUnit.getAppropriateUnit(minMemory);
+ command.add("-Xms" + minMemory/unit.getFactor() + getJavaChar(unit));
+ }
+ if (maxMemory > 0) {
+ MemoryUnit unit = MemoryUnit.getAppropriateUnit(maxMemory);
+ command.add("-Xmx" + maxMemory/unit.getFactor() + getJavaChar(unit));
+ }
+ command.addAll(vmArguments);
+ command.add("-jar");
+ command.add(jarPath);
+ command.addAll(arguments);
+ return command;
+ }
+
+ private static char getJavaChar(MemoryUnit unit) {
+ switch (unit) {
+ case BYTES:
+ return 'b';
+ case KILOBYTES:
+ default:
+ return 'k';
+ case MEGABYTES:
+ return 'm';
+ case GIGABYTES:
+ return 'g';
+ case TERABYTES:
+ return 't';
+ }
+ }
+
+ public static File getJavaPath() {
+ return new File(System.getProperty("java.home"), "bin/java");
+ }
+
+ public enum MemoryUnit {
+ BYTES ("B", 1),
+ KILOBYTES ("kB", 1024),
+ MEGABYTES ("MB", 1048576),
+ GIGABYTES ("GB", 1073741824),
+ TERABYTES ("TB", 1099511627776L);
+
+ private final String suffix;
+ private final long factor;
+
+ MemoryUnit(String suffix, long factor) {
+ this.suffix = suffix;
+ this.factor = factor;
+ }
+
+ public String getSuffix() {
+ return suffix;
+ }
+
+ public long getFactor() {
+ return factor;
+ }
+
+ public long getBytes(double val) {
+ return (long) (val * factor);
+ }
+
+ public long getBytes(long val) {
+ return val * factor;
+ }
+
+ public double convert(double val, MemoryUnit unit) {
+ long bytes = getBytes(val);
+ return bytes / (double) unit.getFactor();
+ }
+
+ public static MemoryUnit getAppropriateUnit(long val) {
+ if (val < KILOBYTES.getFactor())
+ return MemoryUnit.BYTES;
+ if (val < MEGABYTES.getFactor())
+ return MemoryUnit.KILOBYTES;
+ if (val < GIGABYTES.getFactor())
+ return MemoryUnit.MEGABYTES;
+ if (val < TERABYTES.getFactor())
+ return MemoryUnit.GIGABYTES;
+ return MemoryUnit.TERABYTES;
+ }
+
+ }
+
+}
diff --git a/src/com/projectswg/common/utilities/LocalUtilities.java b/src/com/projectswg/common/utilities/LocalUtilities.java
new file mode 100644
index 0000000..09022e6
--- /dev/null
+++ b/src/com/projectswg/common/utilities/LocalUtilities.java
@@ -0,0 +1,50 @@
+package com.projectswg.common.utilities;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class LocalUtilities {
+
+ private static final AtomicReference APP_NAME = new AtomicReference<>(".projectswg");
+
+ public static void setApplicationName(String name) {
+ APP_NAME.set(name);
+ }
+
+ public static File getSubApplicationDirectory(String ... names) {
+ File dir = getApplicationDirectory();
+ for (String name : names) {
+ dir = new File(dir, name);
+ if (!dir.exists())
+ dir.mkdir();
+ }
+ return dir;
+ }
+
+ public static File getApplicationDirectory() {
+ String home = getHomeDirectory();
+ if (home == null)
+ throw new IllegalStateException("Unknown home directory for OS '"+System.getProperty("os.name")+"'");
+
+ File dir = new File(home, APP_NAME.get());
+ if (!dir.exists())
+ dir.mkdirs();
+ return dir;
+ }
+
+ private static String getHomeDirectory() {
+ String os = System.getProperty("os.name").toUpperCase();
+
+ if (os.contains("WIN"))
+ return System.getenv("APPDATA");
+
+ if (os.contains("MAC"))
+ return System.getProperty("user.home") + "/Library/Application Support";
+
+ if (os.contains("NUX"))
+ return System.getProperty("user.home");
+
+ return System.getProperty("user.dir");
+ }
+
+}