matchmaking works
This commit is contained in:
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------------------
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user