diff --git a/client/src/components/useHooks.tsx b/client/src/components/useHooks.tsx index e2c4ca21d..e351caa14 100644 --- a/client/src/components/useHooks.tsx +++ b/client/src/components/useHooks.tsx @@ -4,7 +4,7 @@ import { StateEventType } from "../game/gameManager.d"; import GameState, { LobbyState, PlayerGameState } from "../game/gameState.d"; import DUMMY_NAMES from "../resources/dummyNames.json"; -function usePacketListener(listener: (type?: StateEventType) => void) { +export function usePacketListener(listener: (type?: StateEventType) => void) { // Catch all the packets we miss between setState and useEffect const packets: StateEventType[] = []; const packetQueueListener = (type?: StateEventType) => { diff --git a/client/src/game/gameManager.d.tsx b/client/src/game/gameManager.d.tsx index 4085ca857..85cf7a5ba 100644 --- a/client/src/game/gameManager.d.tsx +++ b/client/src/game/gameManager.d.tsx @@ -104,6 +104,10 @@ export type GameManager = { ): void sendVoteFastForwardPhase(fastForward: boolean): void; + sendHostDataRequest(): void; + sendHostEndGamePacket(): void; + sendHostSkipPhase(): void; + sendHostSetPlayerNamePacket(player_id: number, name: string): void; messageListener(serverMessage: ToClientPacket): void; diff --git a/client/src/game/gameManager.tsx b/client/src/game/gameManager.tsx index 382c3cab7..fd80df8cf 100644 --- a/client/src/game/gameManager.tsx +++ b/client/src/game/gameManager.tsx @@ -13,6 +13,7 @@ import PlayMenu from "../menu/main/PlayMenu"; import { createGameState, createLobbyState } from "./gameState"; import { deleteReconnectData } from "./localStorage"; import AudioController from "../menu/AudioController"; +import ListMap from "../ListMap"; export function createGameManager(): GameManager { @@ -77,7 +78,11 @@ export function createGameManager(): GameManager { GAME_MANAGER.state.roleList = lobbyState.roleList; GAME_MANAGER.state.phaseTimes = lobbyState.phaseTimes; GAME_MANAGER.state.enabledRoles = lobbyState.enabledRoles; - GAME_MANAGER.state.host = lobbyState.players.get(lobbyState.myId!)?.ready === "host"; + if (lobbyState.players.get(lobbyState.myId!)?.ready === "host") { + GAME_MANAGER.state.host = { + clients: new ListMap() + }; + } GAME_MANAGER.state.myId = lobbyState.myId } }, @@ -451,6 +456,29 @@ export function createGameManager(): GameManager { }); }, + sendHostDataRequest() { + this.server.sendPacket({ + type: "hostDataRequest" + }) + }, + sendHostEndGamePacket() { + this.server.sendPacket({ + type: "endGame" + }) + }, + sendHostSkipPhase() { + this.server.sendPacket({ + type: "skipPhase" + }) + }, + sendHostSetPlayerNamePacket(playerId, name) { + this.server.sendPacket({ + type: "setPlayerName", + id: playerId, + name + }) + }, + messageListener(serverMessage) { messageListener(serverMessage); }, diff --git a/client/src/game/gameState.d.tsx b/client/src/game/gameState.d.tsx index 131d41d82..58d2e4f9b 100644 --- a/client/src/game/gameState.d.tsx +++ b/client/src/game/gameState.d.tsx @@ -40,9 +40,22 @@ export type LobbyState = { } export type LobbyClient = { ready: "host" | "ready" | "notReady", - connection: "connected" | "disconnected" | "couldReconnect", + connection: ClientConnection, clientType: LobbyClientType } +export type ClientConnection = "connected" | "disconnected" | "couldReconnect"; +export type GameClient = { + clientType: GameClientType, + connection: ClientConnection, + host: boolean, +} +export type GameClientType = { + type: "spectator", + index: number +} | { + type: "player", + index: number, +} export type LobbyClientType = { type: "spectator" } | PlayerClientType; @@ -78,7 +91,9 @@ type GameState = { ticking: boolean, clientState: PlayerGameState | {type: "spectator"}, - host: boolean, + host: null | { + clients: ListMap + }, missedChatMessages: boolean } diff --git a/client/src/game/gameState.tsx b/client/src/game/gameState.tsx index 8513afa41..2ab9caca2 100644 --- a/client/src/game/gameState.tsx +++ b/client/src/game/gameState.tsx @@ -62,7 +62,7 @@ export function createGameState(): GameState { ticking: true, clientState: createPlayerGameState(), - host: false, + host: null, missedChatMessages: false } diff --git a/client/src/game/messageListener.tsx b/client/src/game/messageListener.tsx index cd64bcf4d..f7d7545bd 100644 --- a/client/src/game/messageListener.tsx +++ b/client/src/game/messageListener.tsx @@ -4,7 +4,7 @@ import { ANCHOR_CONTROLLER, chatMessageToAudio } from "./../menu/Anchor"; import GAME_MANAGER from "./../index"; import GameScreen from "./../menu/game/GameScreen"; import { ToClientPacket } from "./packet"; -import { PlayerIndex, Tag } from "./gameState.d"; +import { GameClient, PlayerIndex, Tag } from "./gameState.d"; import { Role } from "./roleState.d"; import translate from "./lang"; import { computePlayerKeywordData, computePlayerKeywordDataForLobby } from "../components/StyledText"; @@ -23,7 +23,6 @@ import { defaultAlibi } from "../menu/game/gameScreenContent/WillMenu"; import ListMap from "../ListMap"; import { sortControllerIdCompare } from "./abilityInput"; - function sendDefaultName() { const defaultName = loadSettingsParsed().defaultName; if(defaultName !== null && defaultName !== undefined && defaultName !== ""){ @@ -32,10 +31,8 @@ function sendDefaultName() { } export default function messageListener(packet: ToClientPacket){ - console.log(JSON.stringify(packet, null, 2)); - switch(packet.type) { case "pong": if (GAME_MANAGER.state.stateType !== "disconnected") { @@ -149,7 +146,19 @@ export default function messageListener(packet: ToClientPacket){ } GAME_MANAGER.state.players = new ListMap(GAME_MANAGER.state.players.entries()); }else if(GAME_MANAGER.state.stateType === "game"){ - GAME_MANAGER.state.host = packet.hosts.includes(GAME_MANAGER.state.myId ?? -1) + if (packet.hosts.includes(GAME_MANAGER.state.myId ?? -1)) { + if (GAME_MANAGER.state.host === null) { + GAME_MANAGER.state.host = { + clients: new ListMap() + } + } + + for (const [id, client] of GAME_MANAGER.state.host.clients.entries()) { + client.host = packet.hosts.includes(id); + } + } else { + GAME_MANAGER.state.host = null + } } break; case "playersReady": @@ -193,11 +202,10 @@ export default function messageListener(packet: ToClientPacket){ break; case "lobbyClients": if(GAME_MANAGER.state.stateType === "lobby"){ - const oldMySpectator = GAME_MANAGER.state.players.get(GAME_MANAGER.state.myId!)?.clientType.type === "spectator"; GAME_MANAGER.state.players = new ListMap(); - for(let [clientId, lobbyClient] of packet.clients){ + for(const [clientId, lobbyClient] of packet.clients){ GAME_MANAGER.state.players.insert(clientId, lobbyClient); } const newMySpectator = GAME_MANAGER.state.players.get(GAME_MANAGER.state.myId!)?.clientType.type === "spectator"; @@ -215,6 +223,13 @@ export default function messageListener(packet: ToClientPacket){ ); } break; + case "hostData": + if (GAME_MANAGER.state.stateType === "game") { + GAME_MANAGER.state.host = { + clients: new ListMap(packet.clients) + } + } + break; case "lobbyName": if(GAME_MANAGER.state.stateType === "lobby" || GAME_MANAGER.state.stateType === "game"){ GAME_MANAGER.state.lobbyName = packet.name; @@ -322,14 +337,6 @@ export default function messageListener(packet: ToClientPacket){ if(GAME_MANAGER.state.stateType === "game") GAME_MANAGER.state.timeLeftMs = packet.secondsLeft * 1000; break; - case "playerOnTrial": - if(GAME_MANAGER.state.stateType === "game" && ( - GAME_MANAGER.state.phaseState.type === "testimony" || - GAME_MANAGER.state.phaseState.type === "judgement" || - GAME_MANAGER.state.phaseState.type === "finalWords" - )) - GAME_MANAGER.state.phaseState.playerOnTrial = packet.playerIndex; - break; case "playerAlive": if(GAME_MANAGER.state.stateType === "game"){ for(let i = 0; i < GAME_MANAGER.state.players.length && i < packet.alive.length; i++){ diff --git a/client/src/game/packet.tsx b/client/src/game/packet.tsx index 238608c2c..315f37668 100644 --- a/client/src/game/packet.tsx +++ b/client/src/game/packet.tsx @@ -1,4 +1,4 @@ -import { PhaseType, PlayerIndex, Verdict, PhaseTimes, Tag, LobbyClientID, ChatGroup, PhaseState, LobbyClient, ModifierType, InsiderGroup } from "./gameState.d" +import { PhaseType, PlayerIndex, Verdict, PhaseTimes, Tag, LobbyClientID, ChatGroup, PhaseState, LobbyClient, ModifierType, InsiderGroup, GameClient } from "./gameState.d" import { Grave } from "./graveState" import { ChatMessage } from "../components/ChatMessage" import { RoleList, RoleOutline } from "./roleListState.d" @@ -16,6 +16,9 @@ export type LobbyPreviewData = { export type ToClientPacket = { type: "pong", +} | { + type: "hostData", + clients: ListMapData } | { type: "rateLimitExceeded", } | { @@ -102,9 +105,6 @@ export type ToClientPacket = { type: "phaseTimeLeft", secondsLeft: number } |{ - type: "playerOnTrial", - playerIndex: PlayerIndex -} | { type: "playerAlive", alive: [boolean] } | { @@ -178,6 +178,8 @@ export type ToServerPacket = { type: "ping", } | { type: "lobbyListRequest", +} | { + type: "hostDataRequest", } | { type: "reJoin", roomCode: number, @@ -289,4 +291,12 @@ export type ToServerPacket = { } | { type: "voteFastForwardPhase", fastForward: boolean +} | { + type: "endGame", +} | { + type: "skipPhase", +} | { + type: "setPlayerName", + id: number, + name: string } \ No newline at end of file diff --git a/client/src/menu/GlobalMenu.tsx b/client/src/menu/GlobalMenu.tsx index 8adff94b4..922fb395f 100644 --- a/client/src/menu/GlobalMenu.tsx +++ b/client/src/menu/GlobalMenu.tsx @@ -12,6 +12,7 @@ import Icon from '../components/Icon'; import SettingsMenu from './Settings'; import { useLobbyOrGameState } from '../components/useHooks'; import { Button } from '../components/Button'; +import HostMenu from './HostMenu'; export default function GlobalMenu(): ReactElement { const lobbyName = useLobbyOrGameState( @@ -21,7 +22,7 @@ export default function GlobalMenu(): ReactElement { const host = useLobbyOrGameState( state => { if (state.stateType === "game") { - return state.host + return state.host !== null } else { return state.players.get(state.myId!)?.ready === "host" } @@ -71,9 +72,14 @@ export default function GlobalMenu(): ReactElement {

{lobbyName}

- {(stateType === "game" && host) && } + {(stateType === "game" && host) && <> + + + }
}
diff --git a/client/src/menu/HostMenu.tsx b/client/src/menu/HostMenu.tsx new file mode 100644 index 000000000..751cbff09 --- /dev/null +++ b/client/src/menu/HostMenu.tsx @@ -0,0 +1,57 @@ +import React, { ReactElement, useContext, useEffect, useState } from "react"; +import translate from "../game/lang"; +import GAME_MANAGER from ".."; +import { Button } from "../components/Button"; +import { usePacketListener } from "../components/useHooks"; +import { AnchorControllerContext } from "./Anchor"; +import "./lobby/lobbyMenu.css" +import LobbyPlayerList from "./lobby/LobbyPlayerList"; + +export default function HostMenu(): ReactElement { + const anchorController = useContext(AnchorControllerContext)!; + + useEffect(() => { + GAME_MANAGER.sendHostDataRequest(); + }, []) + + const [lastRefreshed, setLastRefreshed] = useState(new Date()); + + usePacketListener(type => { + // Check on every packet since like 1 million packets can affect this + if (!(GAME_MANAGER.state.stateType === "game" && GAME_MANAGER.state.host !== null)) { + anchorController.clearCoverCard(); + } + + if (type === "hostData") { + setLastRefreshed(new Date(Date.now())) + } + }); + + return
+
+

{translate("menu.hostSettings.title")}

+ {translate("menu.hostSettings.lastRefresh", lastRefreshed.toLocaleTimeString())} +
+ + + +
+ +
+

{translate("menu.hostSettings.lobby")}

+
+ + + +
+
+
+
+} \ No newline at end of file diff --git a/client/src/menu/game/HeaderMenu.tsx b/client/src/menu/game/HeaderMenu.tsx index 5304fbeeb..6f55b4c39 100644 --- a/client/src/menu/game/HeaderMenu.tsx +++ b/client/src/menu/game/HeaderMenu.tsx @@ -18,7 +18,7 @@ export default function HeaderMenu(props: Readonly<{ const phaseState = useGameState( gameState => gameState.phaseState, - ["phase", "playerOnTrial"] + ["phase"] )! const backgroundStyle = @@ -27,7 +27,7 @@ export default function HeaderMenu(props: Readonly<{ "background-day"; const host = useGameState( - state => state.host, + state => state.host !== null, ["playersHost"] )!; @@ -78,7 +78,7 @@ function Information(): ReactElement { )! const phaseState = useGameState( gameState => gameState.phaseState, - ["phase", "playerOnTrial"] + ["phase"] )! const players = useGameState( gameState => gameState.players, diff --git a/client/src/menu/game/gameScreenContent/PlayerListMenu.tsx b/client/src/menu/game/gameScreenContent/PlayerListMenu.tsx index e6611cd11..9e0c9ff7a 100644 --- a/client/src/menu/game/gameScreenContent/PlayerListMenu.tsx +++ b/client/src/menu/game/gameScreenContent/PlayerListMenu.tsx @@ -82,7 +82,7 @@ function PlayerCard(props: Readonly<{ )!; const phaseState = useGameState( gameState => gameState.phaseState, - ["phase", "playerOnTrial"] + ["phase"] )! const numVoted = useGameState( gameState => gameState.players[props.playerIndex].numVoted, diff --git a/client/src/menu/lobby/LobbyMenu.tsx b/client/src/menu/lobby/LobbyMenu.tsx index 8de1205ec..83d6497de 100644 --- a/client/src/menu/lobby/LobbyMenu.tsx +++ b/client/src/menu/lobby/LobbyMenu.tsx @@ -19,6 +19,7 @@ import LobbyChatMenu from "./LobbyChatMenu"; import { useLobbyState } from "../../components/useHooks"; import { Button } from "../../components/Button"; import { EnabledModifiersSelector } from "../../components/gameModeSettings/EnabledModifiersSelector"; +import LobbyNamePane from "./LobbyNamePane"; export default function LobbyMenu(): ReactElement { const isSpectator = useLobbyState( @@ -56,6 +57,7 @@ export default function LobbyMenu(): ReactElement { {advancedView ?
+
@@ -65,6 +67,7 @@ export default function LobbyMenu(): ReactElement {
:
+
diff --git a/client/src/menu/lobby/LobbyPlayerList.tsx b/client/src/menu/lobby/LobbyPlayerList.tsx index 7dcf4ce6c..1037850e2 100644 --- a/client/src/menu/lobby/LobbyPlayerList.tsx +++ b/client/src/menu/lobby/LobbyPlayerList.tsx @@ -1,52 +1,161 @@ -import React, { ReactElement } from "react"; +import React, { ReactElement, useRef, useState } from "react"; import translate from "../../game/lang"; import GAME_MANAGER from "../../index"; import "./lobbyMenu.css"; -import { PlayerClientType } from "../../game/gameState.d"; -import LobbyNamePane from "./LobbyNamePane"; +import { ClientConnection } from "../../game/gameState.d"; import Icon from "../../components/Icon"; -import { useLobbyState } from "../../components/useHooks"; +import { useLobbyOrGameState } from "../../components/useHooks"; +import { Button, RawButton } from "../../components/Button"; +import Popover from "../../components/Popover"; +import { dropdownPlacementFunction } from "../../components/Select"; +import StyledText from "../../components/StyledText"; + +type PlayerDisplayData = { + id: number, + clientType: "player" | "spectator", + connection: ClientConnection, + ready: boolean | null, + host: boolean, + name: string | null, + displayName: string, +} export default function LobbyPlayerList(): ReactElement { - const players = useLobbyState( - lobbyState => lobbyState.players, - ["playersHost", "playersLostConnection", "lobbyClients", "playersReady"] - )!; - const host = useLobbyState( - lobbyState => lobbyState.players.get(lobbyState.myId!)?.ready === "host", - ["playersHost", "lobbyClients", "yourId", "playersReady"] + const players: PlayerDisplayData[] = useLobbyOrGameState( + state => { + if (state.stateType === "lobby") { + return state.players.entries().map(([id, player]) => { + const name = player.clientType.type === "player" ? player.clientType.name : null; + return { + id, + clientType: player.clientType.type, + ready: player.ready === "ready", + connection: player.connection, + host: player.ready === "host", + name, + displayName: name ?? "Spectator" + } + }) + } else if (state.host !== null) { + return state.host.clients.entries().map(([id, player]) => { + return { + id, + clientType: player.clientType.type, + connection: player.connection, + ready: null, + host: player.host, + name: player.clientType.type === "player" + ? state.players[player.clientType.index].name + : player.clientType.index.toString(), + displayName: player.clientType.type === "player" + ? state.players[player.clientType.index].toString() + : player.clientType.index.toString(), + } + }) + } + }, + ["playersHost", "playersLostConnection", "lobbyClients", "playersReady", "hostData", "gamePlayers"] + ) ?? []; + + const host = useLobbyOrGameState( + state => { + if (state.stateType === "lobby") + return state.players.get(state.myId!)?.ready === "host" + else + return state.host !== null + }, + ["playersHost", "lobbyClients", "yourId", "playersReady", "hostData"] )!; - return <> - -
-

{translate("menu.lobby.players")}

-
+ return
+

{translate("menu.lobby.players")}

+
+
    + {players + .filter(player => player.clientType === "player") + .map(player => ) + } +
+
+ {host && <> +

{translate("menu.hostSettings.spectators")}

+
    - {[...players.entries()] - .filter(([_, player]) => player.clientType.type !== "spectator") - .map(([id, player]) => -
  1. -
    - {player.connection === "couldReconnect" && signal_cellular_connected_no_internet_4_bar} - {player.ready === "host" && shield} - {player.ready === "ready" && check} - {(player.clientType as PlayerClientType).name} -
    - {host && } -
  2. - ) + {players + .filter(player => player.clientType === "spectator") + .map(player => ) }
-
- {translate("menu.lobby.spectatorsReady", - [...players.values()].filter(p => p.clientType.type === "spectator" && p.ready !== "notReady").length, - [...players.values()].filter(p => p.clientType.type === "spectator").length - )} -
-
- + } + {!host &&
+ {translate("menu.lobby.spectatorsReady", + [...players.values()].filter(p => p.clientType === "spectator" && p.ready !== null).length, + [...players.values()].filter(p => p.clientType === "spectator").length + )} +
} +
+} + +function LobbyPlayerListPlayer(props: Readonly<{ player: PlayerDisplayData }>): ReactElement { + const host = useLobbyOrGameState( + state => { + if (state.stateType === "lobby") + return state.players.get(state.myId!)?.ready === "host" + else + return state.host !== null + }, + ["playersHost", "lobbyClients", "yourId", "playersReady", "hostData"] + )!; + + const [renameOpen, setRenameOpen] = useState(false); + const renameButtonRef = useRef(null); + + return
  • +
    + {props.player.connection === "couldReconnect" && signal_cellular_connected_no_internet_4_bar} + {props.player.connection === "disconnected" && sentiment_very_dissatisfied} + {props.player.host && shield} + {props.player.ready && check} + {props.player.displayName} +
    +
    + {host && props.player.connection !== "disconnected" && } + {host && props.player.clientType === "player" && <> + setRenameOpen(open => !open)} + >edit + + } +
    +
  • +} + +function LobbyPlayerListPlayerRename(props: Readonly<{ player: PlayerDisplayData }>): ReactElement { + const [playerName, setPlayerName] = useState(props.player.name ?? ""); + + return
    + setPlayerName((e.target as HTMLInputElement).value)} + onKeyUp={e => { + if (e.key === "Enter") { + const newName = (e.target as HTMLInputElement).value; + setPlayerName(newName); + GAME_MANAGER.sendHostSetPlayerNamePacket(props.player.id, newName); + } + }} + /> + +
    } \ No newline at end of file diff --git a/client/src/menu/lobby/lobbyMenu.css b/client/src/menu/lobby/lobbyMenu.css index ff33c2ce6..d4cb05f91 100644 --- a/client/src/menu/lobby/lobbyMenu.css +++ b/client/src/menu/lobby/lobbyMenu.css @@ -106,19 +106,19 @@ flex-grow: 1; } -.lm > div .player-list > .list { +.lobby-player-list { border: .13rem solid var(--background-border-color); border-top-color: var(--background-border-shadow-color); border-left-color: var(--background-border-shadow-color); border-radius: .5rem; } -.lm > div .player-list > .spectators-ready { +.spectators-ready { text-align: left; margin-top: .5rem; } -.lm > div .player-list ol li { +.lobby-player-list ol li { display: flex; flex-direction: row; justify-content: space-between; @@ -128,12 +128,19 @@ min-height: 2em; } -.lm > div .player-list ol li:nth-of-type(even) { +.lobby-player-list ol li:nth-of-type(even) { background-color: var(--primary-color); } -.lm > div .player-list ol li > div { +.lobby-player-list ol li > div { display: flex; flex-direction: row; gap: .25rem; } + +.lobby-player-list-player-rename { + border: .13rem solid var(--background-border-color); + background-color: var(--secondary-color); + border-radius: .25rem; + min-width: min-content; +} diff --git a/client/src/resources/lang/en_us.json b/client/src/resources/lang/en_us.json index b8be33150..9920237d3 100644 --- a/client/src/resources/lang/en_us.json +++ b/client/src/resources/lang/en_us.json @@ -42,6 +42,15 @@ "menu.globalMenu.settings": "Settings", "menu.globalMenu.gameSettingsEditor": "Game Mode Editor", + "menu.hostSettings.title": "Host Settings", + "menu.hostSettings.lastRefresh": "Last refresh: \\0", + "menu.hostSettings.players": "Manage Players", + "menu.hostSettings.spectators": "Spectators", + "menu.hostSettings.lobby": "Manage Lobby", + "menu.hostSettings.renamePlayer": "Rename", + "menu.hostSettings.endGame": "End Game", + "menu.hostSettings.skipPhase": "Skip Phase", + "menu.settings.title": "Settings", "menu.settings.general": "General", "menu.settings.gameplay": "Gameplay", diff --git a/server/src/game/game_conclusion.rs b/server/src/game/game_conclusion.rs index 56b7db287..df218bbca 100644 --- a/server/src/game/game_conclusion.rs +++ b/server/src/game/game_conclusion.rs @@ -69,6 +69,10 @@ impl GameConclusion { ) ) } + + pub fn get_premature_conclusion(game: &Game) -> GameConclusion { + GameConclusion::game_is_over(game).unwrap_or(GameConclusion::Draw) + } ///Town, Mafia, Cult, NK diff --git a/server/src/game/mod.rs b/server/src/game/mod.rs index c014158d6..28a1ff14f 100644 --- a/server/src/game/mod.rs +++ b/server/src/game/mod.rs @@ -436,8 +436,6 @@ impl Game { if start_trial_instantly { if let Some(player_on_trial) = voted_player { - self.send_packet_to_all(ToClientPacket::PlayerOnTrial { player_index: player_on_trial.index() } ); - PhaseStateMachine::next_phase(self, Some(PhaseState::Testimony { trials_left: trials_left.saturating_sub(1), player_on_trial, @@ -556,7 +554,9 @@ impl Game { spectator_pointer.index } pub fn remove_spectator(&mut self, i: SpectatorIndex){ - self.spectators.remove(i as usize); + if (i as usize) < self.spectators.len() { + self.spectators.remove(i as usize); + } } pub fn send_packet_to_all(&self, packet: ToClientPacket){ diff --git a/server/src/game/phase.rs b/server/src/game/phase.rs index ba97066af..d3f4d48ea 100644 --- a/server/src/game/phase.rs +++ b/server/src/game/phase.rs @@ -180,7 +180,6 @@ impl PhaseState { .collect() } ); - game.send_packet_to_all(ToClientPacket::PlayerOnTrial { player_index: player_on_trial.index() }); }, PhaseState::Briefing | PhaseState::Night @@ -214,9 +213,6 @@ impl PhaseState { if Modifiers::modifier_is_enabled(game, ModifierType::ScheduledNominations){ if let Some(player_on_trial) = game.count_nomination_and_start_trial(false){ - - game.send_packet_to_all(ToClientPacket::PlayerOnTrial { player_index: player_on_trial.index() } ); - Self::Testimony{ trials_left: trials_left.saturating_sub(1), player_on_trial, diff --git a/server/src/game/player/player_accessors.rs b/server/src/game/player/player_accessors.rs index 57ecf0260..967de5a64 100644 --- a/server/src/game/player/player_accessors.rs +++ b/server/src/game/player/player_accessors.rs @@ -15,6 +15,13 @@ impl PlayerReference{ pub fn name<'a>(&self, game: &'a Game) -> &'a String { &self.deref(game).name } + pub fn set_name(&self, game: &mut Game, new_name: String) { + self.deref_mut(game).name = new_name; + + game.send_packet_to_all(ToClientPacket::GamePlayers { + players: PlayerReference::all_players(game).map(|p| p.name(game)).cloned().collect() + }); + } pub fn role(&self, game: &Game) -> Role { self.deref(game).role_state.role() diff --git a/server/src/game/player/player_send_packet.rs b/server/src/game/player/player_send_packet.rs index 240922f1f..6a5503213 100644 --- a/server/src/game/player/player_send_packet.rs +++ b/server/src/game/player/player_send_packet.rs @@ -76,13 +76,6 @@ impl PlayerReference{ self.send_packet(game, ToClientPacket::GameOver { reason: GameOverReason::Draw }) } - if let PhaseState::Testimony { player_on_trial, .. } - | PhaseState::Judgement { player_on_trial, .. } - | PhaseState::FinalWords { player_on_trial } = game.current_phase() { - self.send_packet(game, ToClientPacket::PlayerOnTrial{ - player_index: player_on_trial.index() - }); - } let votes_packet = ToClientPacket::new_player_votes(game); self.send_packet(game, votes_packet); for grave in game.graves.iter(){ diff --git a/server/src/game/spectator/spectator_pointer.rs b/server/src/game/spectator/spectator_pointer.rs index 9c49a642e..ba9b44656 100644 --- a/server/src/game/spectator/spectator_pointer.rs +++ b/server/src/game/spectator/spectator_pointer.rs @@ -86,14 +86,7 @@ impl SpectatorPointer { if !game.ticking { self.send_packet(game, ToClientPacket::GameOver { reason: GameOverReason::Draw }) } - - if let PhaseState::Testimony { player_on_trial, .. } - | PhaseState::Judgement { player_on_trial, .. } - | PhaseState::FinalWords { player_on_trial } = game.current_phase() { - self.send_packet(game, ToClientPacket::PlayerOnTrial{ - player_index: player_on_trial.index() - }); - } + let votes_packet = ToClientPacket::new_player_votes(game); self.send_packet(game, votes_packet); for grave in game.graves.iter(){ diff --git a/server/src/lobby/game_client.rs b/server/src/lobby/game_client.rs index b37d3fabe..a57dc7a47 100644 --- a/server/src/lobby/game_client.rs +++ b/server/src/lobby/game_client.rs @@ -1,15 +1,21 @@ use std::{collections::VecDeque, time::Instant}; +use serde::Serialize; + use crate::game::{player::PlayerIndex, spectator::spectator_pointer::SpectatorIndex}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] pub struct GameClient{ + #[serde(rename = "clientType")] pub client_location: GameClientLocation, pub host: bool, + #[serde(skip)] pub last_message_times: VecDeque, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", content = "index", rename_all="camelCase")] pub enum GameClientLocation { Player(PlayerIndex), Spectator(SpectatorIndex) diff --git a/server/src/lobby/mod.rs b/server/src/lobby/mod.rs index 3d9a38bea..847770dbc 100644 --- a/server/src/lobby/mod.rs +++ b/server/src/lobby/mod.rs @@ -92,11 +92,21 @@ impl Lobby { } } + pub fn get_player_names(clients: &VecMap) -> Vec { + clients.values().filter_map(|p| { + if let LobbyClientType::Player { name } = p.client_type.clone() { + Some(name) + } else { + None + } + }).collect() + } + pub fn join_player(&mut self, send: &ClientSender) -> Result{ match &mut self.lobby_state { LobbyState::Lobby { clients, settings } => { - let name = name_validation::sanitize_name("".to_string(), clients); + let name = name_validation::sanitize_name("".to_string(), &Self::get_player_names(clients)); let mut new_player = LobbyClient::new(name.clone(), send.clone(), clients.is_empty()); let lobby_client_id: LobbyClientID = @@ -192,6 +202,14 @@ impl Lobby { } }, GameClientLocation::Spectator(idx) => { + clients.remove(&lobby_client_id); + for client in clients.iter_mut() { + if let GameClientLocation::Spectator(index) = &mut client.1.client_location { + if *index > idx { + *index -= 1; + } + } + } game.remove_spectator(idx); } } @@ -299,6 +317,34 @@ impl Lobby { } } + pub fn set_player_name(lobby_client_id: LobbyClientID, name: String, clients: &mut VecMap) { + let mut other_players = clients.clone(); + other_players.remove(&lobby_client_id); + + let new_name: String = name_validation::sanitize_name(name, &Self::get_player_names(&other_players)); + + if let Some(player) = clients.get_mut(&lobby_client_id){ + if let LobbyClientType::Player { name } = &mut player.client_type { + *name = new_name; + } + } + + Self::send_players_lobby(clients); + } + + pub fn set_player_name_game(game: &mut Game, player_ref: PlayerReference, name: String) { + let mut other_players: Vec = PlayerReference::all_players(game) + .map(|p| p.name(game)) + .cloned() + .collect(); + + other_players.remove(player_ref.index() as usize); + + let new_name: String = name_validation::sanitize_name(name, &other_players); + + player_ref.set_name(game, new_name); + } + pub fn is_closed(&self) -> bool { matches!(self.lobby_state, LobbyState::Closed) } diff --git a/server/src/lobby/name_validation.rs b/server/src/lobby/name_validation.rs index cf997001c..356233171 100644 --- a/server/src/lobby/name_validation.rs +++ b/server/src/lobby/name_validation.rs @@ -28,33 +28,21 @@ pub const DEFAULT_SERVER_NAME: &str = "Mafia Lobby"; /// Sanitizes a player name. /// If the desired name is invalid or taken, this generates a random acceptable name. /// Otherwise, this trims and returns the input name. -pub fn sanitize_name(mut desired_name: String, players: &VecMap) -> String { +pub fn sanitize_name(mut desired_name: String, other_names: &Vec) -> String { desired_name = desired_name .remove_newline() .trim_whitespace() .truncate(MAX_NAME_LENGTH) .truncate_lines(1); - let name_already_taken = players.values().any(|existing_player| - if let LobbyClientType::Player { name } = &existing_player.client_type { - desired_name == *name - }else{ - false - } + let name_already_taken = other_names.iter().any(|name| + desired_name == *name ); if !desired_name.is_empty() && !name_already_taken { desired_name } else { - generate_random_name(&players.values() - .filter_map(|p| - if let LobbyClientType::Player { name } = &p.client_type { - Some(name.as_str()) - }else{ - None - } - ) - .collect::>()) + generate_random_name(&other_names.iter().map(|s| s.as_str()).collect::>()) } } diff --git a/server/src/lobby/on_client_message.rs b/server/src/lobby/on_client_message.rs index 5bd0d6e58..9d6838517 100644 --- a/server/src/lobby/on_client_message.rs +++ b/server/src/lobby/on_client_message.rs @@ -1,6 +1,6 @@ use std::{collections::VecDeque, time::{Duration, Instant}}; -use crate::{game::{chat::{ChatMessage, ChatMessageVariant}, phase::PhaseType, player::{PlayerIndex, PlayerInitializeParameters}, spectator::{spectator_pointer::SpectatorIndex, SpectatorInitializeParameters}, Game}, lobby::game_client::{GameClient, GameClientLocation}, log, packet::{ToClientPacket, ToServerPacket}, strings::TidyableString, vec_map::VecMap, websocket_connections::connection::ClientSender}; +use crate::{game::{chat::{ChatMessage, ChatMessageVariant}, event::{on_fast_forward::OnFastForward, on_game_ending::OnGameEnding}, game_conclusion::GameConclusion, phase::PhaseType, player::{PlayerIndex, PlayerInitializeParameters, PlayerReference}, spectator::{spectator_pointer::{SpectatorIndex, SpectatorPointer}, SpectatorInitializeParameters}, Game}, lobby::game_client::{GameClient, GameClientLocation}, log, packet::{HostDataPacketGameClient, ToClientPacket, ToServerPacket}, strings::TidyableString, vec_map::VecMap, websocket_connections::connection::ClientSender}; use super::{lobby_client::{LobbyClient, LobbyClientID, LobbyClientType, Ready}, name_validation::{self, sanitize_server_name}, Lobby, LobbyState}; @@ -92,7 +92,7 @@ impl Lobby { return }; - let new_name = name_validation::sanitize_name("".to_string(), clients); + let new_name = name_validation::sanitize_name("".to_string(), &Self::get_player_names(clients)); if let Some(player) = clients.get_mut(&lobby_client_id){ match &player.client_type { LobbyClientType::Spectator => { @@ -119,17 +119,7 @@ impl Lobby { return }; - let mut other_players = clients.clone(); - other_players.remove(&lobby_client_id); - - let new_name: String = name_validation::sanitize_name(name, &other_players); - if let Some(player) = clients.get_mut(&lobby_client_id){ - if let LobbyClientType::Player { name } = &mut player.client_type { - *name = new_name; - } - } - - Self::send_players_lobby(clients); + Self::set_player_name(lobby_client_id, name, clients); }, ToServerPacket::ReadyUp{ ready } => { let LobbyState::Lobby { clients, .. } = &mut self.lobby_state else { @@ -396,6 +386,75 @@ impl Lobby { _ => unreachable!("LobbyState::Lobby was set to be to LobbyState::Lobby in the previous line") } } + ToServerPacket::EndGame => { + let LobbyState::Game { game, clients } = &mut self.lobby_state else { + log!(error "Lobby"; "{} {}", "Can't end game while in lobby", lobby_client_id); + return; + }; + if let Some(player) = clients.get(&lobby_client_id){ + if !player.host {return;} + } + + let conclusion = GameConclusion::get_premature_conclusion(game); + + OnGameEnding::new(conclusion).invoke(game); + } + ToServerPacket::SkipPhase => { + let LobbyState::Game { game, clients } = &mut self.lobby_state else { + log!(error "Lobby"; "{} {}", "Can't skip phase while in lobby", lobby_client_id); + return; + }; + if let Some(player) = clients.get(&lobby_client_id){ + if !player.host {return;} + } + + OnFastForward::invoke(game); + } + ToServerPacket::HostDataRequest => { + let LobbyState::Game { clients, game } = &mut self.lobby_state else { + log!(error "Lobby"; "{} {}", "Can't request game host data while in lobby", lobby_client_id); + return; + }; + if let Some(player) = clients.get(&lobby_client_id){ + if !player.host {return;} + } + + send.send(ToClientPacket::HostData { clients: clients.iter() + .map(|(id, client)| { + return (*id, HostDataPacketGameClient { + client_type: client.client_location.clone(), + connection: match client.client_location { + GameClientLocation::Player(index) => { + unsafe { PlayerReference::new_unchecked(index) }.connection(game).clone() + } + GameClientLocation::Spectator(index) => { + SpectatorPointer::new(index).connection(game) + } + }, + host: client.host + }) + }).collect() + }); + } + ToServerPacket::SetPlayerName { id, name } => { + if let LobbyState::Game { game, clients } = &mut self.lobby_state { + if let Some(player) = clients.get(&lobby_client_id){ + if !player.host {return;} + } + if let Some(player) = clients.get(&id) { + if let GameClientLocation::Player(index) = player.client_location { + if let Ok(player_ref) = PlayerReference::new(game, index) { + Self::set_player_name_game(game, player_ref, name); + } + } + } + } else if let LobbyState::Lobby { clients, .. } = &mut self.lobby_state { + if let Some(player) = clients.get(&lobby_client_id) { + if !player.is_host() { return } + } + Self::set_player_name(id, name, clients); + }; + } _ => { let LobbyState::Game { game, clients } = &mut self.lobby_state else { log!(error "Lobby"; "{} {:?}", "ToServerPacket not implemented for lobby was sent during lobby: ", incoming_packet); diff --git a/server/src/packet.rs b/server/src/packet.rs index bb8f6dd5a..051725c95 100644 --- a/server/src/packet.rs +++ b/server/src/packet.rs @@ -22,20 +22,12 @@ use serde::{Deserialize, Serialize}; use vec1::Vec1; use crate::{ - game::{ - ability_input::*, - available_buttons::AvailableButtons, - chat::{ChatGroup, ChatMessage}, - components::insider_group::InsiderGroupID, - grave::Grave, modifiers::ModifierType, phase::{PhaseState, PhaseType}, - player::{PlayerIndex, PlayerReference}, - role::{ + client_connection::ClientConnection, game::{ + ability_input::*, available_buttons::AvailableButtons, chat::{ChatGroup, ChatMessage}, components::insider_group::InsiderGroupID, grave::Grave, modifiers::ModifierType, phase::{PhaseState, PhaseType}, player::{PlayerIndex, PlayerReference}, role::{ doomsayer::DoomsayerGuess, ClientRoleStateEnum, Role - }, - role_list::{RoleList, RoleOutline}, settings::PhaseTimeSettings, - tag::Tag, verdict::Verdict, Game, GameOverReason, RejectStartReason - }, listener::RoomCode, lobby::lobby_client::{LobbyClient, LobbyClientID}, log, vec_map::VecMap, vec_set::VecSet + }, role_list::{RoleList, RoleOutline}, settings::PhaseTimeSettings, spectator::spectator_pointer::SpectatorIndex, tag::Tag, verdict::Verdict, Game, GameOverReason, RejectStartReason + }, listener::RoomCode, lobby::{game_client::GameClientLocation, lobby_client::{LobbyClient, LobbyClientID}}, log, vec_map::VecMap, vec_set::VecSet }; #[derive(Serialize, Debug, Clone)] @@ -46,6 +38,14 @@ pub struct LobbyPreviewData { pub players: Vec<(LobbyClientID, String)> } +#[derive(Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct HostDataPacketGameClient { + pub client_type: GameClientLocation, + pub connection: ClientConnection, + pub host: bool, +} + #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[serde(tag = "type")] @@ -66,22 +66,20 @@ pub enum ToClientPacket{ RejectJoin{reason: RejectJoinReason}, // Lobby + LobbyName{name: String}, #[serde(rename_all = "camelCase")] YourId{player_id: LobbyClientID}, #[serde(rename_all = "camelCase")] LobbyClients{clients: VecMap}, - LobbyName{name: String}, - #[serde(rename_all = "camelCase")] - RejectStart{reason: RejectStartReason}, PlayersHost{hosts: Vec}, PlayersReady{ready: Vec}, #[serde(rename_all = "camelCase")] PlayersLostConnection{lost_connection: Vec}, StartGame, - GameInitializationComplete, - BackToLobby, + #[serde(rename_all = "camelCase")] + RejectStart{reason: RejectStartReason}, - GamePlayers{players: Vec}, + // Settings #[serde(rename_all = "camelCase")] RoleList{role_list: RoleList}, #[serde(rename_all = "camelCase")] @@ -95,8 +93,13 @@ pub enum ToClientPacket{ #[serde(rename_all = "camelCase")] EnabledModifiers{modifiers: Vec}, + // Host + HostData { clients: VecMap }, + // Game - + GamePlayers{players: Vec}, + GameInitializationComplete, + BackToLobby, #[serde(rename_all = "camelCase")] YourPlayerIndex{player_index: PlayerIndex}, #[serde(rename_all = "camelCase")] @@ -105,8 +108,6 @@ pub enum ToClientPacket{ Phase{phase: PhaseState, day_number: u8}, #[serde(rename_all = "camelCase")] PhaseTimeLeft{seconds_left: u64}, - #[serde(rename_all = "camelCase")] - PlayerOnTrial{player_index: PlayerIndex}, PlayerAlive{alive: Vec}, #[serde(rename_all = "camelCase")] @@ -214,6 +215,8 @@ pub enum ToServerPacket{ ReadyUp{ready: bool}, SetLobbyName{name: String}, StartGame, + + // Settings #[serde(rename_all = "camelCase")] SetRoleList{role_list: RoleList}, #[serde(rename_all = "camelCase")] @@ -229,7 +232,12 @@ pub enum ToServerPacket{ #[serde(rename_all = "camelCase")] SetEnabledModifiers{modifiers: Vec}, + // Host BackToLobby, + HostDataRequest, + EndGame, + SkipPhase, + SetPlayerName { id: LobbyClientID, name: String }, // Game #[serde(rename_all = "camelCase")]