matchmaking works

This commit is contained in:
2025-11-28 13:41:35 +05:30
parent 4d6511505a
commit 9916939f29
2 changed files with 69 additions and 157 deletions

View File

@@ -4,7 +4,6 @@ import Board from "./Board";
export default function TicTacToe() {
const [username, setUsername] = useState("");
const [matchId, setMatchId] = useState("");
const [board, setBoard] = useState<string[][]>([
["", "", ""],
@@ -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);
})
}
// ------------------------------------------

View File

@@ -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<Session>;
loginOrRegister(username: string): Promise<void>;
joinMatchmaker(mode: string): Promise<string>;
joinMatch(matchId: string): Promise<void>;
joinMatchmaker: (mode: string) => Promise<string>;
leaveMatchmaker: (ticket: string) => Promise<void>;
sendMatchData(matchId: string, op: number, data: object): void;
joinMatch: (matchId: string) => Promise<Match>;
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<NakamaContextType | null>(null);
export const NakamaContext = createContext<NakamaContextType>(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<Session | null>(null);
const [socket, setSocket] = useState<Socket | null>(null);
const [matchId, setMatchId] = useState<string | null>(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<Session> {
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<string> {
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<Match> {
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 (
<NakamaContext.Provider
value={{
client,
socket,
session,
socket,
matchId,
loginOrRegister,
joinMatchmaker,
leaveMatchmaker,
joinMatch,
sendMatchData,
onMatchData,
onMatchmakerMatched,
}}
>
{children}