import { z } from "zod"; import { lobbies, lobbyMembers, players, type Lobby } from "@/server/db/schema"; import { createTRPCRouter, protectedProcedure, publicProcedure, } from "@/server/api/trpc"; import { LobbyMemberRoleSchema, lobbyPatchSchema, } from "@/lib/validations/lobby"; import { and, eq } from "drizzle-orm"; import { tracked } from "@trpc/server"; import type { EventArgs } from "@/server/sse/events"; import { decreaseLobbyMemberCount, increaseLobbyMemberCount, handleLobbyAfterLeave, } from "@/server/api/mutation-utils/lobby-utils"; import { createGameConfig } from "../mutation-utils/game-config"; import { ee } from "@/server/sse"; export const lobbyRouter = createTRPCRouter({ // queries getCurrentLobby: protectedProcedure.query(async ({ ctx }) => { const reuslt = await ctx.db.query.lobbyMembers.findFirst({ where: eq(lobbyMembers.playerId, ctx.session.user.id), with: { lobby: { with: { gameConfig: true, members: { with: { player: true, }, }, }, }, }, }); return reuslt?.lobby!; }), get: publicProcedure .input( z.object({ id: z.string(), }), ) .query( async ({ ctx, input }) => await ctx.db.query.lobbies.findFirst({ where: eq(lobbies.id, input.id), with: { gameConfig: true, members: { with: { player: true, }, }, }, }), ), // mutations create: protectedProcedure .input( z.object({ lobby: lobbyPatchSchema, }), ) .mutation(async ({ ctx, input }) => { const [lobby] = await ctx.db .insert(lobbies) .values({ ...input.lobby, createdById: ctx.session.user.id, }) .returning({ id: lobbies.id }); if (!lobby) throw new Error("Error creating lobby"); const [member] = await ctx.db .insert(lobbyMembers) .values({ lobbyId: lobby.id, playerId: ctx.session.user.id, isReady: false, role: "admin", }) .returning({ id: lobbyMembers.playerId, }); if (member && lobby) { increaseLobbyMemberCount(lobby.id); createGameConfig(lobby.id, ctx.db); } return lobby; }), update: protectedProcedure .input( z.object({ lobby: lobbyPatchSchema, lobbyId: z.string(), }), ) .mutation(async ({ ctx, input }) => { console.log("Check if user is admin"); const [lobby] = await ctx.db .update(lobbies) .set(input.lobby) .where( and( eq(lobbies.id, input.lobbyId), eq(lobbies.createdById, ctx.session.user.id), ), ) .returning(); if (lobby) ee.emit("lobby:update", lobby); return lobby; }), delete: protectedProcedure .input( z.object({ lobbyId: z.string(), }), ) .mutation(async ({ ctx, input }) => { console.log("Check if user is admin"); const [lobby] = await ctx.db .delete(lobbies) .where( and( eq(lobbies.id, input.lobbyId), eq(lobbies.createdById, ctx.session.user.id), ), ) .returning({ id: lobbies.id }); return lobby; }), membership: protectedProcedure .input( z.object({ lobbyId: z.string(), join: z.boolean(), }), ) .mutation(async ({ ctx, input }) => { if (input.join) { try { const [member] = await ctx.db .insert(lobbyMembers) .values({ lobbyId: input.lobbyId, playerId: ctx.session.user.id, isReady: false, role: "player", }) .returning(); const player = member ? await ctx.db.query.players.findFirst({ where: eq(players.id, member.playerId), }) : undefined; if (member) { ee.emit("lobby:member:membership", input.lobbyId, true, { ...member, player, }); increaseLobbyMemberCount(input.lobbyId); return { success: true, member }; } } catch (e: unknown) { if (e instanceof Error) { if ( e.message.includes( "duplicate key value violates unique constraint", ) ) return { knownError: true, succes: false, error: "You can only be in one lobby at a time.", }; } else { return { knownError: true, succes: false, error: "Error joining lobby", }; } } } else { const [member] = await ctx.db .delete(lobbyMembers) .where( and( eq(lobbyMembers.lobbyId, input.lobbyId), eq(lobbyMembers.playerId, ctx.session.user.id), ), ) .returning(); if (member) { ee.emit( "lobby:member:membership", input.lobbyId, false, member.playerId, ); decreaseLobbyMemberCount(input.lobbyId); handleLobbyAfterLeave(input.lobbyId); return { success: true, member }; } } }), // admin mutaions changeRole: protectedProcedure .input( z.object({ lobbyId: z.string(), playerId: z.string(), role: LobbyMemberRoleSchema, }), ) .mutation(async ({ ctx, input }) => { console.log("Check if user is admin"); const [member] = await ctx.db .update(lobbyMembers) .set(input.role === "admin" ? { role: "admin" } : { role: "player" }) .where( and( eq(lobbyMembers.lobbyId, input.lobbyId), eq(lobbyMembers.playerId, input.playerId), ), ) .returning(); if (member) ee.emit("lobby:member:update", member); return member; }), kick: protectedProcedure .input( z.object({ lobbyId: z.string(), playerId: z.string(), }), ) .mutation(async ({ ctx, input }) => { console.log("Check if user is admin"); const [member] = await ctx.db .delete(lobbyMembers) .where( and( eq(lobbyMembers.lobbyId, input.lobbyId), eq(lobbyMembers.playerId, input.playerId), ), ) .returning(); if (member) ee.emit( "lobby:member:membership", input.lobbyId, false, member.playerId, true, ); decreaseLobbyMemberCount(input.lobbyId); return member; }), // subscriptions onUpdate: publicProcedure .input(z.object({ lobbyId: z.string() })) .subscription(async function* (opts) { const iterable = ee.toIterable("lobby:update", { signal: opts.signal, }); function* maybeYield([lobby, deleted]: EventArgs["lobby:update"]) { if (lobby.id !== opts.input.lobbyId) { return; } yield tracked(lobby.id, { lobby, deleted }); } for await (const args of iterable) { yield* maybeYield(args); } }), onMemberUpdate: publicProcedure .input(z.object({ lobbyId: z.string() })) .subscription(async function* (opts) { const iterable = ee.toIterable("lobby:member:membership", { signal: opts.signal, }); function* maybeYield([ lobbyId, joined, member, kicked, ]: EventArgs["lobby:member:membership"]) { if (lobbyId !== opts.input.lobbyId) { return; } yield tracked(lobbyId, { member, joined, kicked }); } for await (const args of iterable) { yield* maybeYield(args); } }), });