Coverage Summary for Class: LobbyServer (it.polimi.ingsw.Server)
Class |
Method, %
|
Branch, %
|
Line, %
|
LobbyServer |
0%
(0/6)
|
0%
(0/40)
|
0%
(0/138)
|
LobbyServer$1 |
0%
(0/1)
|
0%
(0/1)
|
LobbyServer$State |
0%
(0/2)
|
0%
(0/5)
|
Total |
0%
(0/9)
|
0%
(0/40)
|
0%
(0/144)
|
1 package it.polimi.ingsw.Server;
2
3 import it.polimi.ingsw.Controller.Actions.PlayerAction;
4 import it.polimi.ingsw.Exceptions.Input.InputValidationException;
5 import it.polimi.ingsw.Exceptions.Operation.OperationException;
6 import it.polimi.ingsw.Logger;
7 import it.polimi.ingsw.Network.SocketWrapper;
8 import it.polimi.ingsw.Server.Messages.Events.ClientEvent;
9 import it.polimi.ingsw.Server.Messages.Events.Internal.*;
10 import it.polimi.ingsw.Server.Messages.Events.Requests.*;
11 import it.polimi.ingsw.Server.Messages.ServerResponses.*;
12 import it.polimi.ingsw.Server.Messages.ServerResponses.SupportStructures.LobbyInfo;
13
14 import java.io.IOException;
15 import java.util.*;
16 import java.util.concurrent.ArrayBlockingQueue;
17 import java.util.concurrent.BlockingQueue;
18 import java.util.concurrent.ConcurrentHashMap;
19 import java.util.function.Predicate;
20
21 /**
22 * Handler of game events, responsible for communication of game related information with a single client.
23 */
24 public class LobbyServer implements Runnable {
25 protected static final Map<UUID, Lobby> lobbyMap = new ConcurrentHashMap<>();
26 private static final Set<String> connectedNicknames = new HashSet<>();
27 private final SocketWrapper sw;
28 private final BlockingQueue<ClientEvent> eventQueue;
29
30 /**
31 * Creates the server around a {@link SocketWrapper}
32 *
33 * @param sw the socket to use to communicate with the client
34 */
35 private LobbyServer(SocketWrapper sw) {
36 this.sw = sw;
37 this.eventQueue = new ArrayBlockingQueue<>(10);
38 }
39
40 /**
41 * Start a server on the provided socket wrapper
42 *
43 * @param socketWrapper the wrapped connection to the client of the server
44 */
45 public static void spawn(SocketWrapper socketWrapper) {
46 LobbyServer lobbyServer = new LobbyServer(socketWrapper);
47 new Thread(lobbyServer).start();
48 SocketListener.subscribe(socketWrapper, lobbyServer.getEventQueue());
49 }
50
51 /**
52 * Get the event queue this server listens on
53 *
54 * @return the {@link BlockingQueue<ClientEvent>} linked to this server
55 */
56 private BlockingQueue<ClientEvent> getEventQueue() {
57 return this.eventQueue;
58 }
59
60 /**
61 * When run, polls for events from the client and sends appropriate responses while handling interactions with the game
62 */
63 @Override
64 public void run() {
65 String nickname = "unknown";
66 int playerID = -1;
67 Lobby currentLobby = null;
68 State state = State.ACCEPT_PHASE;
69 while (true) {
70 try {
71 ClientEvent event = this.eventQueue.take();
72 Logger.info("Lobby server received a new Event: " + event.getClass());
73 if (event instanceof SocketClosedEvent) {
74 if (currentLobby != null) {
75 currentLobby.disconnectPlayer(nickname);
76 }
77 synchronized (connectedNicknames) {
78 connectedNicknames.remove(nickname);
79 }
80 Logger.info("Lobby server was closed for player: " +
81 nickname +
82 " on address " +
83 this.sw.getInetAddress());
84 return;
85 } else {
86 switch (state) {
87 case ACCEPT_PHASE -> {
88 if (event instanceof DeclarePlayerRequest castedEvent) {
89 nickname = castedEvent.getNickname();
90 synchronized (connectedNicknames) {
91 if (connectedNicknames.contains(nickname)) {
92 sw.sendMessage(LobbyServerAccept.fail());
93 } else {
94 connectedNicknames.add(nickname);
95 List<LobbyInfo> publicLobbies = lobbyMap.values().stream()
96 .filter(Lobby::isPublic)
97 .filter(Predicate.not(Lobby::isGameInProgress))
98 .map(LobbyInfo::new)
99 .toList();
100 sw.sendMessage(LobbyServerAccept.success(publicLobbies));
101 state = State.REDIRECT_PHASE;
102 }
103 }
104 } else {
105 sw.sendMessage(new InvalidRequest());
106 }
107 }
108 case REDIRECT_PHASE -> {
109 // redirect phase: wait for valid lobby action
110 // either:
111 // - create
112 // - join or rejoin
113 switch (event) {
114 case CreateLobbyRequest castedEvent -> {
115 if (castedEvent.getMaxPlayers() < 1 || castedEvent.getMaxPlayers() > 4) {
116 sw.sendMessage(LobbyConnected.fail());
117 break;
118 }
119 UUID lobbyID = generateUUID();
120 currentLobby = new Lobby(
121 lobbyID,
122 castedEvent.isPublic(),
123 castedEvent.getMaxPlayers(),
124 nickname
125 );
126 currentLobby.addPlayer(nickname, this.getEventQueue());
127 lobbyMap.put(lobbyID, currentLobby);
128 state = State.GAME_START_PHASE;
129 sw.sendMessage(LobbyConnected.success(lobbyID, currentLobby.getAdmin()));
130 }
131 case ConnectLobbyRequest castedEvent -> {
132 UUID lobbyID = castedEvent.getCode();
133 if (!lobbyMap.containsKey(lobbyID) || !lobbyMap.get(lobbyID).addPlayer(nickname, this.getEventQueue())) {
134 sw.sendMessage(LobbyConnected.fail());
135 break;
136 }
137 currentLobby = lobbyMap.get(lobbyID);
138 sw.sendMessage(LobbyConnected.success(lobbyID, currentLobby.getAdmin()));
139 state = State.GAME_START_PHASE;
140 }
141 case default -> sw.sendMessage(new InvalidRequest());
142 }
143 }
144 case GAME_START_PHASE -> {
145 // wait phase: wait for valid lobby action
146 // either:
147 // - start (only from admin)
148 // - start (as admin event reaction)
149 switch (event) {
150 case LobbyClosedEvent ignored -> {
151 currentLobby = null;
152 state = State.REDIRECT_PHASE;
153 sw.sendMessage(new LobbyClosed());
154 }
155 case ClientConnectEvent clientConnectedEvent ->
156 sw.sendMessage(new ClientConnected(clientConnectedEvent.getNickname(), clientConnectedEvent.getPlayers()));
157 case ClientDisconnectEvent clientDisconnectedEvent ->
158 sw.sendMessage(new ClientDisconnected(clientDisconnectedEvent.getNickname(), clientDisconnectedEvent.getPlayers()));
159 case StartGameRequest castedEvent -> {
160 if (!currentLobby.getAdmin().equals(nickname)) {
161 sw.sendMessage(GameInit.fail("Only the admin of the lobby can start the game."));
162 break;
163 }
164 if (!currentLobby.isLobbyFull()) {
165 sw.sendMessage(GameInit.fail("The lobby has not been filled"));
166 break;
167 }
168 try {
169 currentLobby.startGame(castedEvent.getGameMode());
170 } catch (InputValidationException e) {
171 sw.sendMessage(GameInit.fail(e.getMessage()));
172 break;
173 }
174 // code executes only when a gameLobby was created
175 sw.sendMessage(GameInit.success());
176 }
177 case GameStartEvent gameStartEvent -> {
178 state = State.GAME_IN_PROGRESS_PHASE;
179 playerID = gameStartEvent.nickToID().get(nickname);
180 sw.sendMessage(new GameStarted());
181 }
182 case default -> sw.sendMessage(new InvalidRequest());
183 }
184 }
185 case GAME_IN_PROGRESS_PHASE -> {
186 // wait phase: wait for valid lobby action
187 // either:
188 // - start (only from admin)
189 // - start (as admin event reaction)
190 switch (event) {
191 case LobbyClosedEvent ignored -> {
192 currentLobby = null;
193 state = State.REDIRECT_PHASE;
194 sw.sendMessage(new LobbyClosed());
195 }
196 case ModelUpdateEvent modelUpdateEvent ->
197 sw.sendMessage(new ModelUpdated(modelUpdateEvent.getModel()));
198 case GameOverEvent gameOverEvent -> {
199 sw.sendMessage(new GameOver(gameOverEvent.winners()));
200 currentLobby.close();
201 }
202 case PlayerActionRequest playerActionRequest -> {
203 try {
204 PlayerAction pa = playerActionRequest.getAction();
205 if (pa.getPlayerBoardID() == playerID) {
206 PlayerActionFeedback feedback;
207 try {
208 currentLobby.executeAction(pa);
209 feedback = PlayerActionFeedback.success();
210 } catch (InputValidationException e) {
211 feedback = PlayerActionFeedback.fail(e.getMessage());
212 }
213 sw.sendMessage(feedback);
214 } else {
215 sw.sendMessage(PlayerActionFeedback.fail("You shall not impersonate others."));
216 }
217 } catch (OperationException e) {
218 Logger.severe("Supposedly unreachable statement was reached:\n" + e.getMessage());
219 sw.sendMessage(PlayerActionFeedback.fail(e.getMessage()));
220 throw new RuntimeException(e);
221 }
222 }
223 case ClientDisconnectEvent clientDisconnectedEvent ->
224 sw.sendMessage(new ClientDisconnected(clientDisconnectedEvent.getNickname(), clientDisconnectedEvent.getPlayers()));
225 case default -> sw.sendMessage(new InvalidRequest());
226 }
227 }
228 }
229 }
230 } catch (Exception e) {
231 Logger.severe(e.getMessage());
232 try {
233 this.sw.close();
234 } catch (IOException ex) {
235 throw new RuntimeException(ex);
236 }
237 }
238 }
239 }
240
241 /**
242 * generates a unique UUID for a lobby
243 *
244 * @return a not yet in use UUID for the lobby
245 */
246 private static UUID generateUUID() {
247 UUID id = UUID.randomUUID();
248 while (lobbyMap.containsKey(id)) {
249 id = UUID.randomUUID();
250 }
251 return id;
252 }
253
254 /**
255 * Represents the various stages of the connection between client and server
256 */
257 private enum State {
258 /**
259 * client has not identified itself yet
260 */
261 ACCEPT_PHASE,
262 /**
263 * client now has a name, but is not part of a lobby
264 */
265 REDIRECT_PHASE,
266 /**
267 * client is part of a lobby, waiting for the game to start
268 */
269 GAME_START_PHASE,
270 /**
271 * client is now in a game, playing
272 */
273 GAME_IN_PROGRESS_PHASE,
274 }
275 }