From a1e735031fdd2b886d66b39244c03204cb9ff743 Mon Sep 17 00:00:00 2001 From: Collin Smith Date: Wed, 30 Jan 2019 15:40:17 -0800 Subject: [PATCH] Added server browser chat Some work on in-game chat Separated Server into multiple systems (will be easier to transition to distributed servers later) Minor entity changes -- may transition to an ECS soon --- core/src/gdx/diablo/Keys.java | 1 + core/src/gdx/diablo/Textures.java | 3 + core/src/gdx/diablo/entity/Direction.java | 7 +- core/src/gdx/diablo/entity3/Entity.java | 51 ++++- core/src/gdx/diablo/entity3/Player.java | 2 +- core/src/gdx/diablo/item/BodyLoc.java | 4 + core/src/gdx/diablo/screen/GameScreen.java | 35 +++- core/src/gdx/diablo/screen/LobbyScreen.java | 102 +++++++++- core/src/gdx/diablo/server/Message.java | 10 + core/src/gdx/diablo/server/Session.java | 14 +- core/src/gdx/diablo/server/SessionError.java | 27 +++ core/src/gdx/diablo/widget/TextArea.java | 26 +++ core/test/gdx/diablo/entity3/EntityTest.java | 11 +- server/build.gradle | 2 +- server/src/gdx/diablo/server/ChatServer.java | 131 +++++++++++++ server/src/gdx/diablo/server/Server.java | 166 ++-------------- .../src/gdx/diablo/server/ServerBrowser.java | 179 ++++++++++++++++++ server/src/gdx/diablo/server/ServerUtils.java | 35 ++++ 18 files changed, 640 insertions(+), 166 deletions(-) create mode 100644 core/src/gdx/diablo/server/Message.java create mode 100644 core/src/gdx/diablo/server/SessionError.java create mode 100644 core/src/gdx/diablo/widget/TextArea.java create mode 100644 server/src/gdx/diablo/server/ChatServer.java create mode 100644 server/src/gdx/diablo/server/ServerBrowser.java create mode 100644 server/src/gdx/diablo/server/ServerUtils.java diff --git a/core/src/gdx/diablo/Keys.java b/core/src/gdx/diablo/Keys.java index 089db86a..116258a3 100644 --- a/core/src/gdx/diablo/Keys.java +++ b/core/src/gdx/diablo/Keys.java @@ -41,5 +41,6 @@ public class Keys { public static final MappedKey Character = new MappedKey("Character", "character", Input.Keys.C, Input.Keys.A); public static final MappedKey Stash = new MappedKey("Stash", "stash", Input.Keys.NUMPAD_1); public static final MappedKey SwapWeapons = new MappedKey("SwapWeapons", "swap", Input.Keys.W); + public static final MappedKey Enter = new MappedKey("Enter", "enter", Input.Keys.ENTER); } diff --git a/core/src/gdx/diablo/Textures.java b/core/src/gdx/diablo/Textures.java index c2c0b52d..f45c6fdb 100644 --- a/core/src/gdx/diablo/Textures.java +++ b/core/src/gdx/diablo/Textures.java @@ -7,9 +7,11 @@ import com.badlogic.gdx.utils.Disposable; public class Textures implements Disposable { public final Texture white; + public final Texture modal; public Textures() { white = createTexture(Diablo.colors.white); + modal = createTexture(new Color(0.0f, 0.0f, 0.0f, 0.5f)); } public Texture createTexture(Color color) { @@ -24,5 +26,6 @@ public class Textures implements Disposable { @Override public void dispose() { white.dispose(); + modal.dispose(); } } diff --git a/core/src/gdx/diablo/entity/Direction.java b/core/src/gdx/diablo/entity/Direction.java index 270497c2..11d2bf2b 100644 --- a/core/src/gdx/diablo/entity/Direction.java +++ b/core/src/gdx/diablo/entity/Direction.java @@ -72,7 +72,12 @@ public class Direction { private Direction() {} public static int radiansToDirection(float radians, int directions) { - return radiansToDirection16(radians); + switch (directions) { + case 1: return 0; + case 8: return radiansToDirection8(radians); + case 16: return radiansToDirection16(radians); + default: return 0; + } } static final int DIRS_16[] = {7, 13, 2, 12, 6, 11, 1, 10, 5, 9, 0, 8, 4, 15, 3, 14}; diff --git a/core/src/gdx/diablo/entity3/Entity.java b/core/src/gdx/diablo/entity3/Entity.java index b84cbe17..c6d68a3a 100644 --- a/core/src/gdx/diablo/entity3/Entity.java +++ b/core/src/gdx/diablo/entity3/Entity.java @@ -23,6 +23,7 @@ import gdx.diablo.codec.DCC; import gdx.diablo.entity.Direction; import gdx.diablo.graphics.PaletteIndexedBatch; import gdx.diablo.item.Item; +import gdx.diablo.map.DS1; import gdx.diablo.map.DT1.Tile; public class Entity { @@ -121,6 +122,18 @@ public class Entity { invalidate(); } + public static Entity create(DS1 ds1, DS1.Object obj) { + final int type = obj.type; + switch (type) { + case DS1.Object.DYNAMIC_TYPE: + throw new UnsupportedOperationException("Unsupported type: " + type); + case DS1.Object.STATIC_TYPE: + return null; + default: + throw new AssertionError("Unsupported type: " + type); + } + } + public void setMode(String mode) { setMode(mode, mode); } @@ -219,17 +232,42 @@ public class Entity { Gdx.app.log(TAG, path); if (DEBUG_ASSETS) { - AssetDescriptor descriptor = new AssetDescriptor<>(path, DCC.class); + final AssetDescriptor descriptor = new AssetDescriptor<>(path, DCC.class); Diablo.assets.load(descriptor); Diablo.assets.finishLoadingAsset(descriptor); DCC dcc = Diablo.assets.get(descriptor); animation.setLayer(c, dcc); + + /*Runnable loader = new Runnable() { + @Override + public void run() { + if (!Diablo.assets.isLoaded(descriptor)) { + Gdx.app.postRunnable(this); + return; + } + + DCC dcc = Diablo.assets.get(descriptor); + animation.setLayer(c, dcc); + + Item item = getItem(comp); + if (item != null) { + animation.getLayer(c).setTransform(item.charColormap, item.charColorIndex); + } + } + };*/ + //Gdx.app.postRunnable(loader); } - Item item = getItem(comp); - if (item != null) { - animation.getLayer(layer.component).setTransform(item.charColormap, item.charColorIndex); - } + //if (BodyLoc.TORS.contains(c)) { + Item item = getItem(comp); + if (item != null) { + // FIXME: colors don't look right for sorc Tirant circlet changing hair color + // putting a ruby in a white circlet not change color on item or character + // circlets and other items with hidden magic level might work different? + animation.getLayer(layer.component).setTransform(item.charColormap, item.charColorIndex); + //System.out.println(item.getName() + ": " + item.charColormap + " ; " + item.charColorIndex); + } + //} } dirty = 0; @@ -272,7 +310,8 @@ public class Entity { } public int getDirection() { - return Direction.radiansToDirection(angle, 16); + int numDirs = animation.getNumDirections(); + return Direction.radiansToDirection(angle, numDirs); } public GridPoint2 origin() { diff --git a/core/src/gdx/diablo/entity3/Player.java b/core/src/gdx/diablo/entity3/Player.java index c3066e08..11102c7b 100644 --- a/core/src/gdx/diablo/entity3/Player.java +++ b/core/src/gdx/diablo/entity3/Player.java @@ -71,7 +71,7 @@ public class Player extends Entity { this.stats = new Stats(); equipped.putAll(d2s.items.equipped); inventory.addAll(d2s.items.inventory); - + setMode("TN"); for (Map.Entry entry : equipped.entrySet()) { entry.getValue().load(); diff --git a/core/src/gdx/diablo/item/BodyLoc.java b/core/src/gdx/diablo/item/BodyLoc.java index 296b53db..f52a9f64 100644 --- a/core/src/gdx/diablo/item/BodyLoc.java +++ b/core/src/gdx/diablo/item/BodyLoc.java @@ -53,4 +53,8 @@ public enum BodyLoc { public int components() { return components; } + + public boolean contains(int component) { + return components != 0 && (components & (1 << component)) != 0; + } } diff --git a/core/src/gdx/diablo/screen/GameScreen.java b/core/src/gdx/diablo/screen/GameScreen.java index 540d2592..60525ed5 100644 --- a/core/src/gdx/diablo/screen/GameScreen.java +++ b/core/src/gdx/diablo/screen/GameScreen.java @@ -68,6 +68,7 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable OrthographicCamera camera; InputProcessor inputProcessorTest; + //TextArea input; //Char character; public Player player; @@ -85,6 +86,19 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable public GameScreen(final Player player) { this.player = player; + /* + input = new TextArea("", new TextArea.TextFieldStyle() {{ + this.font = Diablo.fonts.fontformal12; + this.fontColor = Diablo.colors.white; + this.background = new TextureRegionDrawable(Diablo.textures.modal); + this.cursor = new TextureRegionDrawable(Diablo.textures.white); + }}); + input.setSize(Diablo.VIRTUAL_WIDTH * 0.75f, Diablo.fonts.fontformal12.getLineHeight() * 3); + input.setPosition(Diablo.VIRTUAL_WIDTH_CENTER - input.getWidth() / 2, 100); + input.setAlignment(Align.topLeft); + input.setVisible(false); + */ + escapePanel = new EscapePanel(); controlPanel = new ControlPanel(this); @@ -116,12 +130,14 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable stage = new Stage(Diablo.viewport, Diablo.batch); if (mobilePanel != null) stage.addActor(mobilePanel); + //stage.addActor(input); stage.addActor(controlPanel); stage.addActor(escapePanel); stage.addActor(inventoryPanel); stage.addActor(characterPanel); stage.addActor(stashPanel); controlPanel.toFront(); + //input.toFront(); escapePanel.toFront(); if (Gdx.app.getType() == Application.ApplicationType.Android || DEBUG_TOUCHPAD) { @@ -141,7 +157,7 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable public void changed(ChangeEvent event, Actor actor) { float x = touchpad.getKnobPercentX(); float y = touchpad.getKnobPercentY(); - if (x == 0 && y == 0) { + if (x == 0 && y == 0 || UIUtils.shift()) { player.setMode("TN"); return; //} else if (-0.5f < x && x < 0.5f @@ -171,6 +187,20 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable } else { escapePanel.setVisible(true); } + /*} else if (key == Keys.Enter) { + boolean visible = !input.isVisible(); + if (!visible) { + String text = input.getText(); + if (!text.isEmpty()) { + Gdx.app.debug(TAG, text); + input.setText(""); + } + } + + input.setVisible(visible); + if (visible) { + stage.setKeyboardFocus(input); + }*/ } else if (key == Keys.Inventory) { inventoryPanel.setVisible(!inventoryPanel.isVisible()); } else if (key == Keys.Character) { @@ -292,12 +322,14 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable Keys.Character.addStateListener(mappedKeyStateListener); Keys.Stash.addStateListener(mappedKeyStateListener); Keys.SwapWeapons.addStateListener(mappedKeyStateListener); + Keys.Enter.addStateListener(mappedKeyStateListener); Diablo.input.addProcessor(stage); Diablo.input.addProcessor(inputProcessorTest); updateTask = Timer.schedule(new Timer.Task() { @Override public void run() { + if (UIUtils.shift()) return; player.move(); mapRenderer.setPosition(player.origin()); } @@ -311,6 +343,7 @@ public class GameScreen extends ScreenAdapter implements LoadingScreen.Loadable Keys.Character.removeStateListener(mappedKeyStateListener); Keys.Stash.removeStateListener(mappedKeyStateListener); Keys.SwapWeapons.removeStateListener(mappedKeyStateListener); + Keys.Enter.removeStateListener(mappedKeyStateListener); Diablo.input.removeProcessor(stage); Diablo.input.removeProcessor(inputProcessorTest); diff --git a/core/src/gdx/diablo/screen/LobbyScreen.java b/core/src/gdx/diablo/screen/LobbyScreen.java index 646a7574..c9479902 100644 --- a/core/src/gdx/diablo/screen/LobbyScreen.java +++ b/core/src/gdx/diablo/screen/LobbyScreen.java @@ -7,6 +7,7 @@ import com.badlogic.gdx.ScreenAdapter; import com.badlogic.gdx.assets.AssetDescriptor; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.net.HttpRequestBuilder; +import com.badlogic.gdx.net.Socket; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.EventListener; import com.badlogic.gdx.scenes.scene2d.Group; @@ -24,7 +25,14 @@ import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Json; import com.badlogic.gdx.utils.ObjectMap; +import com.badlogic.gdx.utils.SerializationException; +import org.apache.commons.io.IOUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; import java.net.ConnectException; import java.net.SocketTimeoutException; @@ -34,8 +42,10 @@ import gdx.diablo.graphics.PaletteIndexedBatch; import gdx.diablo.loader.DC6Loader; import gdx.diablo.server.Account; import gdx.diablo.server.Session; +import gdx.diablo.server.SessionError; import gdx.diablo.util.EventUtils; import gdx.diablo.widget.Label; +import gdx.diablo.widget.TextArea; import gdx.diablo.widget.TextButton; import gdx.diablo.widget.TextField; @@ -70,6 +80,13 @@ public class LobbyScreen extends ScreenAdapter { private Stage stage; + private TextArea taChatOutput; + private TextField tfChatInput; + + private Socket socket; + private PrintWriter out; + private BufferedReader in; + public LobbyScreen(Account account) { Diablo.assets.load(waitingroombkgdDescriptor); Diablo.assets.load(blankbckgDescriptor); @@ -262,7 +279,37 @@ public class LobbyScreen extends ScreenAdapter { desc = tfDesc.getText(); }}) .build(); - Gdx.net.sendHttpRequest(request, null); + Gdx.net.sendHttpRequest(request, new Net.HttpResponseListener() { + @Override + public void handleHttpResponse(Net.HttpResponse httpResponse) { + String response = httpResponse.getResultAsString(); + try { + Session session = new Json().fromJson(Session.class, response); + Gdx.app.log(TAG, "create-session " + response); + + Socket socket = null; + try { + socket = Gdx.net.newClientSocket(Net.Protocol.TCP, session.host, session.port, null); + Gdx.app.log(TAG, "create-session connect " + session.host + ":" + session.port + " " + socket.isConnected()); + } finally { + if (socket != null) socket.dispose(); + } + } catch (SerializationException e) { + SessionError error = new Json().fromJson(SessionError.class, response); + Gdx.app.log(TAG, "create-session " + error.toString()); + } + } + + @Override + public void failed(Throwable t) { + Gdx.app.log(TAG, "create-session " + t.getMessage()); + } + + @Override + public void cancelled() { + Gdx.app.log(TAG, "create-session " + "cancelled"); + } + }); } }); tfGameName.addListener(new ChangeListener() { @@ -420,12 +467,54 @@ public class LobbyScreen extends ScreenAdapter { }}; stage.addActor(right); + final Table left = new Table() {{ + setSize(350, 332); + setPosition(55, 115); + add(taChatOutput = new TextArea("", new TextArea.TextFieldStyle() {{ + font = Diablo.fonts.fontformal10; + fontColor = Diablo.colors.white; + cursor = new TextureRegionDrawable(Diablo.textures.white); + }}) {{ + setDisabled(true); + }}).grow().row(); + add(tfChatInput = new TextField("", textFieldStyle) {{ + setTextFieldListener(new TextFieldListener() { + @Override + public void keyTyped(com.badlogic.gdx.scenes.scene2d.ui.TextField textField, char c) { + if (c == '\r' || c == '\n') { + if (socket != null && socket.isConnected()) { + out.println(textField.getText()); + textField.setText(""); + } + } + } + }); + }}).growX(); + }}; + stage.addActor(left); + stage.setKeyboardFocus(tfChatInput); + Diablo.input.addProcessor(stage); + connect(); + } + + private void connect() { + try { + socket = Gdx.net.newClientSocket(Net.Protocol.TCP, "hydra", 6113, null); + in = IOUtils.buffer(new InputStreamReader(socket.getInputStream())); + out = new PrintWriter(socket.getOutputStream(), true); + } catch (Throwable t) { + Gdx.app.error(TAG, t.getMessage()); + taChatOutput.appendText(t.getMessage()); + taChatOutput.appendText("\n"); + } } @Override public void hide() { Diablo.input.removeProcessor(stage); + IOUtils.closeQuietly(out); + if (socket != null) socket.dispose(); } @Override @@ -442,6 +531,17 @@ public class LobbyScreen extends ScreenAdapter { @Override public void render(float delta) { + if (in != null) { + try { + for (String str; in.ready() && (str = in.readLine()) != null;) { + taChatOutput.appendText(str); + taChatOutput.appendText("\n"); + } + } catch (IOException e) { + Gdx.app.error(TAG, e.getMessage()); + } + } + PaletteIndexedBatch b = Diablo.batch; b.begin(Diablo.palettes.act1); b.draw(waitingroombkgd, Diablo.VIRTUAL_WIDTH_CENTER - (waitingroombkgd.getRegionWidth() / 2), Diablo.VIRTUAL_HEIGHT - waitingroombkgd.getRegionHeight() + 72); diff --git a/core/src/gdx/diablo/server/Message.java b/core/src/gdx/diablo/server/Message.java new file mode 100644 index 00000000..f5e0f6af --- /dev/null +++ b/core/src/gdx/diablo/server/Message.java @@ -0,0 +1,10 @@ +package gdx.diablo.server; + +public class Message { + + public String from; + public String text; + + private Message() {} + +} diff --git a/core/src/gdx/diablo/server/Session.java b/core/src/gdx/diablo/server/Session.java index 019f59dc..771420f8 100644 --- a/core/src/gdx/diablo/server/Session.java +++ b/core/src/gdx/diablo/server/Session.java @@ -2,9 +2,11 @@ package gdx.diablo.server; public class Session { - private String name; - private String password; - private String desc; + public String name; + public String password; + public String desc; + public String host; + public int port; private Session() {} @@ -18,9 +20,13 @@ public class Session { desc = builder.desc; } + public String getName() { + return name; + } + @Override public String toString() { - return name; + return getName(); } public static class Builder { diff --git a/core/src/gdx/diablo/server/SessionError.java b/core/src/gdx/diablo/server/SessionError.java new file mode 100644 index 00000000..75080c0d --- /dev/null +++ b/core/src/gdx/diablo/server/SessionError.java @@ -0,0 +1,27 @@ +package gdx.diablo.server; + +public class SessionError { + + private int code; + private String message; + + private SessionError() {} + + public SessionError(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + return code + " " + message; + } +} diff --git a/core/src/gdx/diablo/widget/TextArea.java b/core/src/gdx/diablo/widget/TextArea.java new file mode 100644 index 00000000..e8ce6302 --- /dev/null +++ b/core/src/gdx/diablo/widget/TextArea.java @@ -0,0 +1,26 @@ +package gdx.diablo.widget; + +import com.badlogic.gdx.utils.Disposable; + +public class TextArea extends com.badlogic.gdx.scenes.scene2d.ui.TextArea implements Disposable { + + public TextArea(TextFieldStyle style) { + this("", style); + } + + public TextArea(String text, TextFieldStyle style) { + super(text, style); + } + + @Override + public void dispose() { + + } + + public static class TextFieldStyle extends com.badlogic.gdx.scenes.scene2d.ui.TextArea.TextFieldStyle { + public TextFieldStyle() { + + } + } + +} diff --git a/core/test/gdx/diablo/entity3/EntityTest.java b/core/test/gdx/diablo/entity3/EntityTest.java index bae797ac..c16fc6fe 100644 --- a/core/test/gdx/diablo/entity3/EntityTest.java +++ b/core/test/gdx/diablo/entity3/EntityTest.java @@ -17,7 +17,11 @@ import gdx.diablo.COFs; import gdx.diablo.Diablo; import gdx.diablo.Files; import gdx.diablo.codec.D2S; +import gdx.diablo.codec.DC6; +import gdx.diablo.codec.DCC; import gdx.diablo.codec.StringTBLs; +import gdx.diablo.loader.DC6Loader; +import gdx.diablo.loader.DCCLoader; import gdx.diablo.mpq.MPQFileHandleResolver; public class EntityTest { @@ -33,7 +37,12 @@ public class EntityTest { resolver.add(Gdx.files.absolute("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Diablo II\\patch_d2.mpq")); resolver.add(Gdx.files.absolute("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Diablo II\\d2exp.mpq")); resolver.add(Gdx.files.absolute("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Diablo II\\d2data.mpq")); - Diablo.assets = new AssetManager(resolver); + resolver.add(Gdx.files.absolute("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Diablo II\\d2char.mpq")); + + Diablo.assets = new AssetManager(); + Diablo.assets.setLoader(DCC.class, new DCCLoader(Diablo.mpqs)); + Diablo.assets.setLoader(DC6.class, new DC6Loader(Diablo.mpqs)); + Diablo.cofs = new COFs(Diablo.assets); Diablo.files = new Files(Diablo.assets); Diablo.string = new StringTBLs(resolver); diff --git a/server/build.gradle b/server/build.gradle index 6b4675f1..40099c8b 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -4,7 +4,7 @@ sourceCompatibility = 1.7 sourceSets.main.java.srcDirs = [ "src/" ] sourceSets.test.java.srcDirs = [ "test/" ] -project.ext.mainClassName = "gdx.diablo.server.Server" +project.ext.mainClassName = "gdx.diablo.server.ServerBrowser" project.ext.assetsDir = new File("../android/assets"); task run(dependsOn: classes, type: JavaExec) { diff --git a/server/src/gdx/diablo/server/ChatServer.java b/server/src/gdx/diablo/server/ChatServer.java new file mode 100644 index 00000000..b7601306 --- /dev/null +++ b/server/src/gdx/diablo/server/ChatServer.java @@ -0,0 +1,131 @@ +package gdx.diablo.server; + +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.Json; + +import org.apache.commons.io.IOUtils; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ChatServer extends ApplicationAdapter { + private static final String TAG = "ChatServer"; + + public static void main(String[] args) { + HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); + new HeadlessApplication(new ChatServer(), config); + } + + private final Json json = new Json(); + private Set clients = new CopyOnWriteArraySet<>(); + + ThreadGroup clientThreads; + ServerSocket server; + Thread thread; + AtomicBoolean kill; + + ChatServer() {} + + @Override + public void create() { + 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()); + Gdx.app.log(TAG, "Host Name: " + address.getHostName()); + } catch (UnknownHostException e) { + Gdx.app.error(TAG, e.getMessage(), e); + } + + clientThreads = new ThreadGroup("Clients"); + + kill = new AtomicBoolean(false); + server = Gdx.net.newServerSocket(Net.Protocol.TCP, 6113, null); + thread = new Thread(new Runnable() { + @Override + public void run() { + while (!kill.get()) { + Socket socket = server.accept(null); + Gdx.app.log(TAG, "CONNECT " + socket.getRemoteAddress()); + new Client(socket).start(); + } + } + }); + thread.setName("ChatServer"); + thread.start(); + } + + @Override + public void render() { + + } + + @Override + public void dispose() { + Gdx.app.log(TAG, "shutting down..."); + kill.set(true); + try { + thread.join(); + } catch (Throwable ignored) {} + server.dispose(); + } + + private class Client extends Thread { + Socket socket; + BufferedReader in; + PrintWriter out; + + public Client(Socket socket) { + super(clientThreads, "Client-" + String.format("%08X", MathUtils.random(1, Integer.MAX_VALUE))); + this.socket = socket; + } + + @Override + public void run() { + try { + in = IOUtils.buffer(new InputStreamReader(socket.getInputStream())); + out = new PrintWriter(socket.getOutputStream(), true); + clients.add(out); + + for (String input; (input = in.readLine()) != null; ) { + String message = "MESSAGE " + socket.getRemoteAddress() + ": " + input; + Gdx.app.log(TAG, message); + for (PrintWriter client : clients) { + client.println(message); + } + } + + } catch (Throwable t) { + Gdx.app.log(TAG, "ERROR " + socket.getRemoteAddress() + ": " + t.getMessage()); + } finally { + String message = "DISCONNECT " + socket.getRemoteAddress(); + Gdx.app.log(TAG, message); + for (PrintWriter client : clients) { + client.println(message); + } + //IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + if (out != null) clients.remove(out); + if (socket != null) socket.dispose(); + } + } + } +} diff --git a/server/src/gdx/diablo/server/Server.java b/server/src/gdx/diablo/server/Server.java index cb7c6e73..2d8cd5a3 100644 --- a/server/src/gdx/diablo/server/Server.java +++ b/server/src/gdx/diablo/server/Server.java @@ -1,173 +1,39 @@ package gdx.diablo.server; -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.net.ServerSocket; import com.badlogic.gdx.net.Socket; -import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Json; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.text.DateFormat; -import java.util.Calendar; import java.util.concurrent.atomic.AtomicBoolean; -public class Server extends ApplicationAdapter { +public class Server extends Thread { private static final String TAG = "Server"; - public static void main(String[] args) { - HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); - new HeadlessApplication(new Server(), config); - } - private final Json json = new Json(); - private Array sessions = new Array<>(new Session[] { - new Session("Kmbaal-33"), - new Session("Cbaalz73"), - new Session("Killin Foos"), - new Session("Skulders 4 Scri"), - }); ServerSocket server; - Thread serverThread; - AtomicBoolean killServer; + AtomicBoolean kill; + int port; - Server() {} - - @Override - public void create() { - 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()); - Gdx.app.log(TAG, "Host Name: " + address.getHostName()); - } catch (UnknownHostException e) { - Gdx.app.error(TAG, e.getMessage(), e); - } - - Gdx.app.log(TAG, "awaiting connection..."); - - server = Gdx.net.newServerSocket(Net.Protocol.TCP, 6112, null); - killServer = new AtomicBoolean(false); - serverThread = new Thread(new Runnable() { - @Override - public void run() { - while (!killServer.get()) { - Socket socket = null; - BufferedReader in = null; - PrintWriter out = null; - try { - socket = server.accept(null); - - Gdx.app.log(TAG, "connection from " + socket.getRemoteAddress()); - - in = IOUtils.buffer(new InputStreamReader(socket.getInputStream())); - out = new PrintWriter(socket.getOutputStream(), false); - - String statusLine = in.readLine(); - Gdx.app.log(TAG, statusLine); - String[] parts = statusLine.split("\\s+", 3); - - String path = parts[1]; - if (path.equals("/get-sessions")) { - getSessions(out); - } else if (path.equals("/create-session")) { - createSession(in, out); - } else if (path.equals("/find-server")) { - findServer(out); - } else if (path.equals("/login")) { - login(in, out); - } - } catch (Throwable t) { - Gdx.app.error(TAG, t.getMessage(), t); - } finally { - IOUtils.closeQuietly(out); - //IOUtils.closeQuietly(in); - if (socket != null) socket.dispose(); - } - } - } - }); - serverThread.start(); + Server(ThreadGroup group, String name, int port) { + super(group, name); + this.port = port; + kill = new AtomicBoolean(false); } @Override - public void dispose() { - Gdx.app.log(TAG, "shutting down..."); - killServer.set(true); - try { - serverThread.join(); - } catch (Throwable ignored) {} - server.dispose(); - } - - private void getSessions(PrintWriter out) { - out.print("HTTP/1.1 200\r\n"); - out.print("\r\n"); - out.print(json.toJson(sessions)); - } - - private void createSession(BufferedReader in, PrintWriter out) { - String content = getContent(in); - //System.out.println(content); - Session.Builder builder = json.fromJson(Session.Builder.class, content); - - sessions.add(builder.build()); - - out.print("HTTP/1.1 200\r\n"); - out.print("\r\n"); - out.print(json.toJson(builder.build())); - } - - private void findServer(PrintWriter out) { - out.print("HTTP/1.1 200\r\n"); - out.print("\r\n"); - } - - private void login(BufferedReader in, PrintWriter out) { - String content = getContent(in); - //System.out.println(content); - Account.Builder builder = json.fromJson(Account.Builder.class, content); - - out.print("HTTP/1.1 200\r\n"); - out.print("\r\n"); - out.print(json.toJson(builder.build())); - } - - /** - * TODO: parse packet content-length and read that many chars into a string - */ - public String getContent(BufferedReader reader) { - try { - int length = -1; - for (String str; (str = reader.readLine()) != null && !str.isEmpty();) { - if (StringUtils.startsWithIgnoreCase(str, "Content-Length:")) { - str = StringUtils.replaceIgnoreCase(str, "Content-Length:", "").trim(); - length = NumberUtils.toInt(str, length); - } + public void run() { + server = Gdx.net.newServerSocket(Net.Protocol.TCP, port, null); + while (!kill.get()) { + Socket client = null; + try { + client = server.accept(null); + Gdx.app.log(getName(), "connection from " + client.getRemoteAddress()); + } finally { + if (client != null) client.dispose(); } - //return reader.readLine(); - char[] chars = new char[length]; - reader.read(chars); - return new String(chars); - } catch (IOException e) { - Gdx.app.error(TAG, e.getMessage(), e); - return null; } } } diff --git a/server/src/gdx/diablo/server/ServerBrowser.java b/server/src/gdx/diablo/server/ServerBrowser.java new file mode 100644 index 00000000..d1aa7374 --- /dev/null +++ b/server/src/gdx/diablo/server/ServerBrowser.java @@ -0,0 +1,179 @@ +package gdx.diablo.server; + +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.Json; + +import org.apache.commons.io.IOUtils; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ServerBrowser extends ApplicationAdapter { + private static final String TAG = "Server"; + + public static void main(String[] args) { + HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); + new HeadlessApplication(new ServerBrowser(), config); + } + + private final Json json = new Json(); + private Map sessions = new ConcurrentHashMap<>(); + + ServerSocket server; + Thread thread; + AtomicBoolean kill; + + ThreadGroup sessionGroup = new ThreadGroup("Sessions"); + private Map servers = new ConcurrentHashMap<>(); + + ServerBrowser() {} + + @Override + public void create() { + 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()); + Gdx.app.log(TAG, "Host Name: " + address.getHostName()); + } catch (UnknownHostException e) { + Gdx.app.error(TAG, e.getMessage(), e); + } + + kill = new AtomicBoolean(false); + server = Gdx.net.newServerSocket(Net.Protocol.TCP, 6112, null); + thread = new Thread(new Runnable() { + @Override + public void run() { + while (!kill.get()) { + Socket socket = null; + BufferedReader in = null; + PrintWriter out = null; + try { + socket = server.accept(null); + Gdx.app.log(TAG, "connection from " + socket.getRemoteAddress()); + + in = IOUtils.buffer(new InputStreamReader(socket.getInputStream())); + out = new PrintWriter(socket.getOutputStream(), false); + + String statusLine = in.readLine(); + Gdx.app.log(TAG, statusLine); + String[] parts = statusLine.split("\\s+", 3); + + String path = parts[1]; + if (path.equals("/get-sessions")) { + getSessions(out); + } else if (path.equals("/create-session")) { + createSession(in, out); + } else if (path.equals("/find-server")) { + findServer(out); + } else if (path.equals("/login")) { + login(in, out); + } else if (path.equals("/chat")) { + chat(in, out); + } + } catch (Throwable t) { + Gdx.app.error(TAG, t.getMessage(), t); + } finally { + //IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + if (socket != null) socket.dispose(); + } + } + } + }); + thread.setName("ServerBrowser"); + thread.start(); + } + + @Override + public void render() { + + } + + @Override + public void dispose() { + Gdx.app.log(TAG, "shutting down..."); + kill.set(true); + try { + thread.join(); + } catch (Throwable ignored) {} + server.dispose(); + } + + private void getSessions(PrintWriter out) { + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + out.print(json.toJson(sessions.values())); + } + + private void createSession(BufferedReader in, PrintWriter out) { + String content = ServerUtils.getContent(in); + Session.Builder builder = json.fromJson(Session.Builder.class, content); + + if (sessions.containsKey(builder.name)) { + SessionError error = new SessionError(5138, "A game already exists with that name"); + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + out.print(json.toJson(error)); + return; + } else if (sessions.size() >= 2) { + SessionError error = new SessionError(5140, "No game server available"); + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + out.print(json.toJson(error)); + return; + } + + Session session = builder.build(); + session.host = "hydra"; + session.port = 6114 + sessions.size(); + sessions.put(session.getName(), session); + + String id = String.format("%08x", MathUtils.random(Integer.MAX_VALUE - 1)); + Server server = new Server(sessionGroup, "Session-" + id, session.port); + server.start(); + servers.put(session.getName(), server); + + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + out.print(json.toJson(session)); + } + + private void findServer(PrintWriter out) { + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + } + + private void login(BufferedReader in, PrintWriter out) { + String content = ServerUtils.getContent(in); + //System.out.println(content); + Account.Builder builder = json.fromJson(Account.Builder.class, content); + + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + out.print(json.toJson(builder.build())); + } + + private void chat(BufferedReader in, PrintWriter out) { + out.print("HTTP/1.1 200\r\n"); + out.print("\r\n"); + } +} diff --git a/server/src/gdx/diablo/server/ServerUtils.java b/server/src/gdx/diablo/server/ServerUtils.java new file mode 100644 index 00000000..897322ca --- /dev/null +++ b/server/src/gdx/diablo/server/ServerUtils.java @@ -0,0 +1,35 @@ +package gdx.diablo.server; + +import com.badlogic.gdx.Gdx; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.io.BufferedReader; +import java.io.IOException; + +public class ServerUtils { + private static final String TAG = "ServerUtils"; + + private ServerUtils() {} + + public static String getContent(BufferedReader reader) { + try { + int length = -1; + for (String str; (str = reader.readLine()) != null && !str.isEmpty();) { + if (StringUtils.startsWithIgnoreCase(str, "Content-Length:")) { + str = StringUtils.replaceIgnoreCase(str, "Content-Length:", "").trim(); + length = NumberUtils.toInt(str, length); + } + } + + char[] chars = new char[length]; + reader.read(chars); + return new String(chars); + } catch (IOException e) { + Gdx.app.error(TAG, e.getMessage(), e); + return null; + } + } + +}