package ch.epfl.xblast.client; import ch.epfl.xblast.Lists; import ch.epfl.xblast.PlayerAction; import ch.epfl.xblast.PlayerID; import ch.epfl.xblast.Time; import ch.epfl.xblast.server.Server; import javax.swing.*; import java.awt.*; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.StandardProtocolFamily; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.util.*; import java.util.List; import java.util.function.Consumer; /** * The Client class. * * @author Pacien TRAN-GIRARD (261948) */ public class Client { /** * Client's parameters. */ private static final String DEFAULT_SERVER_HOST = "localhost"; private static final int DEFAULT_SERVER_PORT = Server.DEFAULT_PORT; private static final int PACKET_MAX_SIZE = 1000; private static final long REGISTER_PING_INTERVAL = 1 * Time.NS_PER_S; // ns /** * Communication channel. */ private static class Channel { /** * Transform a buffer to a list of bytes. * * @param buf given buffer * @return the list of bytes built from the given buffer */ private static List bufferToList(ByteBuffer buf) { List l = new ArrayList<>(buf.remaining()); while (buf.hasRemaining()) l.add(buf.get()); return Collections.unmodifiableList(l); } /** * Create the UDP communication Channel. * * @return the UDP communication Channel */ private static DatagramChannel openChannel() { try { return DatagramChannel.open(StandardProtocolFamily.INET); } catch (IOException e) { e.printStackTrace(); System.exit(1); return null; } } /** * Parameters of the Channel. */ private final SocketAddress serverAddr; private final DatagramChannel channel; /** * Instantiates a new Channel. * * @param iface socket */ Channel(InetSocketAddress iface) { this.serverAddr = iface; this.channel = openChannel(); } /** * Instantiates a new Channel. * * @param host hostname * @param port port */ Channel(String host, Integer port) { this(new InetSocketAddress( Optional.ofNullable(host).orElse(DEFAULT_SERVER_HOST), Optional.ofNullable(port).orElse(DEFAULT_SERVER_PORT))); } /** * Close the Channel. */ void closeChannel() { try { this.channel.close(); } catch (IOException e) { e.printStackTrace(); } } /** * Send an action through the Channel. * * @param action action to send */ void sendAction(PlayerAction action) { this.sendByte(action.toByte()); } /** * Receive a GameState through the Channel. * * @param block * @return the received GameState */ List receiveGameState(boolean block) { Optional> state; do { state = this.receive(block); } while (!state.isPresent()); return state.get(); } /** * Send a byte through the Channel. * * @param b byte to send */ private void sendByte(byte b) { this.send(ByteBuffer.wrap(new byte[]{b})); } /** * Send the content of a buffer. * * @param b buffer containing the bytes to send */ private void send(ByteBuffer b) { try { this.channel.send(b, this.serverAddr); } catch (IOException e) { e.printStackTrace(); } } /** * Receive data from the Channel. * * @param block * @return the received data */ private Optional> receive(boolean block) { try { ByteBuffer buf = ByteBuffer.allocate(PACKET_MAX_SIZE); this.channel.configureBlocking(block); this.channel.receive(buf); buf.flip(); return Optional.of(bufferToList(buf)); } catch (IOException e) { e.printStackTrace(); return Optional.empty(); } } } /** * The Graphical User Interface. */ private static class GUI { /** * Build the window. * * @param content content of the window * @return the frame */ private static JFrame buildFrame(Container content) { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); frame.setContentPane(content); frame.pack(); return frame; } /** * Attach the keyboard events handler to the component. * @param comp component * @param actionConsumer actionConsumer corresponding to the keyboardEventHandler */ private static void attachKeyboardHandler(Component comp, Consumer actionConsumer) { comp.addKeyListener(new KeyboardEventHandler(actionConsumer)); comp.requestFocusInWindow(); } /** * GUI's parameters. */ private final XBlastComponent gameComponent; private final Consumer actionConsumer; /** * Instantiates a new GUI. * * @param actionConsumer actionConsumer corresponding to the keyboardEventHandler */ GUI(Consumer actionConsumer) { this.gameComponent = new XBlastComponent(); this.actionConsumer = actionConsumer; } /** * Build and display the GUI. */ public void display() { buildFrame(this.gameComponent); attachKeyboardHandler(this.gameComponent, this.actionConsumer); } /** * Update the displayed GameState. * * @param gs new GameState to be displayed * @param p Player ID corresponding to the client */ public void setGameState(GameState gs, PlayerID p) { this.gameComponent.setGameState(gs, p); } } /** * Deserialize the players' IDs. * * @param b a given byte * @return the Player ID corresponding to the given byte */ private static PlayerID deserializePlayerID(byte b) { if (b == Server.OBSERVER) return null; try { return PlayerID.fromByte(b); } catch (IllegalArgumentException e) { return null; } } /** * Deserialize a received GameState. * * @param b the serialized GameState * @return the deserialized GameState */ private static GameState deserializeGameState(List b) { try { return GameStateDeserializer.deserialize(b); } catch (IllegalArgumentException e) { return null; } } /** * Client's parameters. */ private final Channel channel; private final GUI gui; /** * Instatiates a new client. * * @param host hostname * @param port port */ public Client(String host, Integer port) { this.channel = new Channel(host, port); this.gui = new GUI(this.channel::sendAction); } /** * Run the whole client. */ public void run() { this.displayGUI(); this.establishConnection(); this.runGame(); this.channel.closeChannel(); } /** * Display the Graphical User Interface. */ private void displayGUI() { try { SwingUtilities.invokeAndWait(this.gui::display); } catch (InterruptedException | InvocationTargetException e) { e.printStackTrace(); System.exit(1); } } /** * Launch the game. */ private void runGame() { while (true) updateGameState(); } /** * Connect to the server and fetch the new GameState. */ private void establishConnection() { Thread joinThread = new Thread(this::sendJoinRequest); joinThread.start(); updateGameState(); joinThread.interrupt(); } /** * Fetch the new GameState. */ private void updateGameState() { List serializedGameState = this.channel.receiveGameState(true); if (serializedGameState.size() < 1) return; PlayerID player = deserializePlayerID(serializedGameState.get(0)); GameState gameState = deserializeGameState(Lists.firstDropped(serializedGameState, 1)); if (Objects.isNull(gameState)) return; this.gui.setGameState(gameState, player); } /** * Send a "Join" request to the server. */ private void sendJoinRequest() { while (!Thread.currentThread().isInterrupted()) { this.channel.sendAction(PlayerAction.JOIN_GAME); Time.sleep(REGISTER_PING_INTERVAL); } } }