connection works
This commit is contained in:
@@ -19,8 +19,8 @@ export default function TicTacToe() {
|
|||||||
joinMatchmaker,
|
joinMatchmaker,
|
||||||
joinMatch,
|
joinMatch,
|
||||||
onMatchData,
|
onMatchData,
|
||||||
socket,
|
onMatchmakerMatched,
|
||||||
sendMatchData
|
sendMatchData,
|
||||||
} = useNakama();
|
} = useNakama();
|
||||||
|
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
@@ -42,15 +42,14 @@ export default function TicTacToe() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// When matchmaker finds a match
|
onMatchmakerMatched(async (matched) => {
|
||||||
socket!.onmatchmakermatched = async (matched) => {
|
|
||||||
console.log("Matched:", matched);
|
console.log("Matched:", matched);
|
||||||
|
|
||||||
const fullMatchId = matched.match_id;
|
const fullMatchId = matched.match_id;
|
||||||
setMatchId(fullMatchId);
|
setMatchId(fullMatchId);
|
||||||
|
|
||||||
await joinMatch(fullMatchId);
|
await joinMatch(fullMatchId);
|
||||||
};
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
@@ -77,7 +76,7 @@ export default function TicTacToe() {
|
|||||||
{!matchId && (
|
{!matchId && (
|
||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
placeholder="username/device ID"
|
placeholder="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,90 +6,186 @@ import {
|
|||||||
MatchmakerTicket,
|
MatchmakerTicket,
|
||||||
Match,
|
Match,
|
||||||
MatchData,
|
MatchData,
|
||||||
|
MatchmakerMatched,
|
||||||
} from "@heroiclabs/nakama-js";
|
} 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 {
|
export interface NakamaContextType {
|
||||||
client: Client;
|
client: Client;
|
||||||
socket: Socket | null;
|
socket: Socket | null;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
|
|
||||||
loginOrRegister: (username: string) => Promise<Session>;
|
loginOrRegister: (username: string) => Promise<Session>;
|
||||||
|
|
||||||
joinMatchmaker: (mode: string) => Promise<string>;
|
joinMatchmaker: (mode: string) => Promise<string>;
|
||||||
leaveMatchmaker: (ticket: string) => Promise<void>;
|
leaveMatchmaker: (ticket: string) => Promise<void>;
|
||||||
|
|
||||||
joinMatch: (matchId: string) => Promise<Match>;
|
joinMatch: (matchId: string) => Promise<Match>;
|
||||||
|
|
||||||
sendMatchData: (matchId: string, opCode: number, data: object) => void;
|
sendMatchData: (matchId: string, opCode: number, data: object) => void;
|
||||||
|
|
||||||
onMatchData: (
|
onMatchData: (
|
||||||
cb: (msg: { opCode: number; data: any; userId: string | null }) => void
|
cb: (msg: { opCode: number; data: any; userId: string | null }) => void
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
onMatchmakerMatched: (cb: (result: MatchmakerMatched) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NakamaContext = createContext<NakamaContextType | null>(null);
|
export const NakamaContext = createContext<NakamaContextType | null>(null);
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
// PROVIDER IMPLEMENTATION
|
||||||
|
// ---------------------------------------------
|
||||||
export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [client] = useState<Client>(
|
// Use "defaultkey", "127.0.0.1", "7350" — match your given client
|
||||||
() => new Client(
|
const [client] = useState(
|
||||||
"defaultkey",
|
() =>
|
||||||
"127.0.0.1",
|
new Client(
|
||||||
"7350"
|
"defaultkey",
|
||||||
)
|
"127.0.0.1",
|
||||||
|
"7350",
|
||||||
|
false, // use SSL?
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
|
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
// LOGIN / REGISTER
|
// CALLBACK REGISTRIES (React-safe)
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
|
const matchDataCallbacks = useRef<
|
||||||
|
((msg: { opCode: number; data: any; userId: string | null }) => void)[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const matchmakerMatchedCallbacks = useRef<
|
||||||
|
((result: MatchmakerMatched) => void)[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
// Register match data listener
|
||||||
|
function onMatchData(
|
||||||
|
cb: (msg: { opCode: number; data: any; userId: string | null }) => void
|
||||||
|
) {
|
||||||
|
matchDataCallbacks.current.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register matchmaker matched listener
|
||||||
|
function onMatchmakerMatched(cb: (result: MatchmakerMatched) => void) {
|
||||||
|
matchmakerMatchedCallbacks.current.push(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
// SETUP SOCKET EVENT LISTENERS WHEN SOCKET IS READY
|
||||||
|
// ---------------------------------------------
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket) return;
|
||||||
|
|
||||||
|
// MATCH DATA
|
||||||
|
socket.onmatchdata = (msg: MatchData) => {
|
||||||
|
const raw = msg.data;
|
||||||
|
let decoded: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
decoded = JSON.parse(new TextDecoder().decode(raw));
|
||||||
|
} catch {
|
||||||
|
decoded = raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
async function loginOrRegister(username: string): Promise<Session> {
|
||||||
try {
|
try {
|
||||||
const s = await client.authenticateDevice(username, true);
|
const deviceId = getOrCreateDeviceId();
|
||||||
|
|
||||||
|
// authenticateDevice(id, create, username)
|
||||||
|
const s = await client.authenticateDevice(deviceId, true, username);
|
||||||
setSession(s);
|
setSession(s);
|
||||||
|
|
||||||
const newSocket = new DefaultSocket(
|
const newSocket = client.createSocket(false, false);
|
||||||
client.host,
|
|
||||||
client.port,
|
|
||||||
false, // useSSL
|
|
||||||
false // verbose
|
|
||||||
);
|
|
||||||
|
|
||||||
await newSocket.connect(s, true);
|
await newSocket.connect(s, true);
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
|
|
||||||
console.log("[Nakama] Connected WS");
|
console.log("[Nakama] Connected via WebSocket");
|
||||||
return s;
|
return s;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[Nakama] Login error", err);
|
console.error("[Nakama] Login error:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
// MATCHMAKING
|
// MATCHMAKING
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
async function joinMatchmaker(mode: string): Promise<string> {
|
async function joinMatchmaker(mode: string): Promise<string> {
|
||||||
if (!socket) throw new Error("Socket not ready.");
|
if (!socket) throw new Error("Socket not ready.");
|
||||||
|
|
||||||
const ticket = await socket.addMatchmaker(
|
const ticket: MatchmakerTicket = await socket.addMatchmaker(
|
||||||
`+mode:"${mode}"`, // query
|
`+mode:"${mode}"`, // query
|
||||||
2, // min players
|
2, // min count
|
||||||
2, // max players
|
2, // max count
|
||||||
{ mode } // string properties
|
{ mode } // stringProperties
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[Nakama] Matchmaker ticket:", ticket.ticket);
|
console.log("[Nakama] Ticket:", ticket.ticket);
|
||||||
return ticket.ticket;
|
return ticket.ticket;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function leaveMatchmaker(ticket: string): Promise<void> {
|
async function leaveMatchmaker(ticket: string) {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
await socket.removeMatchmaker(ticket);
|
await socket.removeMatchmaker(ticket);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
// JOIN MATCH
|
// JOIN MATCH
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
async function joinMatch(matchId: string): Promise<Match> {
|
async function joinMatch(matchId: string): Promise<Match> {
|
||||||
if (!socket) throw new Error("Socket not connected.");
|
if (!socket) throw new Error("Socket not connected.");
|
||||||
const match = await socket.joinMatch(matchId);
|
const match = await socket.joinMatch(matchId);
|
||||||
@@ -97,41 +193,17 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
// SEND MATCH DATA
|
// SEND MATCH DATA
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
function sendMatchData(matchId: string, opCode: number, data: object = {}): void {
|
function sendMatchData(matchId: string, opCode: number, data: object) {
|
||||||
if (!socket) return;
|
if (!socket) return;
|
||||||
socket.sendMatchState(matchId, opCode, JSON.stringify(data));
|
socket.sendMatchState(matchId, opCode, JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
// LISTENERS
|
// PROVIDER VALUE
|
||||||
// -----------------------
|
// ---------------------------------------------
|
||||||
function onMatchData(
|
|
||||||
cb: (msg: { opCode: number; data: any; userId: string | null }) => void
|
|
||||||
) {
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
socket.onmatchdata = (msg: MatchData) => {
|
|
||||||
const raw = msg.data;
|
|
||||||
let decoded: any = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
decoded = JSON.parse(new TextDecoder().decode(raw));
|
|
||||||
} catch {
|
|
||||||
decoded = raw; // fallback to raw binary
|
|
||||||
}
|
|
||||||
|
|
||||||
cb({
|
|
||||||
opCode: msg.op_code,
|
|
||||||
data: decoded,
|
|
||||||
userId: msg.presence?.user_id ?? null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NakamaContext.Provider
|
<NakamaContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -144,6 +216,7 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
joinMatch,
|
joinMatch,
|
||||||
sendMatchData,
|
sendMatchData,
|
||||||
onMatchData,
|
onMatchData,
|
||||||
|
onMatchmakerMatched,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -151,10 +224,11 @@ export function NakamaProvider({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------
|
||||||
|
// USE HOOK
|
||||||
|
// ---------------------------------------------
|
||||||
export function useNakama(): NakamaContextType {
|
export function useNakama(): NakamaContextType {
|
||||||
const ctx = useContext(NakamaContext);
|
const ctx = useContext(NakamaContext);
|
||||||
if (!ctx) {
|
if (!ctx) throw new Error("useNakama must be inside a NakamaProvider");
|
||||||
throw new Error("useNakama must be used inside a NakamaProvider");
|
|
||||||
}
|
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user