diff --git a/leaky-ships/components/Lobby/LobbyFrame.tsx b/leaky-ships/components/Lobby/LobbyFrame.tsx
index b5e651d..cfe7147 100644
--- a/leaky-ships/components/Lobby/LobbyFrame.tsx
+++ b/leaky-ships/components/Lobby/LobbyFrame.tsx
@@ -4,14 +4,22 @@ import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useGameProps } from "@hooks/useGameProps"
import useSocket from "@hooks/useSocket"
+import { socket } from "@lib/socket"
+import { useSession } from "next-auth/react"
import { useRouter } from "next/router"
import { Fragment, useEffect, useState } from "react"
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
- const { payload } = useGameProps()
+ const { payload, full, leave, reset } = useGameProps()
const [dots, setDots] = useState(3)
const { isConnected } = useSocket()
const router = useRouter()
+ const { data: session } = useSession()
+
+ useEffect(() => {
+ if (payload?.game?.id || !isConnected) return
+ socket.emit("update", full)
+ }, [full, payload?.game?.id, isConnected])
useEffect(() => {
if (payload?.player2) return
@@ -38,15 +46,15 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
VS
{payload?.player2 ? (
) : (
@@ -60,9 +68,17 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
+
diff --git a/leaky-ships/components/Lobby/Player.tsx b/leaky-ships/components/Lobby/Player.tsx
index 6cef80b..3e9da40 100644
--- a/leaky-ships/components/Lobby/Player.tsx
+++ b/leaky-ships/components/Lobby/Player.tsx
@@ -1,18 +1,21 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faCaretDown } from "@fortawesome/sharp-solid-svg-icons"
+import { PlayerSchema } from "@lib/zodSchemas"
import classNames from "classnames"
function Player({
src,
- text,
- primary,
- edit,
+ player,
+ userId,
}: {
src: string
- text: string
- primary?: boolean
- edit?: boolean
+ player?: PlayerSchema
+ userId?: string
}) {
+ const text =
+ player?.name ?? "Spieler " + (player?.index === "player2" ? "2" : "1")
+ const primary = userId === player?.id
+
return (

- {edit ? (
+ {primary ? (
diff --git a/leaky-ships/hooks/useGameProps.ts b/leaky-ships/hooks/useGameProps.ts
index 68d067a..d64a1ea 100644
--- a/leaky-ships/hooks/useGameProps.ts
+++ b/leaky-ships/hooks/useGameProps.ts
@@ -1,16 +1,26 @@
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 { toast } from "react-toastify"
import { create } from "zustand"
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 = {
- setSetting: (by: GameSettings) => void
+export type Action = {
+ setSetting: (settings: GameSettings) => string | null
+ setPlayer: (payload: {
+ player1?: PlayerSchema
+ player2?: PlayerSchema
+ }) => string | null
full: (newProps: GamePropsSchema) => void
+ leave: (cb: () => void) => void
reset: () => void
}
@@ -18,13 +28,52 @@ export const useGameProps = create()(
devtools(
(set) => ({
...initialState,
- setSetting: (settings) =>
+ setPlayer: (payload) => {
+ let hash: string | null = null
set(
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
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) =>
set((state) => {
if (state.hash === newGameProps.hash) {
@@ -47,6 +96,14 @@ export const useGameProps = create()(
}
return state
}),
+ leave: (cb) => {
+ socket.emit("leave", (ack) => {
+ if (!ack) {
+ toast.error("Something is wrong...")
+ }
+ cb()
+ })
+ },
reset: () => {
set(initialState)
},
diff --git a/leaky-ships/hooks/useSocket.ts b/leaky-ships/hooks/useSocket.ts
index 5ee003b..ef7bc42 100644
--- a/leaky-ships/hooks/useSocket.ts
+++ b/leaky-ships/hooks/useSocket.ts
@@ -1,28 +1,34 @@
-import { useGameProps } from "@hooks/useGameProps"
+import { useGameProps } from "./useGameProps"
import { socket } from "@lib/socket"
+import status from "http-status"
+import { useRouter } from "next/router"
import { useEffect, useState } from "react"
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. */
function useSocket() {
const [isConnected, setIsConnected] = useState(false)
- const { full } = useGameProps()
-
- useEffect(() => setIsConnected(socket.connected), [socket.connected])
+ const { setPlayer, setSetting, full, hash: stateHash } = useGameProps()
+ const router = useRouter()
useEffect(() => {
socket.connect()
socket.on("connect", () => {
+ setIsConnected(true)
console.log("connected")
const toastId = "connect_error"
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 isActive = toast.isActive(toastId)
+ console.log(toastId, isActive)
if (isActive)
toast.update(toastId, {
autoClose: 5000,
@@ -31,13 +37,51 @@ function useSocket() {
toast.warn("Es gibt Probleme mit der Echtzeitverbindung.", { toastId })
})
- socket.on("disconnect", () => {
- console.log("disconnect")
+ socket.on("gameSetting", (payload, hash) => {
+ 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) => {
- console.log("update")
- full(body)
+ socket.on("playerEvent", (payload, hash, type) => {
+ let message: string
+ 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 () => {
@@ -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(() => {
// if (!isConnected) return
// let count = 0
diff --git a/leaky-ships/interfaces/NextApiSocket.ts b/leaky-ships/interfaces/NextApiSocket.ts
index cbef69f..12467ec 100644
--- a/leaky-ships/interfaces/NextApiSocket.ts
+++ b/leaky-ships/interfaces/NextApiSocket.ts
@@ -1,11 +1,15 @@
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
-import { GamePropsSchema } from "@lib/zodSchemas"
+import { GamePropsSchema, PlayerSchema } from "@lib/zodSchemas"
import { User } from "@prisma/client"
import type { Server as HTTPServer } from "http"
import type { Socket as NetSocket } from "net"
import type { NextApiResponse } from "next"
-import type { Server as IOServer, Server } from "socket.io"
-import { Socket } from "socket.io-client"
+import type {
+ Server as IOServer,
+ Server,
+ Socket as SocketforServer,
+} from "socket.io"
+import type { Socket as SocketforClient } from "socket.io-client"
interface SocketServer extends HTTPServer {
io?: IOServer
@@ -23,16 +27,20 @@ export interface ServerToClientEvents {
// noArg: () => void
// basicEmit: (a: number, b: string, c: Buffer) => 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 {
+ update: (callback: (game: GamePropsSchema) => void) => void
ping: (count: number, callback: (count: number) => void) => void
- join: (withAck: ({ ack }: { ack: boolean }) => void) => void
- gameSetting: (
- payload: GameSettings,
- withAck: ({ ack }: { ack: boolean }) => void
- ) => void
+ join: (withAck: (ack: boolean) => void) => void
+ gameSetting: (payload: GameSettings, callback: (hash: string) => void) => void
+ leave: (withAck: (ack: boolean) => void) => void
}
interface InterServerEvents {
@@ -44,11 +52,20 @@ interface SocketData {
gameId: string | null
}
-export type cServer = Server<
+export type sServer = Server<
+ ClientToServerEvents,
+ ServerToClientEvents,
+ InterServerEvents,
+ SocketData
+>
+export type sSocket = SocketforServer<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
>
-export type cSocket = Socket
+export type cSocket = SocketforClient<
+ ServerToClientEvents,
+ ClientToServerEvents
+>
diff --git a/leaky-ships/lib/getObjectChecksum.ts b/leaky-ships/lib/getPayloadwithChecksum.ts
similarity index 100%
rename from leaky-ships/lib/getObjectChecksum.ts
rename to leaky-ships/lib/getPayloadwithChecksum.ts
diff --git a/leaky-ships/lib/zodSchemas.ts b/leaky-ships/lib/zodSchemas.ts
index 0b10588..7063b29 100644
--- a/leaky-ships/lib/zodSchemas.ts
+++ b/leaky-ships/lib/zodSchemas.ts
@@ -1,25 +1,29 @@
import { GameState, PlayerN } from "@prisma/client"
import { z } from "zod"
-export const PlayerSchema = z.object({
- id: z.string(),
- name: z.string().nullable(),
- index: z.nativeEnum(PlayerN),
- chats: z
- .object({
- id: z.string(),
- event: z.string().nullable(),
- message: z.string().nullable(),
- createdAt: z.date(),
- })
- .array(),
- moves: z
- .object({
- id: z.string(),
- index: z.number(),
- })
- .array(),
-})
+export const PlayerSchema = z
+ .object({
+ id: z.string(),
+ name: z.string().nullable(),
+ index: z.nativeEnum(PlayerN),
+ chats: z
+ .object({
+ id: z.string(),
+ event: z.string().nullable(),
+ message: z.string().nullable(),
+ createdAt: z.date(),
+ })
+ .array(),
+ moves: z
+ .object({
+ id: z.string(),
+ index: z.number(),
+ })
+ .array(),
+ })
+ .nullable()
+
+export type PlayerSchema = z.infer
export const CreateSchema = z
.object({
@@ -34,13 +38,13 @@ export const CreateSchema = z
})
.nullable(),
gamePin: z.string().nullable(),
- player1: PlayerSchema.nullable(),
- player2: PlayerSchema.nullable(),
+ player1: PlayerSchema,
+ player2: PlayerSchema,
})
- .strict()
+ .nullable()
export const GamePropsSchema = z.object({
- payload: CreateSchema.nullable(),
+ payload: CreateSchema,
hash: z.string().nullable(),
})
diff --git a/leaky-ships/pages/api/game/join.ts b/leaky-ships/pages/api/game/join.ts
index fc941d4..f6ca22a 100644
--- a/leaky-ships/pages/api/game/join.ts
+++ b/leaky-ships/pages/api/game/join.ts
@@ -1,5 +1,5 @@
import { authOptions } from "../auth/[...nextauth]"
-import { composeBody, gameSelects } from "./running"
+import running, { composeBody, gameSelects } from "./running"
import sendError from "@backend/sendError"
import sendResponse from "@backend/sendResponse"
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({
data: {
gameId: game.id,
diff --git a/leaky-ships/pages/api/game/running.ts b/leaky-ships/pages/api/game/running.ts
index e4ddbca..85ac7ac 100644
--- a/leaky-ships/pages/api/game/running.ts
+++ b/leaky-ships/pages/api/game/running.ts
@@ -1,7 +1,7 @@
import { authOptions } from "../auth/[...nextauth]"
import sendResponse from "@backend/sendResponse"
import { rejectionErrors } from "@lib/backend/errors"
-import { getPayloadwithChecksum } from "@lib/getObjectChecksum"
+import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
import prisma from "@lib/prisma"
import { GamePropsSchema } from "@lib/zodSchemas"
import type { NextApiRequest, NextApiResponse } from "next"
@@ -40,6 +40,7 @@ export const gameSelects = {
},
user: {
select: {
+ id: true,
name: true,
},
},
diff --git a/leaky-ships/pages/api/ws.ts b/leaky-ships/pages/api/ws.ts
index c757b63..946fa99 100644
--- a/leaky-ships/pages/api/ws.ts
+++ b/leaky-ships/pages/api/ws.ts
@@ -1,6 +1,7 @@
import {
NextApiResponseWithSocket,
- cServer,
+ sServer,
+ sSocket,
} from "../../interfaces/NextApiSocket"
import {
composeBody,
@@ -10,7 +11,9 @@ import {
} from "./game/running"
import logging from "@lib/backend/logging"
import prisma from "@lib/prisma"
+import { GamePropsSchema } from "@lib/zodSchemas"
import colors from "colors"
+import status from "http-status"
import { NextApiRequest } from "next"
import { getSession } from "next-auth/react"
import { Server } from "socket.io"
@@ -25,7 +28,7 @@ const SocketHandler = async (
logging("Socket is already running " + req.url, ["infoCyan"], req)
} else {
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",
cors: {
origin: "https://leaky-ships.mal-noh.de",
@@ -54,14 +57,15 @@ const SocketHandler = async (
["debug"],
socket.request
)
+ next(new Error(status["403"]))
return
}
socket.data.gameId = game.id
socket.join(game.id)
next()
} catch (err) {
- logging("Unauthorized", ["warn"], socket.request)
- next(new Error("Unauthorized"))
+ logging(status["401"], ["warn"], socket.request)
+ next(new Error(status["401"]))
}
})
@@ -73,37 +77,116 @@ const SocketHandler = async (
["infoGreen"],
socket.request
)
- const game = await getAnyGame(socket.data.gameId ?? "")
- if (!socket.data.user || !socket.data.gameId || !game)
- return socket.disconnect()
- const body = composeBody(game)
- io.to(socket.data.gameId).emit("update", body)
+ join(socket, io)
+
+ socket.on("update", async (cb) => {
+ const game = await getAnyGame(socket.data.gameId ?? "")
+ if (!game) return
+ const body = composeBody(game)
+ cb(body)
+ })
socket.on("gameSetting", async (payload, cb) => {
- if (!socket.data.gameId) {
- cb({ ack: false })
- return
- }
const game = await prisma.game.update({
- where: { id: socket.data.gameId },
+ where: { id: socket.data.gameId ?? "" },
data: payload,
...gameSelects,
})
- const body = composeBody(game)
- cb({ ack: true })
- io.to(game.id).emit("update", body)
+ const { hash } = composeBody(game)
+ if (!hash) return
+ cb(hash)
+ io.to(game.id).emit("gameSetting", payload, hash)
})
socket.on("ping", (count, callback) => {
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(
"Disconnecting: " + JSON.stringify(Array.from(socket.rooms)),
["debug"],
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", () => {
@@ -115,4 +198,17 @@ const SocketHandler = async (
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
diff --git a/leaky-ships/pages/start.tsx b/leaky-ships/pages/start.tsx
index 45dcb26..cc1701a 100644
--- a/leaky-ships/pages/start.tsx
+++ b/leaky-ships/pages/start.tsx
@@ -27,7 +27,7 @@ export function isAuthenticated(res: Response) {
toast(status[res.status], {
position: "top-center",
- type: "error",
+ type: "info",
theme: "colored",
})
}
@@ -184,7 +184,15 @@ export default function Start() {
"Raum beitreten"
)}
-
+ {
+ router.push({
+ pathname: router.pathname,
+ query: { q: "watch" },
+ })
+ }}
+ >
{query.watch ? (