package com.cardshifter.client;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ResourceBundle;
import java.util.Set;
import com.cardshifter.api.config.PlayerConfig;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import com.cardshifter.api.config.DeckConfig;
import net.zomis.cardshifter.ecs.usage.CardshifterIO;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.cardshifter.api.both.ChatMessage;
import com.cardshifter.api.both.InviteRequest;
import com.cardshifter.api.both.InviteResponse;
import com.cardshifter.api.both.PlayerConfigMessage;
import com.cardshifter.api.incoming.LoginMessage;
import com.cardshifter.api.incoming.ServerQueryMessage;
import com.cardshifter.api.incoming.ServerQueryMessage.Request;
import com.cardshifter.api.incoming.StartGameRequest;
import com.cardshifter.api.messages.Message;
import com.cardshifter.api.outgoing.AvailableModsMessage;
import com.cardshifter.api.outgoing.NewGameMessage;
import com.cardshifter.api.outgoing.ServerErrorMessage;
import com.cardshifter.api.outgoing.UserStatusMessage;
import com.cardshifter.api.outgoing.UserStatusMessage.Status;
import com.cardshifter.client.buttons.GameTypeButton;
import com.cardshifter.client.buttons.GenericButton;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
public class GameClientLobby implements Initializable {
private static final Logger logger = LogManager.getLogger(GameClientLobby.class);
@FXML private AnchorPane rootPane;
@FXML private ListView<String> usersOnline;
@FXML private ListView<String> chatMessages;
@FXML private TextField messageBox;
@FXML private Button sendMessageButton;
@FXML private AnchorPane inviteButton;
@FXML private AnchorPane inviteWindow;
@FXML private HBox gameTypeBox;
@FXML private AnchorPane deckBuilderButton;
private final ObjectMapper mapper = CardshifterIO.mapper();
private final Set<GameClientController> gamesRunning = new HashSet<>();
private Socket socket;
private InputStream in;
private OutputStream out;
private String ipAddress;
private int port;
private String userName;
private Thread listenThread;
private final Map<String, Integer> usersOnlineList = new HashMap<>();
private String userForGameInvite;
private InviteRequest currentGameRequest;
private final List<String> gameTypes = new ArrayList<>();
private String selectedGameType;
private PlayerConfigMessage currentPlayerConfig;
private DeckBuilderWindow openDeckBuilderWindow;
public void acceptConnectionSettings(String ipAddress, int port, String userName) {
// this is passed into this object after it is automatically created by the FXML document
this.ipAddress = ipAddress;
this.port = port;
this.userName = userName;
}
public boolean connectToLobby() {
// this is called on the object before the scene is displayed
try {
this.socket = new Socket(this.ipAddress, this.port);
this.out = socket.getOutputStream();
this.in = socket.getInputStream();
this.listenThread = new Thread(this::listen);
this.listenThread.start();
} catch (IOException ex) {
logger.info("Connection Failed");
return false;
}
this.sendLoginMessage();
this.sendServerQueryMessage();
this.usersOnline.setOnMouseClicked(this::selectUserForGameInvite);
this.inviteButton.setOnMouseClicked(this::startGameWithUser);
this.deckBuilderButton.setOnMouseClicked(this::openDeckBuilderWindowWithoutGame);
return true;
}
private void listen() {
while (true) {
if (socket.isClosed()) {
chatOutput("Connection Closed");
break;
}
try {
MappingIterator<Message> values = mapper.readValues(new JsonFactory().createParser(this.in), Message.class);
while (values.hasNextValue()) {
Message message = values.next();
//This is where all the magic happens for message handling
Platform.runLater(() -> this.processMessageFromServer(message));
}
} catch (SocketException e) {
this.chatOutput("Error receiving message: " + e.getMessage());
return;
} catch (IOException e) {
this.chatOutput("Lost connection to server");
}
}
}
private void send(Message message) {
try {
logger.info("Sending: " + this.mapper.writeValueAsString(message));
this.mapper.writeValue(out, message);
} catch (IOException e) {
logger.info("Error sending message: " + message);
throw new RuntimeException(e);
}
}
private void sendLoginMessage() {
LoginMessage loginMessage = new LoginMessage(this.userName);
this.send(loginMessage);
}
private void sendServerQueryMessage() {
ServerQueryMessage initialMessage = new ServerQueryMessage(Request.USERS);
this.send(initialMessage);
}
private void processMessageFromServer(Message message) {
//this is for diagnostics so I can copy paste the messages to know their format
logger.info(message);
for (GameClientController gameController : this.gamesRunning) {
gameController.processMessageFromServer(message);
}
if (message instanceof NewGameMessage) {
this.startNewGame((NewGameMessage)message);
} else if (message instanceof UserStatusMessage) {
this.processUserStatusMessage((UserStatusMessage)message);
} else if (message instanceof ChatMessage) {
ChatMessage msg = (ChatMessage) message;
this.chatOutput(msg.getFrom() + ": " + msg.getMessage());
} else if (message instanceof ServerErrorMessage) {
ServerErrorMessage msg = (ServerErrorMessage) message;
this.chatOutput("SERVER ERROR: " + msg.getMessage());
} else if (message instanceof PlayerConfigMessage) {
PlayerConfigMessage msg = (PlayerConfigMessage) message;
this.showConfigDialog(msg);
} else if (message instanceof InviteRequest) {
this.currentGameRequest = (InviteRequest)message;
this.createInviteWindow((InviteRequest)message);
} else if (message instanceof AvailableModsMessage) {
this.gameTypes.addAll(Arrays.asList(((AvailableModsMessage)message).getMods()));
this.createGameTypeButtons();
}
}
private void showConfigDialog(PlayerConfigMessage configMessage) {
this.currentPlayerConfig = configMessage;
Map<String, PlayerConfig> configs = configMessage.getConfigs();
for (Entry<String, PlayerConfig> entry : configs.entrySet()) {
Object value = entry.getValue();
if (value instanceof DeckConfig) {
DeckConfig deckConfig = (DeckConfig) value;
this.showDeckBuilderWindow(deckConfig, configMessage.getModName(), true);
}
}
}
public void sendDeckAndPlayerConfigToServer(DeckConfig deckConfig) {
Map<String, PlayerConfig> configs = this.currentPlayerConfig.getConfigs();
for (Entry<String, PlayerConfig> entry : configs.entrySet()) {
Object value = entry.getValue();
if (value instanceof DeckConfig) {
DeckConfig config = (DeckConfig) value;
deckConfig.getChosen().forEach((id, count) -> config.setChosen(id, count));
}
}
this.send(new PlayerConfigMessage(this.currentPlayerConfig.getGameId(), currentPlayerConfig.getModName(), configs));
}
private void openDeckBuilderWindowWithoutGame(MouseEvent event) {
this.send(new ServerQueryMessage(Request.DECK_BUILDER, selectedGameType));
}
private void showDeckBuilderWindow(DeckConfig deckConfig, String modName, boolean startingGame) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("DeckBuilderDocument.fxml"));
Parent root = (Parent)loader.load();
DeckBuilderWindow controller = loader.<DeckBuilderWindow>getController();
controller.acceptDeckConfig(deckConfig, modName, conf -> this.sendDeckAndPlayerConfigToServer(conf));
controller.configureWindow();
this.openDeckBuilderWindow = controller;
if (!startingGame) {
controller.disableGameStart();
}
Scene scene = new Scene(root);
Stage stage = new Stage();
stage.setScene(scene);
stage.setOnCloseRequest(windowEvent -> this.closeDeckBuilderWindow());
stage.show();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private void startNewGame(NewGameMessage message) {
if (!gamesRunning.isEmpty()) {
this.chatOutput("You already have a running game. Unable to start a new one.");
return;
}
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("ClientDocument.fxml"));
Parent root = (Parent)loader.load();
GameClientController controller = loader.<GameClientController>getController();
this.gamesRunning.add(controller);
controller.acceptConnectionSettings(message, this::send);
Scene scene = new Scene(root);
Stage gameStage = new Stage();
gameStage.setScene(scene);
gameStage.setOnCloseRequest(windowEvent -> this.closeController(controller));
gameStage.show();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private void processUserStatusMessage(UserStatusMessage message) {
if (message.getStatus() == Status.ONLINE) {
this.usersOnlineList.put(message.getName(), message.getUserId());
} else if (message.getStatus() == Status.OFFLINE) {
this.usersOnlineList.remove(message.getName());
chatOutput(message.getName() + " is now offline.");
}
this.usersOnline.getItems().clear();
this.usersOnline.getItems().addAll(this.usersOnlineList.keySet());
}
private void chatOutput(String string) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
String time = "[" + formatter.format(Instant.now()) + "] ";
Platform.runLater(() -> this.chatMessages.getItems().add(time + string));
}
private void createInviteWindow(InviteRequest message) {
this.inviteWindow.setVisible(true);
this.inviteWindow.getChildren().add(new InviteWindow(message, this).getRootPane());
}
public void acceptGameRequest(MouseEvent event) {
this.send(new InviteResponse(this.currentGameRequest.getId(), true));
this.closeInviteWindow();
}
public void declineGameRequest(MouseEvent event) {
this.send(new InviteResponse(this.currentGameRequest.getId(), false));
this.closeInviteWindow();
}
private void closeInviteWindow() {
this.inviteWindow.getChildren().clear();
this.inviteWindow.setVisible(false);
}
private void selectUserForGameInvite(MouseEvent event) {
String selected = this.usersOnline.getSelectionModel().getSelectedItem();
if (selected != null) {
this.userForGameInvite = selected;
} else {
this.usersOnline.getItems().clear();
this.sendServerQueryMessage();
}
}
private void startGameWithUser(MouseEvent event) {
if (this.userForGameInvite != null) {
if (this.selectedGameType != null) {
int userIdToInvite = this.usersOnlineList.get(this.userForGameInvite);
StartGameRequest startGameRequest = new StartGameRequest(userIdToInvite, this.selectedGameType);
this.sendInvite(startGameRequest);
this.chatOutput("Invite sent to " + this.userForGameInvite);
} else {
this.chatOutput("No Game Type selected");
}
} else {
this.chatOutput("No Opponent selected");
}
}
private void sendInvite(StartGameRequest startGameRequest) {
if (!gamesRunning.isEmpty()) {
this.chatOutput("You already have a running game. Unable to start a new one.");
return;
}
this.send(startGameRequest);
}
private void closeController(GameClientController controller) {
this.gamesRunning.remove(controller);
controller.closeGame();
controller.closeWindow();
}
private void closeDeckBuilderWindow() {
//this is a workaround to allow the player to "decline" an invite once the deck builder is open
if(this.gamesRunning.size() == 1) {
for (GameClientController controller : this.gamesRunning) {
this.closeController(controller);
this.openDeckBuilderWindow = null;
}
}
}
private void stopThreads() {
this.listenThread.interrupt();
}
private void breakConnection() {
try {
this.in.close();
this.out.close();
} catch (Exception e) {
logger.info("Failed to break connection");
}
}
public void closeLobby() {
this.stopThreads();
this.breakConnection();
for (GameClientController game : this.gamesRunning) {
game.closeWindow();
}
if (this.openDeckBuilderWindow != null) {
this.openDeckBuilderWindow.closeWindow();
}
}
private void createGameTypeButtons() {
for (String string : this.gameTypes) {
GameTypeButton button = new GameTypeButton(this.gameTypeBox.getPrefWidth() / this.gameTypes.size(), this.gameTypeBox.getPrefHeight(), string, this);
this.gameTypeBox.getChildren().add(button);
}
}
public void clearGameTypeButtons() {
for (Object button : this.gameTypeBox.getChildren()) {
((GenericButton)button).unHighlightButton();
}
}
public void setGameType(String string) {
this.selectedGameType = string;
}
@Override
public void initialize(URL location, ResourceBundle resources) {
this.sendMessageButton.setOnAction(e -> this.sendMessage());
this.messageBox.setOnAction(e -> this.sendMessage());
}
private void sendMessage() {
String message = this.messageBox.getText();
this.send(new ChatMessage(1, "unused", message));
this.messageBox.clear();
}
}