diff --git a/build.gradle b/build.gradle index 10ea419b..d853dc10 100644 --- a/build.gradle +++ b/build.gradle @@ -234,6 +234,24 @@ project(":server:bnls") { } } +project(":server:bncs") { + apply plugin: "java" + + dependencies { + compile project(":core") + compile "com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion" + compile "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" + } + + dependencies { + compile group: 'commons-cli', name: 'commons-cli', version: cliVersion + } + + dependencies { + testCompile 'junit:junit:4.12' + } +} + project(":server:mcp") { apply plugin: "java" diff --git a/core/gen/com/riiablo/net/packet/bncs/BNCS.java b/core/gen/com/riiablo/net/packet/bncs/BNCS.java new file mode 100644 index 00000000..9e683288 --- /dev/null +++ b/core/gen/com/riiablo/net/packet/bncs/BNCS.java @@ -0,0 +1,40 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +package com.riiablo.net.packet.bncs; + +import com.google.flatbuffers.FlatBufferBuilder; +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +@SuppressWarnings("unused") +public final class BNCS extends Table { + public static BNCS getRootAsBNCS(ByteBuffer _bb) { return getRootAsBNCS(_bb, new BNCS()); } + public static BNCS getRootAsBNCS(ByteBuffer _bb, BNCS obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; } + public BNCS __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } + + public byte dataType() { int o = __offset(4); return o != 0 ? bb.get(o + bb_pos) : 0; } + public Table data(Table obj) { int o = __offset(6); return o != 0 ? __union(obj, o) : null; } + + public static int createBNCS(FlatBufferBuilder builder, + byte data_type, + int dataOffset) { + builder.startObject(2); + BNCS.addData(builder, dataOffset); + BNCS.addDataType(builder, data_type); + return BNCS.endBNCS(builder); + } + + public static void startBNCS(FlatBufferBuilder builder) { builder.startObject(2); } + public static void addDataType(FlatBufferBuilder builder, byte dataType) { builder.addByte(0, dataType, 0); } + public static void addData(FlatBufferBuilder builder, int dataOffset) { builder.addOffset(1, dataOffset, 0); } + public static int endBNCS(FlatBufferBuilder builder) { + int o = builder.endObject(); + return o; + } + public static void finishBNCSBuffer(FlatBufferBuilder builder, int offset) { builder.finish(offset); } + public static void finishSizePrefixedBNCSBuffer(FlatBufferBuilder builder, int offset) { builder.finishSizePrefixed(offset); } +} + diff --git a/core/gen/com/riiablo/net/packet/bncs/BNCSData.java b/core/gen/com/riiablo/net/packet/bncs/BNCSData.java new file mode 100644 index 00000000..1f050627 --- /dev/null +++ b/core/gen/com/riiablo/net/packet/bncs/BNCSData.java @@ -0,0 +1,14 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +package com.riiablo.net.packet.bncs; + +public final class BNCSData { + private BNCSData() { } + public static final byte NONE = 0; + public static final byte ChatEvent = 1; + + public static final String[] names = { "NONE", "ChatEvent", }; + + public static String name(int e) { return names[e]; } +} + diff --git a/core/gen/com/riiablo/net/packet/bncs/ChatEvent.java b/core/gen/com/riiablo/net/packet/bncs/ChatEvent.java new file mode 100644 index 00000000..0d5b5459 --- /dev/null +++ b/core/gen/com/riiablo/net/packet/bncs/ChatEvent.java @@ -0,0 +1,46 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +package com.riiablo.net.packet.bncs; + +import com.google.flatbuffers.FlatBufferBuilder; +import com.google.flatbuffers.Table; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +@SuppressWarnings("unused") +public final class ChatEvent extends Table { + public static ChatEvent getRootAsChatEvent(ByteBuffer _bb) { return getRootAsChatEvent(_bb, new ChatEvent()); } + public static ChatEvent getRootAsChatEvent(ByteBuffer _bb, ChatEvent obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); } + public void __init(int _i, ByteBuffer _bb) { bb_pos = _i; bb = _bb; } + public ChatEvent __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; } + + public byte eid() { int o = __offset(4); return o != 0 ? bb.get(o + bb_pos) : 19; } + public String name() { int o = __offset(6); return o != 0 ? __string(o + bb_pos) : null; } + public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(6, 1); } + public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); } + public String text() { int o = __offset(8); return o != 0 ? __string(o + bb_pos) : null; } + public ByteBuffer textAsByteBuffer() { return __vector_as_bytebuffer(8, 1); } + public ByteBuffer textInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 1); } + + public static int createChatEvent(FlatBufferBuilder builder, + byte eid, + int nameOffset, + int textOffset) { + builder.startObject(3); + ChatEvent.addText(builder, textOffset); + ChatEvent.addName(builder, nameOffset); + ChatEvent.addEid(builder, eid); + return ChatEvent.endChatEvent(builder); + } + + public static void startChatEvent(FlatBufferBuilder builder) { builder.startObject(3); } + public static void addEid(FlatBufferBuilder builder, byte eid) { builder.addByte(0, eid, 19); } + public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(1, nameOffset, 0); } + public static void addText(FlatBufferBuilder builder, int textOffset) { builder.addOffset(2, textOffset, 0); } + public static int endChatEvent(FlatBufferBuilder builder) { + int o = builder.endObject(); + return o; + } +} + diff --git a/core/gen/com/riiablo/net/packet/bncs/EID.java b/core/gen/com/riiablo/net/packet/bncs/EID.java new file mode 100644 index 00000000..3558a250 --- /dev/null +++ b/core/gen/com/riiablo/net/packet/bncs/EID.java @@ -0,0 +1,29 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +package com.riiablo.net.packet.bncs; + +public final class EID { + private EID() { } + public static final byte EID_SHOWUSER = 1; + public static final byte EID_JOIN = 2; + public static final byte EID_LEAVE = 3; + public static final byte EID_WHISPER = 4; + public static final byte EID_TALK = 5; + public static final byte EID_BROADCAST = 6; + public static final byte EID_CHANNEL = 7; + public static final byte EID_USERFLAGS = 9; + public static final byte EID_WHISPERSENT = 10; + public static final byte EID_CHANNELFULL = 13; + public static final byte EID_CHANNELDOESNOTEXIST = 14; + public static final byte EID_CHANNELRESTRICTED = 15; + public static final byte EID_INFO = 18; + public static final byte EID_ERROR = 19; + public static final byte EID_IGNORE = 21; + public static final byte EID_ACCEPT = 22; + public static final byte EID_EMOTE = 23; + + public static final String[] names = { "EID_SHOWUSER", "EID_JOIN", "EID_LEAVE", "EID_WHISPER", "EID_TALK", "EID_BROADCAST", "EID_CHANNEL", "", "EID_USERFLAGS", "EID_WHISPERSENT", "", "", "EID_CHANNELFULL", "EID_CHANNELDOESNOTEXIST", "EID_CHANNELRESTRICTED", "", "", "EID_INFO", "EID_ERROR", "", "EID_IGNORE", "EID_ACCEPT", "EID_EMOTE", }; + + public static String name(int e) { return names[e - EID_SHOWUSER]; } +} + diff --git a/core/src/com/riiablo/net/bncs/BNCS.fbs b/core/src/com/riiablo/net/bncs/BNCS.fbs new file mode 100644 index 00000000..0cd0809f --- /dev/null +++ b/core/src/com/riiablo/net/bncs/BNCS.fbs @@ -0,0 +1,13 @@ +include "ChatEvent.fbs"; + +namespace com.riiablo.net.packet.bncs; + +union BNCSData { + ChatEvent, +} + +table BNCS { + data:BNCSData; +} + +root_type BNCS; \ No newline at end of file diff --git a/core/src/com/riiablo/net/bncs/ChatEvent.fbs b/core/src/com/riiablo/net/bncs/ChatEvent.fbs new file mode 100644 index 00000000..fe069f28 --- /dev/null +++ b/core/src/com/riiablo/net/bncs/ChatEvent.fbs @@ -0,0 +1,27 @@ +namespace com.riiablo.net.packet.bncs; + +enum EID : byte { + EID_SHOWUSER = 0x01, // User in channel + EID_JOIN = 0x02, // User joined channel + EID_LEAVE = 0x03, // User left channel + EID_WHISPER = 0x04, // Recieved whisper + EID_TALK = 0x05, // Chat text + EID_BROADCAST = 0x06, // Server broadcast + EID_CHANNEL = 0x07, // Channel information + EID_USERFLAGS = 0x09, // Flags update + EID_WHISPERSENT = 0x0A, // Sent whisper + EID_CHANNELFULL = 0x0D, // Channel full + EID_CHANNELDOESNOTEXIST = 0x0E, // Channel doesn't exist + EID_CHANNELRESTRICTED = 0x0F, // Channel is restricted + EID_INFO = 0x12, // Information + EID_ERROR = 0x13, // Error message + EID_IGNORE = 0x15, // Notifies that a user has been ignored (DEFUNCT) + EID_ACCEPT = 0x16, // Notifies that a user has been unignored (DEFUNCT) + EID_EMOTE = 0x17, // Emote +} + +table ChatEvent { + eid:EID = EID_ERROR; + name:string; + text:string; +} diff --git a/server/bncs/build.gradle b/server/bncs/build.gradle new file mode 100644 index 00000000..21250a01 --- /dev/null +++ b/server/bncs/build.gradle @@ -0,0 +1,29 @@ +apply plugin: "java" + +sourceCompatibility = 1.7 +sourceSets.main.java.srcDirs = [ "src/" ] +sourceSets.test.java.srcDirs = [ "test/" ] + +project.ext.mainClassName = "com.riiablo.server.bncs.BNCS" +project.ext.assetsDir = new File("../android/assets"); + +task run(dependsOn: classes, type: JavaExec) { + main = project.mainClassName + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + workingDir = project.assetsDir + ignoreExitValue = true +} + +task dist(type: Jar) { + from files(sourceSets.main.output.classesDir) + from files(sourceSets.main.output.resourcesDir) + from {configurations.compile.collect {zipTree(it)}} + from files(project.assetsDir); + + manifest { + attributes 'Server-Class': project.mainClassName + } +} + +dist.dependsOn classes diff --git a/server/bncs/src/com/riiablo/server/bncs/Main.java b/server/bncs/src/com/riiablo/server/bncs/Main.java new file mode 100644 index 00000000..d88398be --- /dev/null +++ b/server/bncs/src/com/riiablo/server/bncs/Main.java @@ -0,0 +1,257 @@ +package com.riiablo.server.bncs; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Net; +import com.badlogic.gdx.backends.headless.HeadlessApplication; +import com.badlogic.gdx.backends.headless.HeadlessApplicationConfiguration; +import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.net.ServerSocket; +import com.badlogic.gdx.net.Socket; +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.BufferUtils; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.text.DateFormat; +import java.util.Calendar; +import java.util.concurrent.atomic.AtomicBoolean; + +public class Main extends ApplicationAdapter { + private static final String TAG = "D2CS"; + + private static final int PORT = 6113; + private static final int MAX_CLIENTS = 32; + + ServerSocket server; + ByteBuffer buffer; + Thread main; + AtomicBoolean kill; + ThreadGroup clientThreads; + final Array clients = new Array<>(MAX_CLIENTS); + final Array packets = new Array<>(32); + final Array cache = new Array<>(32); + + public static void main(String[] args) { + HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); + new HeadlessApplication(new Main(), config); + } + + @Override + public void create() { + Gdx.app.setLogLevel(Application.LOG_DEBUG); + + final Calendar calendar = Calendar.getInstance(); + DateFormat format = DateFormat.getDateTimeInstance(); + Gdx.app.log(TAG, format.format(calendar.getTime())); + + try { + InetAddress address = InetAddress.getLocalHost(); + Gdx.app.log(TAG, "IP Address: " + address.getHostAddress() + ":" + PORT); + Gdx.app.log(TAG, "Host Name: " + address.getHostName()); + } catch (UnknownHostException e) { + Gdx.app.error(TAG, e.getMessage(), e); + } + + clientThreads = new ThreadGroup("D2CSClients"); + + Gdx.app.log(TAG, "Starting server..."); + server = Gdx.net.newServerSocket(Net.Protocol.TCP, PORT, null); + buffer = BufferUtils.newByteBuffer(4096); + kill = new AtomicBoolean(false); + main = new Thread(new Runnable() { + @Override + public void run() { + while (!kill.get()) { + Gdx.app.log(TAG, "waiting..."); + Socket socket = server.accept(null); + Gdx.app.log(TAG, "connection from " + socket.getRemoteAddress()); + synchronized (clients) { + if (clients.size >= MAX_CLIENTS) { + try { + ConnectionDenied(socket, "Server is Full"); + } catch (Throwable ignored) { + } finally { + socket.dispose(); + } + } else { + try { + ConnectionAccepted(socket); + int id = clients.size; + Client client = new Client(id, socket); + clients.add(client); + client.start(); + } catch (Throwable ignored) { + socket.dispose(); + } + } + } + } + + Gdx.app.log(TAG, "killing child threads..."); + synchronized (clients) { + for (Client client : clients) { + if (client != null) { + client.kill.set(true); + } + } + + clients.clear(); + } + + Gdx.app.log(TAG, "killing thread..."); + } + }); + main.setName("D2CS"); + main.start(); + } + + @Override + public void dispose() { + Gdx.app.log(TAG, "Shutting down..."); + kill.set(true); + server.dispose(); + try { + main.join(); + } catch (Throwable ignored) {} + } + + @Override + public void render() { + synchronized (packets) { + cache.clear(); + cache.addAll(packets); + packets.clear(); + } + + for (Packet packet : cache) { + process(packet); + } + } + + private void process(Packet packet) { + synchronized (clients) { + for (Client client : clients) { + try { + client.socket.getOutputStream().write(packet.data); + } catch (Throwable t) { + Gdx.app.error(TAG, t.getMessage(), t); + client.kill.set(true); + } + } + } +// BNCS data = packet.data; +// switch (data.dataType()) { +// case BNCSData.ChatEvent: +// synchronized (clients) { +// for (Client client : clients) { +// } +// } +// break; +// default: +// Gdx.app.error(TAG, "Unknown packet type: " + data.dataType()); +// } + } + + private boolean ConnectionDenied(Socket socket, String reason) throws IOException { +// FlatBufferBuilder builder = new FlatBufferBuilder(); +// int reasonOffset = builder.createString(reason); +// int connectionDeniedId = ConnectionClosed.createConnectionClosed(builder, reasonOffset); +// int id = com.riiablo.net.packet.d2gs.D2GS.createD2GS(builder, D2GSData.ConnectionClosed, connectionDeniedId); +// builder.finish(id); +// +// ByteBuffer data = builder.dataBuffer(); +// OutputStream out = socket.getOutputStream(); +// WritableByteChannel channel = Channels.newChannel(out); +// channel.write(data); + return true; + } + + private boolean ConnectionAccepted(Socket socket) throws IOException { +// Gdx.app.debug(TAG, "Connection accepted!"); +// FlatBufferBuilder builder = new FlatBufferBuilder(); +// ConnectionAccepted.startConnectionAccepted(builder); +// int connectionAcceptedId = ConnectionAccepted.endConnectionAccepted(builder); +// int id = com.riiablo.net.packet.d2gs.D2GS.createD2GS(builder, D2GSData.ConnectionAccepted, connectionAcceptedId); +// builder.finish(id); +// +// ByteBuffer data = builder.dataBuffer(); +// OutputStream out = socket.getOutputStream(); +// WritableByteChannel channel = Channels.newChannel(out); +// channel.write(data); + return false; + } + + private void process(int id, byte[] packet) { + synchronized (packets) { + packets.add(Packet.of(id, packet)); + } + } + + static String generateClientName() { + return String.format("Client-%08X", MathUtils.random(1, Integer.MAX_VALUE - 1)); + } + + private class Client extends Thread { + int id; + Socket socket; + AtomicBoolean kill = new AtomicBoolean(false); + + Client(int id, Socket socket) { + super(clientThreads, generateClientName()); + this.id = id; + this.socket = socket; + } + + @Override + public void run() { + while (!kill.get()) { + try { + buffer.clear(); + buffer.mark(); + ReadableByteChannel in = Channels.newChannel(socket.getInputStream()); + if (in.read(buffer) == -1) { + kill.set(true); + break; + } + buffer.limit(buffer.position()); + buffer.reset(); + + byte[] data = com.riiablo.util.BufferUtils.readRemaining(buffer); + + /* + BNCS packet = BNCS.getRootAsBNCS(buffer); + Gdx.app.log(TAG, "packet type " + D2GSData.name(packet.dataType())); + */ + process(id, data); + } catch (Throwable t) { + Gdx.app.log(TAG, t.getMessage(), t); + kill.set(true); + } + } + + Gdx.app.log(TAG, "closing socket..."); + if (socket != null) socket.dispose(); + synchronized (clients) { + clients.removeValue(this, true); + } + } + } + + private static class Packet { + int id; + byte[] data; + + static Packet of(int id, byte[] data) { + Packet packet = new Packet(); + packet.id = id; + packet.data = data; + return packet; + } + } +} diff --git a/settings.gradle b/settings.gradle index da09bfac..bbd2f1dc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,4 @@ include 'desktop' include 'android' include 'tools', 'ds1viewer', 'mpqviewer' include 'tester', 'mpqlib' -include 'server:bnls', 'server:mcp', 'server:d2gs' +include 'server:bnls', 'server:bncs', 'server:mcp', 'server:d2gs'