Working real time lobby
This commit is contained in:
parent
61ae4b901d
commit
f1ea064d4c
13 changed files with 386 additions and 152 deletions
|
@ -4,14 +4,22 @@ import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
import useSocket from "@hooks/useSocket"
|
import useSocket from "@hooks/useSocket"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
import { useRouter } from "next/router"
|
import { useRouter } from "next/router"
|
||||||
import { Fragment, useEffect, useState } from "react"
|
import { Fragment, useEffect, useState } from "react"
|
||||||
|
|
||||||
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
|
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
|
||||||
const { payload } = useGameProps()
|
const { payload, full, leave, reset } = useGameProps()
|
||||||
const [dots, setDots] = useState(3)
|
const [dots, setDots] = useState(3)
|
||||||
const { isConnected } = useSocket()
|
const { isConnected } = useSocket()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { data: session } = useSession()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (payload?.game?.id || !isConnected) return
|
||||||
|
socket.emit("update", full)
|
||||||
|
}, [full, payload?.game?.id, isConnected])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (payload?.player2) return
|
if (payload?.player2) return
|
||||||
|
@ -38,15 +46,15 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
|
||||||
<div className="flex items-center justify-around">
|
<div className="flex items-center justify-around">
|
||||||
<Player
|
<Player
|
||||||
src="player_blue.png"
|
src="player_blue.png"
|
||||||
text={payload?.player1?.name ?? "Spieler 1 (Du)"}
|
player={payload?.player1}
|
||||||
primary={true}
|
userId={session?.user.id}
|
||||||
edit={true}
|
|
||||||
/>
|
/>
|
||||||
<p className="font-farro m-4 text-6xl font-semibold">VS</p>
|
<p className="font-farro m-4 text-6xl font-semibold">VS</p>
|
||||||
{payload?.player2 ? (
|
{payload?.player2 ? (
|
||||||
<Player
|
<Player
|
||||||
src="player_red.png"
|
src="player_red.png"
|
||||||
text={payload?.player2.name ?? "Spieler 2"}
|
player={payload?.player2}
|
||||||
|
userId={session?.user.id}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p className="font-farro w-96 text-center text-4xl font-medium">
|
<p className="font-farro w-96 text-center text-4xl font-medium">
|
||||||
|
@ -60,9 +68,17 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center border-t-2 border-slate-900">
|
<div className="flex items-center justify-center border-t-2 border-slate-900">
|
||||||
<button
|
<button
|
||||||
className="font-farro m-4 rounded-xl border-b-4 border-orange-400 bg-warn px-12 py-4 text-5xl font-medium duration-100 active:border-t-4 active:border-b-0"
|
className="font-farro mx-32 my-4 rounded-xl border-b-4 border-red-400 bg-red-500 px-12 py-4 text-5xl font-medium duration-100 active:border-t-4 active:border-b-0"
|
||||||
onClick={() => router.push("/")}
|
onClick={() => {
|
||||||
|
leave(async () => {
|
||||||
|
await router.push("/")
|
||||||
|
reset()
|
||||||
|
})
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
LEAVE
|
||||||
|
</button>
|
||||||
|
<button className="font-farro mx-32 my-4 rounded-xl border-b-4 border-orange-400 bg-warn px-12 py-4 text-5xl font-medium duration-100 active:border-t-4 active:border-b-0">
|
||||||
START
|
START
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
|
||||||
|
import { PlayerSchema } from "@lib/zodSchemas"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
|
|
||||||
function Player({
|
function Player({
|
||||||
src,
|
src,
|
||||||
text,
|
player,
|
||||||
primary,
|
userId,
|
||||||
edit,
|
|
||||||
}: {
|
}: {
|
||||||
src: string
|
src: string
|
||||||
text: string
|
player?: PlayerSchema
|
||||||
primary?: boolean
|
userId?: string
|
||||||
edit?: boolean
|
|
||||||
}) {
|
}) {
|
||||||
|
const text =
|
||||||
|
player?.name ?? "Spieler " + (player?.index === "player2" ? "2" : "1")
|
||||||
|
const primary = userId === player?.id
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-96 flex-col items-center gap-16 py-8">
|
<div className="flex w-96 flex-col items-center gap-16 py-8">
|
||||||
<p
|
<p
|
||||||
|
@ -25,7 +28,7 @@ function Player({
|
||||||
</p>
|
</p>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<img className="pixelart w-64" src={"/assets/" + src} alt={src} />
|
<img className="pixelart w-64" src={"/assets/" + src} alt={src} />
|
||||||
{edit ? (
|
{primary ? (
|
||||||
<button className="absolute top-4 right-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90">
|
<button className="absolute top-4 right-4 h-14 w-14 rounded-lg border-2 border-dashed border-warn bg-gray-800 bg-opacity-90">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className="h-full w-full text-warn"
|
className="h-full w-full text-warn"
|
||||||
|
|
|
@ -4,10 +4,8 @@ import {
|
||||||
} from "@fortawesome/pro-solid-svg-icons"
|
} from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
import { socket } from "@lib/socket"
|
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import { ReactNode, useMemo } from "react"
|
import { ReactNode, useMemo } from "react"
|
||||||
import { toast } from "react-toastify"
|
|
||||||
|
|
||||||
type GameSettingKeys =
|
type GameSettingKeys =
|
||||||
| "allowSpectators"
|
| "allowSpectators"
|
||||||
|
@ -17,24 +15,14 @@ type GameSettingKeys =
|
||||||
|
|
||||||
export type GameSettings = { [key in GameSettingKeys]?: boolean }
|
export type GameSettings = { [key in GameSettingKeys]?: boolean }
|
||||||
|
|
||||||
export const gameSetting = (payload: GameSettings) => {
|
|
||||||
socket.emit("gameSetting", payload, ({ ack }) => {
|
|
||||||
if (ack) return
|
|
||||||
toast.warn("Something is wrong... ", {
|
|
||||||
toastId: "st_wrong",
|
|
||||||
theme: "colored",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function Setting({
|
function Setting({
|
||||||
children,
|
children,
|
||||||
prop,
|
props: { prop, gameSetting },
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
prop: GameSettingKeys
|
props: { prop: GameSettingKeys; gameSetting: (payload: GameSettings) => void }
|
||||||
}) {
|
}) {
|
||||||
const { payload, setSetting } = useGameProps()
|
const { payload } = useGameProps()
|
||||||
const state = useMemo(() => payload?.game?.[prop], [payload?.game, prop])
|
const state = useMemo(() => payload?.game?.[prop], [payload?.game, prop])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -62,8 +50,6 @@ function Setting({
|
||||||
const payload = {
|
const payload = {
|
||||||
[prop]: !state,
|
[prop]: !state,
|
||||||
}
|
}
|
||||||
|
|
||||||
setSetting(payload)
|
|
||||||
gameSetting(payload)
|
gameSetting(payload)
|
||||||
}}
|
}}
|
||||||
hidden={true}
|
hidden={true}
|
||||||
|
|
|
@ -1,11 +1,25 @@
|
||||||
import Setting, { gameSetting } from "./Setting"
|
import Setting, { GameSettings } from "./Setting"
|
||||||
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
import { faRotateLeft } from "@fortawesome/pro-regular-svg-icons"
|
||||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons"
|
import { faXmark } from "@fortawesome/pro-solid-svg-icons"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "@hooks/useGameProps"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import { useCallback } from "react"
|
||||||
|
|
||||||
function Settings({ closeSettings }: { closeSettings: () => void }) {
|
function Settings({ closeSettings }: { closeSettings: () => void }) {
|
||||||
const { payload, setSetting } = useGameProps()
|
const { setSetting, full } = useGameProps()
|
||||||
|
|
||||||
|
const gameSetting = useCallback(
|
||||||
|
(payload: GameSettings) => {
|
||||||
|
const hash = setSetting(payload)
|
||||||
|
socket.emit("gameSetting", payload, (newHash) => {
|
||||||
|
if (newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", full)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[full, setSetting]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40">
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/40">
|
||||||
|
@ -37,7 +51,6 @@ function Settings({ closeSettings }: { closeSettings: () => void }) {
|
||||||
allowChat: true,
|
allowChat: true,
|
||||||
allowMarkDraw: true,
|
allowMarkDraw: true,
|
||||||
}
|
}
|
||||||
setSetting(payload)
|
|
||||||
gameSetting(payload)
|
gameSetting(payload)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -49,10 +62,18 @@ function Settings({ closeSettings }: { closeSettings: () => void }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<Setting prop="allowSpectators">Erlaube Zuschauer</Setting>
|
<Setting props={{ gameSetting, prop: "allowSpectators" }}>
|
||||||
<Setting prop="allowSpecials">Erlaube spezial Items</Setting>
|
Erlaube Zuschauer
|
||||||
<Setting prop="allowChat">Erlaube den Chat</Setting>
|
</Setting>
|
||||||
<Setting prop="allowMarkDraw">Erlaube zeichen/makieren</Setting>
|
<Setting props={{ gameSetting, prop: "allowSpecials" }}>
|
||||||
|
Erlaube spezial Items
|
||||||
|
</Setting>
|
||||||
|
<Setting props={{ gameSetting, prop: "allowChat" }}>
|
||||||
|
Erlaube den Chat
|
||||||
|
</Setting>
|
||||||
|
<Setting props={{ gameSetting, prop: "allowMarkDraw" }}>
|
||||||
|
Erlaube zeichen/makieren
|
||||||
|
</Setting>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
|
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
|
||||||
|
import { socket } from "@lib/socket"
|
||||||
|
import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas"
|
||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
|
import { toast } from "react-toastify"
|
||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
import { devtools } from "zustand/middleware"
|
import { devtools } from "zustand/middleware"
|
||||||
|
|
||||||
const initialState: GamePropsSchema = { payload: null, hash: null }
|
const initialState: GamePropsSchema & {
|
||||||
|
queue: { payload: string; hash: string }[]
|
||||||
|
} = { payload: null, hash: null, queue: [] }
|
||||||
|
|
||||||
type State = GamePropsSchema
|
export type State = typeof initialState
|
||||||
|
|
||||||
type Action = {
|
export type Action = {
|
||||||
setSetting: (by: GameSettings) => void
|
setSetting: (settings: GameSettings) => string | null
|
||||||
|
setPlayer: (payload: {
|
||||||
|
player1?: PlayerSchema
|
||||||
|
player2?: PlayerSchema
|
||||||
|
}) => string | null
|
||||||
full: (newProps: GamePropsSchema) => void
|
full: (newProps: GamePropsSchema) => void
|
||||||
|
leave: (cb: () => void) => void
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,13 +28,52 @@ export const useGameProps = create<State & Action>()(
|
||||||
devtools(
|
devtools(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
setSetting: (settings) =>
|
setPlayer: (payload) => {
|
||||||
|
let hash: string | null = null
|
||||||
set(
|
set(
|
||||||
produce((state: State) => {
|
produce((state: State) => {
|
||||||
|
if (!state.payload) return
|
||||||
|
Object.assign(state.payload, payload)
|
||||||
|
const body = getPayloadwithChecksum(state.payload)
|
||||||
|
if (!body.hash) {
|
||||||
|
toast.warn("Something is wrong... ", {
|
||||||
|
toastId: "st_wrong",
|
||||||
|
theme: "colored",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash = body.hash
|
||||||
|
state.hash = hash
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return hash
|
||||||
|
},
|
||||||
|
setSetting: (settings) => {
|
||||||
|
const payload = JSON.stringify(settings)
|
||||||
|
let hash: string | null = null
|
||||||
|
set(
|
||||||
|
produce((state: State) => {
|
||||||
|
const length = state.queue.length
|
||||||
|
state.queue.filter((e) => e.payload !== payload || e.hash !== hash)
|
||||||
|
if (state.queue.length !== length) return
|
||||||
|
|
||||||
if (!state.payload?.game) return
|
if (!state.payload?.game) return
|
||||||
Object.assign(state.payload.game, settings)
|
Object.assign(state.payload.game, settings)
|
||||||
|
const body = getPayloadwithChecksum(state.payload)
|
||||||
|
if (!body.hash) {
|
||||||
|
toast.warn("Something is wrong... ", {
|
||||||
|
toastId: "st_wrong",
|
||||||
|
theme: "colored",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash = body.hash
|
||||||
|
state.hash = hash
|
||||||
|
state.queue.push({ payload, hash })
|
||||||
})
|
})
|
||||||
),
|
)
|
||||||
|
return hash
|
||||||
|
},
|
||||||
full: (newGameProps) =>
|
full: (newGameProps) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (state.hash === newGameProps.hash) {
|
if (state.hash === newGameProps.hash) {
|
||||||
|
@ -47,6 +96,14 @@ export const useGameProps = create<State & Action>()(
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}),
|
}),
|
||||||
|
leave: (cb) => {
|
||||||
|
socket.emit("leave", (ack) => {
|
||||||
|
if (!ack) {
|
||||||
|
toast.error("Something is wrong...")
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
})
|
||||||
|
},
|
||||||
reset: () => {
|
reset: () => {
|
||||||
set(initialState)
|
set(initialState)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,28 +1,34 @@
|
||||||
import { useGameProps } from "@hooks/useGameProps"
|
import { useGameProps } from "./useGameProps"
|
||||||
import { socket } from "@lib/socket"
|
import { socket } from "@lib/socket"
|
||||||
|
import status from "http-status"
|
||||||
|
import { useRouter } from "next/router"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { toast } from "react-toastify"
|
import { toast } from "react-toastify"
|
||||||
|
|
||||||
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
|
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
|
||||||
function useSocket() {
|
function useSocket() {
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
const { full } = useGameProps()
|
const { setPlayer, setSetting, full, hash: stateHash } = useGameProps()
|
||||||
|
const router = useRouter()
|
||||||
useEffect(() => setIsConnected(socket.connected), [socket.connected])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.connect()
|
socket.connect()
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
|
setIsConnected(true)
|
||||||
console.log("connected")
|
console.log("connected")
|
||||||
|
|
||||||
const toastId = "connect_error"
|
const toastId = "connect_error"
|
||||||
toast.dismiss(toastId)
|
toast.dismiss(toastId)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("connect_error", () => {
|
socket.on("connect_error", (error) => {
|
||||||
|
console.log("Connection error:", error.message)
|
||||||
|
if (error.message === status["403"]) router.push("/")
|
||||||
|
if (error.message !== "xhr poll error") return
|
||||||
const toastId = "connect_error"
|
const toastId = "connect_error"
|
||||||
const isActive = toast.isActive(toastId)
|
const isActive = toast.isActive(toastId)
|
||||||
|
console.log(toastId, isActive)
|
||||||
if (isActive)
|
if (isActive)
|
||||||
toast.update(toastId, {
|
toast.update(toastId, {
|
||||||
autoClose: 5000,
|
autoClose: 5000,
|
||||||
|
@ -31,13 +37,51 @@ function useSocket() {
|
||||||
toast.warn("Es gibt Probleme mit der Echtzeitverbindung.", { toastId })
|
toast.warn("Es gibt Probleme mit der Echtzeitverbindung.", { toastId })
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("gameSetting", (payload, hash) => {
|
||||||
console.log("disconnect")
|
const newHash = setSetting(payload)
|
||||||
|
if (!newHash || newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", (body) => {
|
||||||
|
console.log("update")
|
||||||
|
full(body)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("update", (body) => {
|
socket.on("playerEvent", (payload, hash, type) => {
|
||||||
console.log("update")
|
let message: string
|
||||||
full(body)
|
switch (type) {
|
||||||
|
case "disconnect":
|
||||||
|
message = "Player is disconnected."
|
||||||
|
break
|
||||||
|
|
||||||
|
case "leave":
|
||||||
|
message = "Player has left the lobby."
|
||||||
|
break
|
||||||
|
|
||||||
|
case "join":
|
||||||
|
if (hash === stateHash || !stateHash) return
|
||||||
|
message = "Player has joined the lobby."
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
message = "Not defined yet."
|
||||||
|
break
|
||||||
|
}
|
||||||
|
toast.info(message, { toastId: message })
|
||||||
|
console.log(payload)
|
||||||
|
const newHash = setPlayer(payload)
|
||||||
|
console.log(newHash, hash, !newHash, newHash === hash)
|
||||||
|
if (!newHash || newHash === hash) return
|
||||||
|
console.log("hash", hash, newHash)
|
||||||
|
socket.emit("update", (body) => {
|
||||||
|
console.log("update")
|
||||||
|
full(body)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("disconnect")
|
||||||
|
setIsConnected(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -46,46 +90,6 @@ function useSocket() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// const toastId = "realtime"
|
|
||||||
|
|
||||||
// toast("Echtzeitverbindung wird hergestellt...", {
|
|
||||||
// icon: Icons.spinner(),
|
|
||||||
// toastId,
|
|
||||||
// autoClose: false,
|
|
||||||
// hideProgressBar: true,
|
|
||||||
// closeButton: false,
|
|
||||||
// })
|
|
||||||
// socket.emit("join", ({ ack }) => {
|
|
||||||
// if (!ack) {
|
|
||||||
// toast.update(toastId, {
|
|
||||||
// render: "Bei der Echtzeitverbindung ist ein Fehler aufgetreten 🤯",
|
|
||||||
// type: "error",
|
|
||||||
// icon: Icons.error,
|
|
||||||
// theme: "colored",
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeButton: true,
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// setHasJoined(true)
|
|
||||||
// toast.update(toastId, {
|
|
||||||
// render: "Echtzeitverbindung hergestellt 👌",
|
|
||||||
// type: "info",
|
|
||||||
// icon: Icons.success,
|
|
||||||
// autoClose: 5000,
|
|
||||||
// hideProgressBar: false,
|
|
||||||
// closeButton: true,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (!isConnected) return
|
|
||||||
// socket.emit("authenticate", { token: `hello from ${session?.user.email}` })
|
|
||||||
// }, [isConnected, status, session?.user.email])
|
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (!isConnected) return
|
// if (!isConnected) return
|
||||||
// let count = 0
|
// let count = 0
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
|
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas"
|
||||||
import { User } from "@prisma/client"
|
import { User } from "@prisma/client"
|
||||||
import type { Server as HTTPServer } from "http"
|
import type { Server as HTTPServer } from "http"
|
||||||
import type { Socket as NetSocket } from "net"
|
import type { Socket as NetSocket } from "net"
|
||||||
import type { NextApiResponse } from "next"
|
import type { NextApiResponse } from "next"
|
||||||
import type { Server as IOServer, Server } from "socket.io"
|
import type {
|
||||||
import { Socket } from "socket.io-client"
|
Server as IOServer,
|
||||||
|
Server,
|
||||||
|
Socket as SocketforServer,
|
||||||
|
} from "socket.io"
|
||||||
|
import type { Socket as SocketforClient } from "socket.io-client"
|
||||||
|
|
||||||
interface SocketServer extends HTTPServer {
|
interface SocketServer extends HTTPServer {
|
||||||
io?: IOServer
|
io?: IOServer
|
||||||
|
@ -23,16 +27,20 @@ export interface ServerToClientEvents {
|
||||||
// noArg: () => void
|
// noArg: () => void
|
||||||
// basicEmit: (a: number, b: string, c: Buffer) => void
|
// basicEmit: (a: number, b: string, c: Buffer) => void
|
||||||
// withAck: (d: string, ) => void
|
// withAck: (d: string, ) => void
|
||||||
update: (game: GamePropsSchema) => void
|
gameSetting: (payload: GameSettings, hash: string) => void
|
||||||
|
playerEvent: (
|
||||||
|
payload: { player1?: PlayerSchema; player2?: PlayerSchema },
|
||||||
|
hash: string,
|
||||||
|
type: "join" | "leave" | "disconnect"
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientToServerEvents {
|
export interface ClientToServerEvents {
|
||||||
|
update: (callback: (game: GamePropsSchema) => void) => void
|
||||||
ping: (count: number, callback: (count: number) => void) => void
|
ping: (count: number, callback: (count: number) => void) => void
|
||||||
join: (withAck: ({ ack }: { ack: boolean }) => void) => void
|
join: (withAck: (ack: boolean) => void) => void
|
||||||
gameSetting: (
|
gameSetting: (payload: GameSettings, callback: (hash: string) => void) => void
|
||||||
payload: GameSettings,
|
leave: (withAck: (ack: boolean) => void) => void
|
||||||
withAck: ({ ack }: { ack: boolean }) => void
|
|
||||||
) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InterServerEvents {
|
interface InterServerEvents {
|
||||||
|
@ -44,11 +52,20 @@ interface SocketData {
|
||||||
gameId: string | null
|
gameId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type cServer = Server<
|
export type sServer = Server<
|
||||||
|
ClientToServerEvents,
|
||||||
|
ServerToClientEvents,
|
||||||
|
InterServerEvents,
|
||||||
|
SocketData
|
||||||
|
>
|
||||||
|
export type sSocket = SocketforServer<
|
||||||
ClientToServerEvents,
|
ClientToServerEvents,
|
||||||
ServerToClientEvents,
|
ServerToClientEvents,
|
||||||
InterServerEvents,
|
InterServerEvents,
|
||||||
SocketData
|
SocketData
|
||||||
>
|
>
|
||||||
|
|
||||||
export type cSocket = Socket<ServerToClientEvents, ClientToServerEvents>
|
export type cSocket = SocketforClient<
|
||||||
|
ServerToClientEvents,
|
||||||
|
ClientToServerEvents
|
||||||
|
>
|
||||||
|
|
|
@ -1,25 +1,29 @@
|
||||||
import { GameState, PlayerN } from "@prisma/client"
|
import { GameState, PlayerN } from "@prisma/client"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const PlayerSchema = z.object({
|
export const PlayerSchema = z
|
||||||
id: z.string(),
|
.object({
|
||||||
name: z.string().nullable(),
|
id: z.string(),
|
||||||
index: z.nativeEnum(PlayerN),
|
name: z.string().nullable(),
|
||||||
chats: z
|
index: z.nativeEnum(PlayerN),
|
||||||
.object({
|
chats: z
|
||||||
id: z.string(),
|
.object({
|
||||||
event: z.string().nullable(),
|
id: z.string(),
|
||||||
message: z.string().nullable(),
|
event: z.string().nullable(),
|
||||||
createdAt: z.date(),
|
message: z.string().nullable(),
|
||||||
})
|
createdAt: z.date(),
|
||||||
.array(),
|
})
|
||||||
moves: z
|
.array(),
|
||||||
.object({
|
moves: z
|
||||||
id: z.string(),
|
.object({
|
||||||
index: z.number(),
|
id: z.string(),
|
||||||
})
|
index: z.number(),
|
||||||
.array(),
|
})
|
||||||
})
|
.array(),
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
|
||||||
|
export type PlayerSchema = z.infer<typeof PlayerSchema>
|
||||||
|
|
||||||
export const CreateSchema = z
|
export const CreateSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -34,13 +38,13 @@ export const CreateSchema = z
|
||||||
})
|
})
|
||||||
.nullable(),
|
.nullable(),
|
||||||
gamePin: z.string().nullable(),
|
gamePin: z.string().nullable(),
|
||||||
player1: PlayerSchema.nullable(),
|
player1: PlayerSchema,
|
||||||
player2: PlayerSchema.nullable(),
|
player2: PlayerSchema,
|
||||||
})
|
})
|
||||||
.strict()
|
.nullable()
|
||||||
|
|
||||||
export const GamePropsSchema = z.object({
|
export const GamePropsSchema = z.object({
|
||||||
payload: CreateSchema.nullable(),
|
payload: CreateSchema,
|
||||||
hash: z.string().nullable(),
|
hash: z.string().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { authOptions } from "../auth/[...nextauth]"
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
import { composeBody, gameSelects } from "./running"
|
import running, { composeBody, gameSelects } from "./running"
|
||||||
import sendError from "@backend/sendError"
|
import sendError from "@backend/sendError"
|
||||||
import sendResponse from "@backend/sendResponse"
|
import sendResponse from "@backend/sendResponse"
|
||||||
import { rejectionErrors } from "@lib/backend/errors"
|
import { rejectionErrors } from "@lib/backend/errors"
|
||||||
|
@ -39,6 +39,27 @@ export default async function join(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const games = await prisma.game.findMany({
|
||||||
|
where: {
|
||||||
|
NOT: {
|
||||||
|
state: "ended",
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
userId: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
if (games.length) {
|
||||||
|
return sendResponse(req, res, {
|
||||||
|
message: "Spieler ist bereits in Spiel!",
|
||||||
|
statusCode: 409,
|
||||||
|
type: ["infoCyan"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const user_Game = await prisma.user_Game.create({
|
const user_Game = await prisma.user_Game.create({
|
||||||
data: {
|
data: {
|
||||||
gameId: game.id,
|
gameId: game.id,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { authOptions } from "../auth/[...nextauth]"
|
import { authOptions } from "../auth/[...nextauth]"
|
||||||
import sendResponse from "@backend/sendResponse"
|
import sendResponse from "@backend/sendResponse"
|
||||||
import { rejectionErrors } from "@lib/backend/errors"
|
import { rejectionErrors } from "@lib/backend/errors"
|
||||||
import { getPayloadwithChecksum } from "@lib/getObjectChecksum"
|
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
|
||||||
import prisma from "@lib/prisma"
|
import prisma from "@lib/prisma"
|
||||||
import { GamePropsSchema } from "@lib/zodSchemas"
|
import { GamePropsSchema } from "@lib/zodSchemas"
|
||||||
import type { NextApiRequest, NextApiResponse } from "next"
|
import type { NextApiRequest, NextApiResponse } from "next"
|
||||||
|
@ -40,6 +40,7 @@ export const gameSelects = {
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
NextApiResponseWithSocket,
|
NextApiResponseWithSocket,
|
||||||
cServer,
|
sServer,
|
||||||
|
sSocket,
|
||||||
} from "../../interfaces/NextApiSocket"
|
} from "../../interfaces/NextApiSocket"
|
||||||
import {
|
import {
|
||||||
composeBody,
|
composeBody,
|
||||||
|
@ -10,7 +11,9 @@ import {
|
||||||
} from "./game/running"
|
} from "./game/running"
|
||||||
import logging from "@lib/backend/logging"
|
import logging from "@lib/backend/logging"
|
||||||
import prisma from "@lib/prisma"
|
import prisma from "@lib/prisma"
|
||||||
|
import { GamePropsSchema } from "@lib/zodSchemas"
|
||||||
import colors from "colors"
|
import colors from "colors"
|
||||||
|
import status from "http-status"
|
||||||
import { NextApiRequest } from "next"
|
import { NextApiRequest } from "next"
|
||||||
import { getSession } from "next-auth/react"
|
import { getSession } from "next-auth/react"
|
||||||
import { Server } from "socket.io"
|
import { Server } from "socket.io"
|
||||||
|
@ -25,7 +28,7 @@ const SocketHandler = async (
|
||||||
logging("Socket is already running " + req.url, ["infoCyan"], req)
|
logging("Socket is already running " + req.url, ["infoCyan"], req)
|
||||||
} else {
|
} else {
|
||||||
logging("Socket is initializing " + req.url, ["infoCyan"], req)
|
logging("Socket is initializing " + req.url, ["infoCyan"], req)
|
||||||
const io: cServer = new Server(res.socket.server, {
|
const io: sServer = new Server(res.socket.server, {
|
||||||
path: "/api/ws",
|
path: "/api/ws",
|
||||||
cors: {
|
cors: {
|
||||||
origin: "https://leaky-ships.mal-noh.de",
|
origin: "https://leaky-ships.mal-noh.de",
|
||||||
|
@ -54,14 +57,15 @@ const SocketHandler = async (
|
||||||
["debug"],
|
["debug"],
|
||||||
socket.request
|
socket.request
|
||||||
)
|
)
|
||||||
|
next(new Error(status["403"]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
socket.data.gameId = game.id
|
socket.data.gameId = game.id
|
||||||
socket.join(game.id)
|
socket.join(game.id)
|
||||||
next()
|
next()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logging("Unauthorized", ["warn"], socket.request)
|
logging(status["401"], ["warn"], socket.request)
|
||||||
next(new Error("Unauthorized"))
|
next(new Error(status["401"]))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -73,37 +77,116 @@ const SocketHandler = async (
|
||||||
["infoGreen"],
|
["infoGreen"],
|
||||||
socket.request
|
socket.request
|
||||||
)
|
)
|
||||||
const game = await getAnyGame(socket.data.gameId ?? "")
|
join(socket, io)
|
||||||
if (!socket.data.user || !socket.data.gameId || !game)
|
|
||||||
return socket.disconnect()
|
socket.on("update", async (cb) => {
|
||||||
const body = composeBody(game)
|
const game = await getAnyGame(socket.data.gameId ?? "")
|
||||||
io.to(socket.data.gameId).emit("update", body)
|
if (!game) return
|
||||||
|
const body = composeBody(game)
|
||||||
|
cb(body)
|
||||||
|
})
|
||||||
|
|
||||||
socket.on("gameSetting", async (payload, cb) => {
|
socket.on("gameSetting", async (payload, cb) => {
|
||||||
if (!socket.data.gameId) {
|
|
||||||
cb({ ack: false })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const game = await prisma.game.update({
|
const game = await prisma.game.update({
|
||||||
where: { id: socket.data.gameId },
|
where: { id: socket.data.gameId ?? "" },
|
||||||
data: payload,
|
data: payload,
|
||||||
...gameSelects,
|
...gameSelects,
|
||||||
})
|
})
|
||||||
const body = composeBody(game)
|
const { hash } = composeBody(game)
|
||||||
cb({ ack: true })
|
if (!hash) return
|
||||||
io.to(game.id).emit("update", body)
|
cb(hash)
|
||||||
|
io.to(game.id).emit("gameSetting", payload, hash)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("ping", (count, callback) => {
|
socket.on("ping", (count, callback) => {
|
||||||
callback(count)
|
callback(count)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("disconnecting", () => {
|
socket.on("leave", async (cb) => {
|
||||||
|
if (!socket.data.gameId || !socket.data.user?.id) return cb(false)
|
||||||
|
const user_Game = await prisma.user_Game.delete({
|
||||||
|
where: {
|
||||||
|
gameId_userId: {
|
||||||
|
gameId: socket.data.gameId,
|
||||||
|
userId: socket.data.user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const enemy = await prisma.user_Game.findFirst({
|
||||||
|
where: {
|
||||||
|
gameId: socket.data.gameId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
let body: GamePropsSchema
|
||||||
|
if (user_Game.index === "player1" && enemy) {
|
||||||
|
console.log(1)
|
||||||
|
body = composeBody(
|
||||||
|
(
|
||||||
|
await prisma.user_Game.update({
|
||||||
|
where: {
|
||||||
|
gameId_index: {
|
||||||
|
gameId: socket.data.gameId,
|
||||||
|
index: "player2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
index: "player1",
|
||||||
|
},
|
||||||
|
|
||||||
|
select: {
|
||||||
|
game: { ...gameSelects },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).game
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.log(2)
|
||||||
|
const game = await prisma.game.findUnique({
|
||||||
|
where: {
|
||||||
|
id: socket.data.gameId,
|
||||||
|
},
|
||||||
|
...gameSelects,
|
||||||
|
})
|
||||||
|
if (!game) return cb(false)
|
||||||
|
body = composeBody(game)
|
||||||
|
}
|
||||||
|
const { payload, hash } = body
|
||||||
|
console.log(payload?.player1, payload?.player2)
|
||||||
|
if (!payload || !hash) return cb(false)
|
||||||
|
io.to(socket.data.gameId).emit(
|
||||||
|
"playerEvent",
|
||||||
|
{ player1: payload.player1, player2: payload.player2 },
|
||||||
|
hash,
|
||||||
|
"leave"
|
||||||
|
)
|
||||||
|
cb(true)
|
||||||
|
|
||||||
|
if (!payload?.player1 && !payload?.player2) {
|
||||||
|
await prisma.game.delete({
|
||||||
|
where: {
|
||||||
|
id: socket.data.gameId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on("disconnecting", async () => {
|
||||||
logging(
|
logging(
|
||||||
"Disconnecting: " + JSON.stringify(Array.from(socket.rooms)),
|
"Disconnecting: " + JSON.stringify(Array.from(socket.rooms)),
|
||||||
["debug"],
|
["debug"],
|
||||||
socket.request
|
socket.request
|
||||||
) // the Set contains at least the socket ID
|
)
|
||||||
|
// if (!socket.data.gameId) return
|
||||||
|
// const game = await prisma.game.findUnique({
|
||||||
|
// where: {
|
||||||
|
// id: socket.data.gameId
|
||||||
|
// },
|
||||||
|
// ...gameSelects
|
||||||
|
// })
|
||||||
|
// if (!game) return
|
||||||
|
// const { payload, hash } = composeBody(game, socket.data.user?.id ?? "")
|
||||||
|
// if (!hash) return
|
||||||
|
// io.to(socket.data.gameId).emit("playerEvent", {}, hash, "disconnect")
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
|
@ -115,4 +198,17 @@ const SocketHandler = async (
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function join(socket: sSocket, io: sServer) {
|
||||||
|
const game = await getAnyGame(socket.data.gameId ?? "")
|
||||||
|
if (!game) return socket.disconnect()
|
||||||
|
const { payload, hash } = composeBody(game)
|
||||||
|
if (!hash) return socket.disconnect()
|
||||||
|
io.to(game.id).emit(
|
||||||
|
"playerEvent",
|
||||||
|
{ player1: payload?.player1, player2: payload?.player2 },
|
||||||
|
hash,
|
||||||
|
"join"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default SocketHandler
|
export default SocketHandler
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function isAuthenticated(res: Response) {
|
||||||
|
|
||||||
toast(status[res.status], {
|
toast(status[res.status], {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
type: "error",
|
type: "info",
|
||||||
theme: "colored",
|
theme: "colored",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,15 @@ export default function Start() {
|
||||||
"Raum beitreten"
|
"Raum beitreten"
|
||||||
)}
|
)}
|
||||||
</OptionButton>
|
</OptionButton>
|
||||||
<OptionButton icon={faEye}>
|
<OptionButton
|
||||||
|
icon={faEye}
|
||||||
|
action={() => {
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: { q: "watch" },
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
{query.watch ? (
|
{query.watch ? (
|
||||||
<OtpInput
|
<OtpInput
|
||||||
shouldAutoFocus
|
shouldAutoFocus
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue