Java tutorial
/* * Copyright 2014 StormCloud Development Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.github.stormcloud_dev.stormcloud; import io.github.stormcloud_dev.stormcloud.event.player.*; import io.github.stormcloud_dev.stormcloud.frame.HandshakeFrame; import io.github.stormcloud_dev.stormcloud.frame.clientbound.*; import io.github.stormcloud_dev.stormcloud.frame.serverbound.*; import io.github.stormcloud_dev.stormcloud.object.Enemy; import io.github.stormcloud_dev.stormcloud.object.Player; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerAdapter; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.group.ChannelGroup; import io.netty.channel.group.DefaultChannelGroup; import io.netty.util.concurrent.GlobalEventExecutor; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static io.github.stormcloud_dev.stormcloud.Attribute.PLAYER; import static io.netty.channel.ChannelHandler.Sharable; import static java.util.concurrent.TimeUnit.SECONDS; @Sharable public class StormCloudHandler extends ChannelHandlerAdapter { private ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); public ChannelGroup getChannels() { return channels; } private ConcurrentHashMap<Double, Player> playerList; public ConcurrentHashMap<Double, Player> getPlayers() { return playerList; } private ConcurrentHashMap<String, Player> disconnectedPlayerList; public ConcurrentHashMap<String, Player> getDisconnectedPlayers() { return disconnectedPlayerList; } private volatile boolean ingame; private StormCloud server; public StormCloudHandler(StormCloud server) { this.server = server; this.playerList = new ConcurrentHashMap<>(); this.disconnectedPlayerList = new ConcurrentHashMap<>(); } public void scheduleTestPacket(Channel channel) { server.getTimer().newTimeout(timeout -> { Player player = channel.attr(PLAYER).get(); if (channel.isActive()) { channel.writeAndFlush(new TestClientBoundFrame(player.getObjectIndex(), player.getMId())); if (player.getRoom() != null) { player.getRoom().getObjects().stream().filter(object -> object instanceof Enemy) .forEach(object -> { Enemy enemy = (Enemy) object; enemy.setX(enemy.getX() + 1); enemy.setHp((short) (enemy.getHp() - 1)); if (enemy.getHp() > 0) { channel.writeAndFlush(new KeyMonsterClientBoundFrame(220.0, enemy.getMId(), enemy.getX(), enemy.getY(), (byte) 0, (byte) 0, (byte) 0, (byte) 0, (short) 1)); channel.writeAndFlush( new NPCHPClientBoundFrame(220.0, enemy.getMId(), enemy.getHp(), enemy.getX(), enemy.getY(), (short) 1, (short) 1, (byte) 0)); } else { channel.writeAndFlush(new MDeadClientBoundFrame(220.0, enemy.getMId())); } }); } scheduleTestPacket(channel); } }, 1, SECONDS); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { server.getLogger().info("Channel active: " + ctx.channel()); //Random random = new Random(); //Multiplayer ID's are starting at 9 Player player = new Player(server, getPlayerId(9.0, ctx.channel().remoteAddress().toString().split(":")[0]), 210.0, ctx.channel().remoteAddress().toString().split(":")[0]); player.setChannel(ctx.channel()); ctx.channel().attr(PLAYER).set(player); channels.add(ctx.channel()); ctx.writeAndFlush(Unpooled.wrappedBuffer("GM:Studio-Connect\u0000".getBytes("utf8"))); //Thread that tests if the connection is alive (The client needs that, else it will disconnect) Thread testThread = new Thread(() -> { Channel channel = ctx.channel(); scheduleTestPacket(channel); }); testThread.start(); } //The id is the start id, if its used it checks the next public double getPlayerId(Double id, String ip) { if (disconnectedPlayerList.containsKey(ip)) { return disconnectedPlayerList.get(ip).getMId(); } return getPlayerId(id); } public double getPlayerId(Double id) { ConcurrentHashMap<Double, Player> allPlayers = new ConcurrentHashMap<>(); allPlayers.putAll(playerList); for (Map.Entry<String, Player> entry : disconnectedPlayerList.entrySet()) { allPlayers.put(entry.getValue().getMId(), entry.getValue()); } for (Map.Entry<Double, Player> entry : allPlayers.entrySet()) { if (entry.getValue().getMId() == id) { return getPlayerId(id + 1); } } return id; } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { server.getLogger().info("Channel inactive: " + ctx.channel()); //Removing the player Player player = ctx.channel().attr(PLAYER).get(); if (player != null) { PlayerRemoveEvent event = new PlayerRemoveEvent(player); server.getEventManager().onEvent(event); server.getLogger().info(event.getPlayer().getName() + " left the game!"); server.getChannels().stream().filter(channel -> !channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { channel.writeAndFlush(new DisPlayerClientBoundFrame(event.getPlayer().getObjectIndex(), event.getPlayer().getMId())); channel.writeAndFlush(new ChatPlayerClientBoundFrame(event.getPlayer().getObjectIndex(), 0.0, "'" + event.getPlayer().getName() + "' has left the game!")); }); server.getDisconnectedPlayers().put(event.getPlayer().getLogin(), event.getPlayer()); server.getPlayers().remove(event.getPlayer().getMId()); } } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //The player that send a frame Player sender = ctx.channel().attr(PLAYER).get(); if (msg instanceof HandshakeFrame) { //These packets have to send in the exact order with exactly these values, else the client gets a black screen... ctx.writeAndFlush( Unpooled.wrappedBuffer(new byte[] { -83, -66, -81, -34, -21, -66, 13, -16, 12, 0, 0, 0 })); //40 = Room ID of Lobby, 15 = Object Index for set player ctx.writeAndFlush(new SetPlayerClientBoundFrame(15.0, 0.0, sender.getMId(), 40.0, "v1.2.4")); ctx.writeAndFlush(new UpdateDiffClientBoundFrame(0.0, 0.0, (byte) 2, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0)); //We tell the player listener that there's a new player PlayerAddEvent event = new PlayerAddEvent(sender); server.getEventManager().onEvent(new PlayerAddEvent(sender)); server.getLogger().info(event.getPlayer().getName() + " joined the game!"); server.getPlayers().put(event.getPlayer().getMId(), event.getPlayer()); server.getChannels().stream().forEach(channel -> { //Telling the clients that there's a new player and informing the new player about current players if (channel.attr(PLAYER).get().equals(event.getPlayer())) { //Sending the new player, which players are already in the game server.getChannels().stream().filter(playerChannel -> !playerChannel.equals(channel)) .forEach(playerChannel -> { Player currentPlayer = playerChannel.attr(PLAYER).get(); channel.writeAndFlush(new AddPlayerClientBoundFrame(currentPlayer.getObjectIndex(), currentPlayer.getMId(), 0.0, 0.0, currentPlayer.getMId(), currentPlayer.getClazz(), 0, currentPlayer.getName())); }); } else { //Sending each player that a new player joined the game channel.writeAndFlush(new AddPlayerClientBoundFrame(event.getPlayer().getObjectIndex(), event.getPlayer().getMId(), 0.0, 0.0, event.getPlayer().getMId(), event.getPlayer().getClazz(), 0, event.getPlayer().getName())); } }); } else if (msg instanceof ChatPlayerServerBoundFrame) { ChatPlayerServerBoundFrame frame = (ChatPlayerServerBoundFrame) msg; if (frame.getText().startsWith("!")) { String[] messageParts = frame.getText().split("\\s+"); String[] args = new String[messageParts.length - 1]; System.arraycopy(messageParts, 1, args, 0, messageParts.length - 1); server.getCommandManager().onCommand(sender, messageParts[0].substring(1), args); } else { PlayerChatEvent event = new PlayerChatEvent(sender, frame); server.getEventManager().onEvent(event); getChannels().stream().filter(channel -> channel != ctx.channel()) .forEach(channel -> channel .writeAndFlush(new ChatPlayerClientBoundFrame(event.getPlayer().getObjectIndex(), event.getPlayer().getMId(), event.getMessage()))); } } else if (msg instanceof UpdatePlayerServerBoundFrame) { UpdatePlayerServerBoundFrame frame = (UpdatePlayerServerBoundFrame) msg; PlayerUpdateEvent event = new PlayerUpdateEvent(sender, frame); server.getEventManager().onEvent(event); event.getPlayer().setName(event.getFrame().getName().replace("|", "")); //event.getPlayer().setLogin(event.getPlayer().getName()); if (ingame) { //Ingame -> direct transport to the map if (event.getFrame().getClazz() == -1) { //The first player update says that this player has no clazz if (server.getDisconnectedPlayers().containsKey(event.getPlayer().getLogin())) { //If there was a player on that slot he has to get the same clazz etc. //event.getPlayer().setMId(oldPlayerList.get(event.getPlayer().getLogin()).getClazz()); event.getPlayer().setClazz( server.getDisconnectedPlayers().get(event.getPlayer().getLogin()).getClazz()); event.getPlayer() .setX(server.getDisconnectedPlayers().get(event.getPlayer().getLogin()).getX()); event.getPlayer() .setY(server.getDisconnectedPlayers().get(event.getPlayer().getLogin()).getY()); server.getDisconnectedPlayers().remove(event.getPlayer().getLogin()); } //Only to the new player server.getChannels().stream() .filter(channel -> channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { //We have to update the positions of the other players if we join mid game server.getPlayers().entrySet().stream() .filter(entry -> !entry.getValue().equals(event.getPlayer())) .forEach(entry -> { Player player = entry.getValue(); channel.writeAndFlush(new UpdatePlayerClientBoundFrame(210.0, player.getMId(), player.getClazz(), player.getX(), player.getY(), player.getName() + "|")); }); //We have to setup the disconnected players too (If more than one player disconnects) server.getDisconnectedPlayers().entrySet().stream() .filter(entry -> !entry.getValue().equals(event.getPlayer())) .forEach(entry -> { Player player = entry.getValue(); channel.writeAndFlush(new UpdatePlayerClientBoundFrame(210.0, player.getMId(), player.getClazz(), player.getX(), player.getY(), player.getName() + "|")); }); //Setup the old character status and go ingame channel.writeAndFlush( new UpdatePlayerClientBoundFrame(210.0, event.getPlayer().getMId(), event.getPlayer().getClazz(), event.getPlayer().getX(), event.getPlayer().getY(), event.getPlayer().getName() + "|")); channel.writeAndFlush(new CrewChoiceClientBoundFrame(0.0, 0.0, (short) 2)); channel.writeAndFlush( new TransportClientBoundFrame(0.0, 0.0, 23.0, event.getPlayer().getX(), event.getPlayer().getY(), 2440.0, 832.0, (byte) 0)); //We have to update the positions of the other players if we join mid game server.getPlayers().entrySet().stream() .filter(entry -> !entry.getValue().equals(event.getPlayer())) .forEach(entry -> { Player player = entry.getValue(); channel.writeAndFlush(new PositionInfoClientBoundFrame(167.0, player.getMId(), player.getX(), player.getY(), (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0)); }); //We also update the positions of the other disconnected players server.getDisconnectedPlayers().entrySet().stream() .filter(entry -> !entry.getValue().equals(event.getPlayer())) .forEach(entry -> { Player player = entry.getValue(); channel.writeAndFlush(new PositionInfoClientBoundFrame(167.0, player.getMId(), player.getX(), player.getY(), (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0)); }); }); return; // We won't remove the character selection if it was already set } } server.getLogger() .info(event.getPlayer().getName() + " changed class to " + event.getFrame().getClazz()); event.getPlayer().setClazz( (event.getFrame().getClazz() != -1 ? CrewMember.values()[event.getFrame().getClazz()] : null)); server.getChannels().stream().filter(channel -> !channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { // Object Index of 210 required (seems to be the index for player updates) channel.writeAndFlush(new UpdatePlayerClientBoundFrame(210.0, event.getPlayer().getMId(), event.getFrame().getClazz(), event.getFrame().getX(), event.getFrame().getY(), event.getFrame().getName())); }); } else if (msg instanceof LagPlayerServerBoundFrame) { PlayerLagEvent event = new PlayerLagEvent(sender); server.getEventManager().onEvent(event); getChannels().stream().filter(channel -> channel != ctx.channel()) .forEach(channel -> channel .writeAndFlush(new LagPlayerClientBoundFrame(event.getPlayer().getObjectIndex(), event.getPlayer().getMId(), event.getPlayer().getName()))); } else if (msg instanceof SetReadyServerBoundFrame) { SetReadyServerBoundFrame frame = (SetReadyServerBoundFrame) msg; PlayerReadyChangeEvent event = new PlayerReadyChangeEvent(sender, frame); server.getEventManager().onEvent(event); server.getLogger().info(event.getPlayer().getName() + " is ready!"); server.getPlayers().get(event.getPlayer().getMId()).setReady(event.isReady()); //Check if all players are ready boolean allReady = true; for (Map.Entry<Double, Player> entry : server.getPlayers().entrySet()) { if (!entry.getValue().isReady()) { allReady = false; } } if (ingame) { //When we are ingame, players have to take the slots of left players (Can't add new players) server.getChannels().stream() .filter(channel -> channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { channel.writeAndFlush(new CrewChoiceClientBoundFrame(0.0, 0.0, (short) 2)); channel.writeAndFlush(new TransportClientBoundFrame(0.0, 0.0, 23.0, event.getPlayer().getX(), event.getPlayer().getY(), 2440.0, 832.0, (byte) 0)); //We have to update the positions of the other players if we join mid game server.getChannels().stream().filter( playerChannel -> !playerChannel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(playerChannel -> { Player player = playerChannel.attr(PLAYER).get(); channel.writeAndFlush(new PositionInfoClientBoundFrame(167.0, player.getMId(), player.getX(), player.getY(), (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0, (byte) 0)); }); }); } else if (allReady) { //If all players are ready, we send the frames to start the game server.getLogger().info("All players are ready... game starting!"); //Random random = new Random(); server.getChannels().stream().forEach(channel -> { //Player player = channel.attr(PLAYER).get(); //TODO: Level generation channel.writeAndFlush(new CrewChoiceClientBoundFrame(0.0, 0.0, (short) 2)); //First double = 40 means lobby //Levels start at 18 channel.writeAndFlush( new TransportClientBoundFrame(0.0, 0.0, 23.0, 1216.0, 736.0, 2440.0, 832.0, (byte) 0)); //channel.writeAndFlush(new TransportClientBoundFrame(0.0, 0.0, 23.0, 1216.0, 736.0, 2440.0, 832.0, (byte) 0)); }); this.ingame = true; } else { //Telling the other clients that someone pressed ready server.getChannels().stream() .filter(channel -> !channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { channel.writeAndFlush(new SetReadyClientBoundFrame(event.getPlayer().getObjectIndex(), event.getPlayer().getMId(), (byte) (event.getPlayer().isReady() ? 1 : 0))); }); } } else if (msg instanceof PositionInfoServerBoundFrame) { PositionInfoServerBoundFrame frame = (PositionInfoServerBoundFrame) msg; PlayerPositionEvent event = new PlayerPositionEvent(sender, frame); server.getEventManager().onEvent(event); event.getPlayer().setX((int) event.getX()); event.getPlayer().setY((int) event.getY()); getChannels().stream().filter(channel -> !channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { // Object Index of 167 required (seems to be the index for position updates) channel.writeAndFlush(new PositionInfoClientBoundFrame(167.0, event.getPlayer().getMId(), event.getX(), event.getY(), event.isLeft() ? (byte) 1 : (byte) 0, event.isRight() ? (byte) 1 : (byte) 0, event.isJump() ? (byte) 1 : (byte) 0, event.isJumpHeld() ? (byte) 1 : (byte) 0, event.isUp() ? (byte) 1 : (byte) 0, event.isDown() ? (byte) 1 : (byte) 0)); }); } else if (msg instanceof KeyPlayerServerBoundFrame) { KeyPlayerServerBoundFrame frame = (KeyPlayerServerBoundFrame) msg; PlayerKeyEvent event = new PlayerKeyEvent(sender, frame); server.getEventManager().onEvent(event); getChannels().stream().filter(channel -> !channel.attr(PLAYER).get().equals(event.getPlayer())) .forEach(channel -> { //Object Index of 167 required (seems to be the index for position updates) channel.writeAndFlush(new KeyPlayerClientBoundFrame(167.0, event.getPlayer().getMId(), event.getFrame().getX(), event.getFrame().getY(), event.getFrame().getZAction(), event.getFrame().getXAction(), event.getFrame().getCAction(), event.getFrame().getVAction(), event.getFrame().getItemUsed(), event.getFrame().getUnknown())); }); } else { server.getLogger().info(msg.getClass().getSimpleName()); } // if (msg instanceof ByteBuf) { // ByteBuf buf = (ByteBuf) msg; // byte[] bytes = new byte[buf.readableBytes()]; // buf.readBytes(bytes); // System.out.println(join(bytes, ", ")); // if (bytes.length > 12) { // byte id = bytes[12]; // byte length = (byte) Math.min(bytes[8], bytes.length - 8); // byte[] data = new byte[length]; // System.arraycopy(bytes, bytes.length - length, data, 0, length); // System.out.printf("id: %d\nlength: %d\ndata: %s\n\n", id, length, join(data, ", ")); // handlePacket(ctx, id, data); // } // } } // private void handlePacket(ChannelHandlerContext ctx, byte id, byte[] data) throws UnsupportedEncodingException { // if (id == 4) { // // Chat message // byte[] chatMessage = new byte[data[8]]; // System.arraycopy(data, data.length - data[8], chatMessage, 0, data[8]); // System.out.println("Chat message: " + new String(chatMessage, "utf8")); // } else if (id == 5) { // // Character selection // // } else if (id == 0) { // // Join // ctx.writeAndFlush(Unpooled.wrappedBuffer(new byte[] {-83, -66, -81, -34, -21, -66, 13, -16, 12, 0, 0, 0})); // } // } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); } // private String join(byte[] bytes, String separator) { // StringBuilder builder = new StringBuilder(); // for (byte b : bytes) { // builder.append(b).append(separator); // } // if (builder.length() > 0) builder.delete(builder.length() - 2, builder.length()); // return builder.toString(); // } }