From 9916939f29bbb0c73283ece3d83688afd4bd2a90 Mon Sep 17 00:00:00 2001 From: Vishesh 'ironeagle' Bangotra Date: Fri, 28 Nov 2025 13:41:35 +0530 Subject: [PATCH] matchmaking works --- src/tictactoe/TicTacToe.tsx | 13 +- src/tictactoe/providers/NakamaProvider.tsx | 213 +++++++-------------- 2 files changed, 69 insertions(+), 157 deletions(-) diff --git a/src/tictactoe/TicTacToe.tsx b/src/tictactoe/TicTacToe.tsx index bab7d52..f101958 100644 --- a/src/tictactoe/TicTacToe.tsx +++ b/src/tictactoe/TicTacToe.tsx @@ -4,7 +4,6 @@ import Board from "./Board"; export default function TicTacToe() { const [username, setUsername] = useState(""); - const [matchId, setMatchId] = useState(""); const [board, setBoard] = useState([ ["", "", ""], @@ -17,10 +16,9 @@ export default function TicTacToe() { const { loginOrRegister, joinMatchmaker, - joinMatch, onMatchData, - onMatchmakerMatched, sendMatchData, + matchId, } = useNakama(); // ------------------------------------------ @@ -41,15 +39,6 @@ export default function TicTacToe() { setWinner(state.winner || null); } }); - - onMatchmakerMatched(async (matched) => { - console.log("Matched:", matched); - - const fullMatchId = matched.match_id; - setMatchId(fullMatchId); - - await joinMatch(fullMatchId); - }) } // ------------------------------------------ diff --git a/src/tictactoe/providers/NakamaProvider.tsx b/src/tictactoe/providers/NakamaProvider.tsx index fca4671..eeed378 100644 --- a/src/tictactoe/providers/NakamaProvider.tsx +++ b/src/tictactoe/providers/NakamaProvider.tsx @@ -1,6 +1,5 @@ import { Client, - DefaultSocket, Session, Socket, MatchmakerTicket, @@ -8,215 +7,139 @@ import { MatchData, MatchmakerMatched, } from "@heroiclabs/nakama-js"; +import React, { createContext, useContext, useState } from "react"; -import React, { - createContext, - useContext, - useEffect, - useRef, - useState, -} from "react"; - -// --------------------------------------------- -// UNIQUE DEVICE ID FOR BROWSER -// --------------------------------------------- function getOrCreateDeviceId(): string { const key = "nakama.deviceId"; let id = localStorage.getItem(key); - if (!id) { id = crypto.randomUUID(); localStorage.setItem(key, id); } - return id; } -// --------------------------------------------- -// CONTEXT TYPE -// --------------------------------------------- export interface NakamaContextType { client: Client; socket: Socket | null; session: Session | null; + matchId: string | null; - loginOrRegister: (username: string) => Promise; + loginOrRegister(username: string): Promise; + joinMatchmaker(mode: string): Promise; + joinMatch(matchId: string): Promise; - joinMatchmaker: (mode: string) => Promise; - leaveMatchmaker: (ticket: string) => Promise; + sendMatchData(matchId: string, op: number, data: object): void; - joinMatch: (matchId: string) => Promise; - - sendMatchData: (matchId: string, opCode: number, data: object) => void; - - onMatchData: ( - cb: (msg: { opCode: number; data: any; userId: string | null }) => void - ) => void; - - onMatchmakerMatched: (cb: (result: MatchmakerMatched) => void) => void; + onMatchData(cb: (msg: any) => void): void; } -export const NakamaContext = createContext(null); +export const NakamaContext = createContext(null!); -// --------------------------------------------- -// PROVIDER IMPLEMENTATION -// --------------------------------------------- export function NakamaProvider({ children }: { children: React.ReactNode }) { - // Use "defaultkey", "127.0.0.1", "7350" — match your given client const [client] = useState( - () => - new Client( - "defaultkey", - "127.0.0.1", - "7350", - false, // use SSL? - ) + () => new Client("defaultkey", "127.0.0.1", "7350") ); const [session, setSession] = useState(null); const [socket, setSocket] = useState(null); + const [matchId, setMatchId] = useState(null); - // --------------------------------------------- - // CALLBACK REGISTRIES (React-safe) - // --------------------------------------------- - const matchDataCallbacks = useRef< - ((msg: { opCode: number; data: any; userId: string | null }) => void)[] - >([]); + // ---------------------------------------------------- + // LOGIN + // ---------------------------------------------------- + async function loginOrRegister(username: string) { + const deviceId = getOrCreateDeviceId(); - const matchmakerMatchedCallbacks = useRef< - ((result: MatchmakerMatched) => void)[] - >([]); + // authenticate user + const newSession = await client.authenticateDevice(deviceId, true, username); + setSession(newSession); - // Register match data listener - function onMatchData( - cb: (msg: { opCode: number; data: any; userId: string | null }) => void - ) { - matchDataCallbacks.current.push(cb); - } + // create socket (new Nakama 3.x signature) + const s = client.createSocket(undefined, undefined); // no SSL on localhost + await s.connect(newSession, true); + setSocket(s); - // Register matchmaker matched listener - function onMatchmakerMatched(cb: (result: MatchmakerMatched) => void) { - matchmakerMatchedCallbacks.current.push(cb); - } + console.log("[Nakama] WebSocket connected"); - // --------------------------------------------- - // SETUP SOCKET EVENT LISTENERS WHEN SOCKET IS READY - // --------------------------------------------- - useEffect(() => { - if (!socket) return; + // MATCHMAKER MATCHED CALLBACK + s.onmatchmakermatched = async (matched: MatchmakerMatched) => { + console.log("[Nakama] MATCHED:", matched); - // MATCH DATA - socket.onmatchdata = (msg: MatchData) => { - const raw = msg.data; - let decoded: any; + setMatchId(matched.match_id); try { - decoded = JSON.parse(new TextDecoder().decode(raw)); - } catch { - decoded = raw; + await s.joinMatch(matched.match_id); + console.log("[Nakama] Auto-joined match:", matched.match_id); + } catch (err) { + console.error("[Nakama] Failed to join match:", err); } - - matchDataCallbacks.current.forEach((cb) => - cb({ - opCode: msg.op_code, - data: decoded, - userId: msg.presence?.user_id || null, - }) - ); }; - - // MATCHMAKER MATCHED - socket.onmatchmakermatched = (result) => { - matchmakerMatchedCallbacks.current.forEach((cb) => cb(result)); - }; - - return () => { - // Cleanup (not strictly needed, but clean React practice) - socket.onmatchdata = undefined as any; - socket.onmatchmakermatched = undefined as any; - }; - }, [socket]); - - // --------------------------------------------- - // LOGIN OR REGISTER - // --------------------------------------------- - async function loginOrRegister(username: string): Promise { - try { - const deviceId = getOrCreateDeviceId(); - - // authenticateDevice(id, create, username) - const s = await client.authenticateDevice(deviceId, true, username); - setSession(s); - - const newSocket = client.createSocket(false, false); - await newSocket.connect(s, true); - setSocket(newSocket); - - console.log("[Nakama] Connected via WebSocket"); - return s; - } catch (err) { - console.error("[Nakama] Login error:", err); - throw err; - } } - // --------------------------------------------- + // ---------------------------------------------------- // MATCHMAKING - // --------------------------------------------- - async function joinMatchmaker(mode: string): Promise { - if (!socket) throw new Error("Socket not ready."); + // ---------------------------------------------------- + async function joinMatchmaker(mode: string) { + if (!socket) throw new Error("socket missing"); + console.log(`[Nakama] Matchmaking... with +mode:"${mode}"`); const ticket: MatchmakerTicket = await socket.addMatchmaker( - `+mode:"${mode}"`, // query + `*`, // query 2, // min count 2, // max count { mode } // stringProperties ); - console.log("[Nakama] Ticket:", ticket.ticket); return ticket.ticket; } - async function leaveMatchmaker(ticket: string) { - if (socket) { - await socket.removeMatchmaker(ticket); - } + // ---------------------------------------------------- + // EXPLICIT MATCH JOIN + // ---------------------------------------------------- + async function joinMatch(id: string) { + if (!socket) throw new Error("socket missing"); + await socket.joinMatch(id); + setMatchId(id); + console.log("[Nakama] Joined match", id); } - // --------------------------------------------- - // JOIN MATCH - // --------------------------------------------- - async function joinMatch(matchId: string): Promise { - if (!socket) throw new Error("Socket not connected."); - const match = await socket.joinMatch(matchId); - console.log("[Nakama] Joined match:", matchId); - return match; - } - - // --------------------------------------------- - // SEND MATCH DATA - // --------------------------------------------- - function sendMatchData(matchId: string, opCode: number, data: object) { + // ---------------------------------------------------- + // MATCH STATE SEND + // ---------------------------------------------------- + function sendMatchData(matchId: string, op: number, data: object) { if (!socket) return; - socket.sendMatchState(matchId, opCode, JSON.stringify(data)); + socket.sendMatchState(matchId, op, JSON.stringify(data)); + } + + // ---------------------------------------------------- + // MATCH DATA LISTENER + // ---------------------------------------------------- + function onMatchData(cb: (msg: any) => void) { + if (!socket) return; + + socket.onmatchdata = (m: MatchData) => { + const decoded = JSON.parse(new TextDecoder().decode(m.data)); + cb({ + opCode: m.op_code, + data: decoded, + userId: m.presence?.user_id ?? null, + }); + }; } - // --------------------------------------------- - // PROVIDER VALUE - // --------------------------------------------- return ( {children}