diff --git a/backend/src/main/java/fr/zelytra/session/SessionManager.java b/backend/src/main/java/fr/zelytra/session/SessionManager.java index d880a3bd..5502be37 100644 --- a/backend/src/main/java/fr/zelytra/session/SessionManager.java +++ b/backend/src/main/java/fr/zelytra/session/SessionManager.java @@ -137,6 +137,15 @@ public void leaveSession(Player player) { Log.info("[" + fleet.getSessionId() + "] Has been disbanded"); } + //Close the socket if not yet closed + if (player.getSocket().isOpen()) { + try { + player.getSocket().close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } /** @@ -276,6 +285,18 @@ public void playerJoinSotServer(Player player, SotServer server) { broadcastDataToSession(fleet.getSessionId(), MessageType.UPDATE, fleet); } + @Lock(value = Lock.Type.READ, time = 200) + @Nullable + public Player getPlayerFromUsername(String username) { + Fleet fleet = this.getFleetByPlayerName(username); + for (Player playerInList : fleet.getPlayers()) { + if (playerInList.getUsername().equalsIgnoreCase(username)) { + return playerInList; + } + } + return null; + } + @Lock(value = Lock.Type.WRITE, time = 200) public void playerLeaveSotServer(Player player, SotServer server) { SotServer findedSotServer = getServerFromHashing(server); @@ -381,7 +402,7 @@ public void sendDataToPlayer(Session session, MessageType messageType, T dat }); } - public String formatMessage(MessageType messageType, T data) { + public String formatMessage(MessageType messageType, T data) { SocketMessage message = new SocketMessage<>(messageType, data); ObjectMapper objectMapper = new ObjectMapper(); diff --git a/backend/src/main/java/fr/zelytra/session/SessionSocket.java b/backend/src/main/java/fr/zelytra/session/SessionSocket.java index d121787a..0aa1d95b 100644 --- a/backend/src/main/java/fr/zelytra/session/SessionSocket.java +++ b/backend/src/main/java/fr/zelytra/session/SessionSocket.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import fr.zelytra.session.fleet.Fleet; import fr.zelytra.session.player.Player; +import fr.zelytra.session.player.PlayerAction; import fr.zelytra.session.server.SotServer; import fr.zelytra.session.socket.MessageType; import fr.zelytra.session.socket.SocketMessage; @@ -83,6 +84,18 @@ public void onMessage(String message, Session session, @PathParam("sessionId") S Player player = objectMapper.convertValue(socketMessage.data(), Player.class); handleUpdateMessage(player); } + case KICK_PLAYER -> { + PlayerAction player = objectMapper.convertValue(socketMessage.data(), PlayerAction.class); + handleKick(player); + } + case PROMOTE_PLAYER -> { + PlayerAction player = objectMapper.convertValue(socketMessage.data(), PlayerAction.class); + handlePromote(player, true); + } + case DEMOTE_PLAYER -> { + PlayerAction player = objectMapper.convertValue(socketMessage.data(), PlayerAction.class); + handlePromote(player, false); + } case START_COUNTDOWN -> handleStartCountdown(session); case CLEAR_STATUS -> handleClearStatus(session); case KEEP_ALIVE -> { @@ -100,6 +113,34 @@ public void onMessage(String message, Session session, @PathParam("sessionId") S } } + private void handleKick(PlayerAction player) { + SessionManager manager = sessionManager; + Fleet fleet = manager.getFleetByPlayerName(player.username()); + Player foundedPlayer = manager.getPlayerFromUsername(player.username()); + + if (foundedPlayer != null) { + Log.info("[" + fleet.getSessionId() + "] " + player.username() + " has been kicked from the session"); + manager.leaveSession(foundedPlayer); + } else { + Log.warn("[" + fleet.getSessionId() + "] " + player.username() + " cannot be kicked, not found"); + } + + } + + private void handlePromote(PlayerAction player, boolean master) { + SessionManager manager = sessionManager; + Fleet fleet = manager.getFleetByPlayerName(player.username()); + Player foundedPlayer = manager.getPlayerFromUsername(player.username()); + + if (foundedPlayer != null) { + Log.info("[" + fleet.getSessionId() + "] " + player.username() + " has been " + (master ? "promoted" : "demoted")); + foundedPlayer.setMaster(master); + sessionManager.broadcastDataToSession(fleet.getSessionId(), MessageType.UPDATE, fleet); + } else { + Log.warn("[" + fleet.getSessionId() + "] " + player.username() + " cannot be " + (master ? "promoted" : "demoted") + ", not found"); + } + } + private void handleClearStatus(Session session) { SessionManager manager = sessionManager; diff --git a/backend/src/main/java/fr/zelytra/session/player/PlayerAction.java b/backend/src/main/java/fr/zelytra/session/player/PlayerAction.java new file mode 100644 index 00000000..008483c9 --- /dev/null +++ b/backend/src/main/java/fr/zelytra/session/player/PlayerAction.java @@ -0,0 +1,4 @@ +package fr.zelytra.session.player; + +public record PlayerAction(String username,String sessionId) { +} diff --git a/backend/src/main/java/fr/zelytra/session/socket/MessageType.java b/backend/src/main/java/fr/zelytra/session/socket/MessageType.java index eb5ce3cd..daa5fc1e 100644 --- a/backend/src/main/java/fr/zelytra/session/socket/MessageType.java +++ b/backend/src/main/java/fr/zelytra/session/socket/MessageType.java @@ -12,4 +12,7 @@ public enum MessageType { SESSION_NOT_FOUND, KEEP_ALIVE, CONNECTION_REFUSED, + PROMOTE_PLAYER, + KICK_PLAYER, + DEMOTE_PLAYER, } diff --git a/webapp/package.json b/webapp/package.json index f1029250..48956511 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,7 +9,7 @@ "preview": "vite preview", "tauri": "tauri", "tauri-dev": "npm run tauri dev", - "tauri-build": "npm run tauri build -- --debug" + "tauri-build": "npm run tauri build" }, "dependencies": { "@js-joda/core": "^5.6.2", diff --git a/webapp/src/assets/locales/fr.json b/webapp/src/assets/locales/fr.json index 3d481ba5..73caed5a 100644 --- a/webapp/src/assets/locales/fr.json +++ b/webapp/src/assets/locales/fr.json @@ -107,6 +107,14 @@ "subtitle": "Options de préférence", "title": "Paramètres" }, + "contextMenu": { + "master": { + "demote": "Retirer master", + "kick": "Expulser", + "promote": "Assigner Master", + "title": "Utilisateur" + } + }, "credits": { "and": "et", "description": "Better Fleet a été développé dans le but de faciliter la création d'alliances sur Sea of Thieves", diff --git a/webapp/src/assets/style.scss b/webapp/src/assets/style.scss index c7d4852f..4ca1c6fa 100644 --- a/webapp/src/assets/style.scss +++ b/webapp/src/assets/style.scss @@ -8,7 +8,7 @@ font-family: Windlass, sans-serif; font-weight: 400; - scrollbar-color: var(--primary) rgba(50, 212, 153, 0.10); + scrollbar-color: var(--primary) rgba(50, 212, 153, 0.10); scrollbar-width: thin; color: var(--primary-text); user-select: none; @@ -25,6 +25,15 @@ body { --important: #D43232; --warning: #D49332; --information: #32D499; + + --green: #32D49933; + --green-hover: #32D49980; + + --red: #D4323233; + --red-hover: #D4323280; + + --blue: #286AA833; + --blue-hover: #286AA880; } html, body { diff --git a/webapp/src/components/fleet/session/FleetLobby.vue b/webapp/src/components/fleet/session/FleetLobby.vue index b4ae2370..a246e880 100644 --- a/webapp/src/components/fleet/session/FleetLobby.vue +++ b/webapp/src/components/fleet/session/FleetLobby.vue @@ -39,12 +39,14 @@ return a.isMaster === b.isMaster ? 0 : a.isMaster ? -1 : 1; })" :player="player" + @click.right.prevent="openContextMenu($event,player)" />
@@ -105,6 +107,12 @@ confirm-class="important" title-class="important" /> + @@ -118,11 +126,33 @@ import {UserStore} from "@/objects/stores/UserStore.ts"; import SessionCountdown from "@/components/fleet/session/SessionCountdown.vue"; import ServerContainer from "@/vue/templates/ServerContainer.vue"; import ConfirmationModal from "@/vue/form/ConfirmationModal.vue"; +import MasterContextMenu from "@/vue/context/MasterContextMenu.vue"; +import {ContextMenu, MenuData} from "@/vue/context/ContextMenu.ts"; +import {Player} from "@/objects/fleet/Player.ts"; +import {WebSocketMessageType} from "@/objects/fleet/WebSocet.ts"; const {t} = useI18n(); const displayIdCopy = ref(false); const launchConfirmation = ref(false); const leaveConfirmation = ref(false); +const displayContextMenu = ref(false); +const contextMenu = ref(); +const masterContextMenu = ref>(); +const contextMenuData: MenuData[] = [ + { + display: t('contextMenu.master.promote'), + key: "promote", + class: "green" + }, { + display: t('contextMenu.master.demote'), + key: "demote", + class: "blue" + }, { + display: t('contextMenu.master.kick'), + key: "kick", + class: "red" + } +] const props = defineProps({ session: { type: Object as PropType, @@ -194,6 +224,55 @@ function copyIdToClipboard(id: string) { displayIdCopy.value = true; setTimeout(() => displayIdCopy.value = false, 2000); } + +function openContextMenu(event: any, player: Player) { + + if (!UserStore.player.isMaster || player.username == UserStore.player.username) { + return; + } + + contextMenu.value.setPos(event); + masterContextMenu.value = { + title: t('contextMenu.master.title') + ": " + player.username, + data: contextMenuData, + metaData: player.username + } + displayContextMenu.value = true; +} + +function onContextAction(action: string) { + console.log(action) + if (!props.session) { + return; + } + switch (action) { + case "promote": { + props.session.playerAction( + { + sessionId: props.session.sessionId, + username: masterContextMenu.value!.metaData + }, WebSocketMessageType.PROMOTE_PLAYER) + break + } + case "demote": { + props.session.playerAction( + { + sessionId: props.session.sessionId, + username: masterContextMenu.value!.metaData + }, WebSocketMessageType.DEMOTE_PLAYER) + break + } + case "kick": { + props.session.playerAction( + { + sessionId: props.session.sessionId, + username: masterContextMenu.value!.metaData + }, WebSocketMessageType.KICK_PLAYER) + break + } + } + displayContextMenu.value = false +} diff --git a/webapp/src/vue/fleet/PlayerFleet.vue b/webapp/src/vue/fleet/PlayerFleet.vue index e292bf9c..7a54dd91 100644 --- a/webapp/src/vue/fleet/PlayerFleet.vue +++ b/webapp/src/vue/fleet/PlayerFleet.vue @@ -57,14 +57,14 @@ import {UserStore} from "@/objects/stores/UserStore.ts"; import {ContributorProvider, ContributorType} from "@/objects/fleet/Contributor.ts"; const {t} = useI18n() -const contributor = ref(ContributorProvider.getPlayerContrib()); -const displayContrib = ref(false) -defineProps({ +const displayContrib = ref(false); +const props = defineProps({ player: { type: Object as PropType, required: true } }) +const contributor = ref(ContributorProvider.getPlayerContrib(props.player.username));