Skip to content

Commit

Permalink
Master context menu (#240)
Browse files Browse the repository at this point in the history
  • Loading branch information
zelytra authored Apr 21, 2024
1 parent 8e329bb commit aa15161
Show file tree
Hide file tree
Showing 16 changed files with 322 additions and 14 deletions.
23 changes: 22 additions & 1 deletion backend/src/main/java/fr/zelytra/session/SessionManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

}

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -381,7 +402,7 @@ public <T> void sendDataToPlayer(Session session, MessageType messageType, T dat
});
}

public <T> String formatMessage(MessageType messageType, T data) {
public <T> String formatMessage(MessageType messageType, T data) {
SocketMessage<T> message = new SocketMessage<>(messageType, data);

ObjectMapper objectMapper = new ObjectMapper();
Expand Down
41 changes: 41 additions & 0 deletions backend/src/main/java/fr/zelytra/session/SessionSocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 -> {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package fr.zelytra.session.player;

public record PlayerAction(String username,String sessionId) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ public enum MessageType {
SESSION_NOT_FOUND,
KEEP_ALIVE,
CONNECTION_REFUSED,
PROMOTE_PLAYER,
KICK_PLAYER,
DEMOTE_PLAYER,
}
2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions webapp/src/assets/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 10 additions & 1 deletion webapp/src/assets/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
79 changes: 79 additions & 0 deletions webapp/src/components/fleet/session/FleetLobby.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@
return a.isMaster === b.isMaster ? 0 : a.isMaster ? -1 : 1;
})"
:player="player"
@click.right.prevent="openContextMenu($event,player)"
/>
</ServerContainer>
<PlayerFleet
v-for="player in getFilteredPlayerList()"
:player="player"
class="player-fleet-card"
@click.right.prevent="openContextMenu($event,player)"
/>
</div>
<div class="lobby-details">
Expand Down Expand Up @@ -105,6 +107,12 @@
confirm-class="important"
title-class="important"
/>
<MasterContextMenu
ref="contextMenu"
v-model:display="displayContextMenu"
:menu="masterContextMenu"
@action="onContextAction"
/>
</section>
</template>

Expand All @@ -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<boolean>(false);
const launchConfirmation = ref<boolean>(false);
const leaveConfirmation = ref<boolean>(false);
const displayContextMenu = ref<boolean>(false);
const contextMenu = ref();
const masterContextMenu = ref<ContextMenu<string>>();
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<Fleet>,
Expand Down Expand Up @@ -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
}
</script>

<style scoped lang="scss">
Expand Down
5 changes: 2 additions & 3 deletions webapp/src/objects/fleet/Contributor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {UserStore} from "@/objects/stores/UserStore.ts";
import contribFile from "@/assets/contributors/contributors.json"

export interface Contributor {
Expand All @@ -19,8 +18,8 @@ export class ContributorProvider {

private static readonly contributors:Contributor = contribFile as Contributor;

public static getPlayerContrib(): ContributorType | null {
const playerUsernameLower = UserStore.player.username.toLowerCase();
public static getPlayerContrib(username:string): ContributorType | null {
const playerUsernameLower = username.toLowerCase();
const roles: Record<ContributorType, keyof Contributor> = {
[ContributorType.DEVELOPER]: 'developers',
[ContributorType.TRANSLATOR]: 'translators',
Expand Down
13 changes: 11 additions & 2 deletions webapp/src/objects/fleet/Fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {WebSocketMessage, WebSocketMessageType} from "@/objects/fleet/WebSocet.t
import {AlertType} from "@/vue/alert/Alert.ts";
import {alertProvider} from "@/main.ts";
import {i18n} from "@/objects/i18n";
import {Player} from "@/objects/fleet/Player.ts";
import {ActionPlayer, Player} from "@/objects/fleet/Player.ts";
import {SotServer} from "@/objects/fleet/SotServer.ts";
import {LocalTime} from "@js-joda/core";
import {HTTPAxios} from "@/objects/utils/HTTPAxios.ts";
Expand Down Expand Up @@ -55,7 +55,7 @@ export class Fleet {
await new HTTPAxios("socket/register", null).get(ResponseType.Text).then((response) => {
this.socket = new WebSocket(
UserStore.player.serverHostName + "/" + response.data + "/" + sessionId);
}).catch(()=>{
}).catch(() => {
alertProvider.sendAlert({
content: t('alert.websocketAuthFailed.content'),
title: t('alert.websocketAuthFailed.title'),
Expand Down Expand Up @@ -171,6 +171,15 @@ export class Fleet {
this.socket.send(JSON.stringify(message));
}

playerAction(playerToExecute: ActionPlayer, actionType: WebSocketMessageType): void {
if (!this.socket) return;
const message: WebSocketMessage = {
data: playerToExecute,
messageType: actionType,
};
this.socket.send(JSON.stringify(message));
}

runCountDown() {
if (!this.socket) return;
const message: WebSocketMessage = {
Expand Down
7 changes: 6 additions & 1 deletion webapp/src/objects/fleet/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,10 @@ export interface Preferences {
lang?: string;
soundEnable: boolean;
soundLevel: number;
macroEnable:boolean;
macroEnable: boolean;
}

export interface ActionPlayer {
username: string
sessionId: string
}
3 changes: 3 additions & 0 deletions webapp/src/objects/fleet/WebSocet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ export enum WebSocketMessageType {
SESSION_NOT_FOUND = "SESSION_NOT_FOUND",
KEEP_ALIVE = "KEEP_ALIVE",
CONNECTION_REFUSED= "CONNECTION_REFUSED",
PROMOTE_PLAYER= "PROMOTE_PLAYER",
KICK_PLAYER= "KICK_PLAYER",
DEMOTE_PLAYER= "DEMOTE_PLAYER",
}
5 changes: 3 additions & 2 deletions webapp/src/objects/stores/UserStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ export const UserStore = reactive({
...readPlayer,
lang: readPlayer.lang || browserLang,
device: readPlayer.device || PlayerDevice.MICROSOFT,
username: readPlayer.username || keycloakStore.user.username,
username: keycloakStore.user.username,
soundEnable: readPlayer.soundEnable !== undefined ? readPlayer.soundEnable : true,
macroEnable: readPlayer.macroEnable !== undefined ? readPlayer.macroEnable : true,
soundLevel: readPlayer.soundLevel || 30,
serverHostName: readPlayer.serverHostName || import.meta.env.VITE_SOCKET_HOST,
clientVersion: readPlayer.clientVersion || import.meta.env.VITE_VERSION,
fleet: new Fleet()
fleet: new Fleet(),
server: undefined
};

//@ts-ignore I18N typescript implementation
Expand Down
11 changes: 11 additions & 0 deletions webapp/src/vue/context/ContextMenu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ContextMenu<T> {
title: string
data: MenuData[],
metaData: T
}

export interface MenuData {
display: string
key: string
class: string
}
Loading

0 comments on commit aa15161

Please sign in to comment.