Skip to content

Commit

Permalink
Upgrade to new sdk (#53)
Browse files Browse the repository at this point in the history
* upgrade to new sdk

* fixes

* fixes after testing

---------

Co-authored-by: Justin <[email protected]>
  • Loading branch information
hpx7 and jchu231 authored Nov 17, 2023
1 parent 228f899 commit 328190e
Show file tree
Hide file tree
Showing 10 changed files with 975 additions and 166 deletions.
417 changes: 381 additions & 36 deletions client/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"dependencies": {
"@hathora/client-sdk": "^1.0.0",
"@hathora/hathora-cloud-sdk": "^0.0.3",
"@hathora/cloud-sdk-typescript": "^2.2.0",
"@heroicons/react": "^2.0.17",
"@react-oauth/google": "^0.10.0",
"dayjs": "^1.11.7",
Expand Down
71 changes: 38 additions & 33 deletions client/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ReactDOM from "react-dom/client";
import React, { useEffect, useState } from "react";
import { GoogleOAuthProvider } from "@react-oauth/google";
import { AuthV1Api, LobbyV2Api, RoomV1Api } from "@hathora/hathora-cloud-sdk";
import { HathoraCloud } from "@hathora/cloud-sdk-typescript";
import { HathoraConnection } from "@hathora/client-sdk";

import { SessionMetadata, LobbyState, InitialConfig } from "../../common/types";
import { SessionMetadata, RoomConfig } from "../../common/types";

import { isReadyForConnect, Token } from "./utils";
import { Socials } from "./components/website/Socials";
Expand All @@ -18,14 +18,12 @@ import { LobbySelector } from "./components/lobby/LobbySelector";
import { BulletButton } from "./components/lobby/BulletButton";
import { GameComponent, GameConfig } from "./components/GameComponent";

const authClient = new AuthV1Api();
const lobbyClient = new LobbyV2Api();
const roomClient = new RoomV1Api();
const appId = process.env.HATHORA_APP_ID;
const hathoraSdk = new HathoraCloud({ appId });

function App() {
const appId = process.env.HATHORA_APP_ID;
const [googleIdToken, setGoogleIdToken] = useState<string | undefined>();
const token = useAuthToken(appId, googleIdToken);
const token = useAuthToken(googleIdToken);
const [connection, setConnection] = useState<HathoraConnection | undefined>();
const [sessionMetadata, setSessionMetadata] = useState<SessionMetadata | undefined>(undefined);
const [failedToConnect, setFailedToConnect] = useState(false);
Expand All @@ -50,33 +48,34 @@ function App() {
!sessionMetadata?.isGameEnd
) {
// Once we parse roomId from the URL, get connection details to connect player to the server
isReadyForConnect(appId, roomClient, lobbyClient, roomIdFromUrl)
isReadyForConnect(appId, roomIdFromUrl, hathoraSdk)
.then(async ({ connectionInfo, lobbyInfo }) => {
setRoomIdNotFound(undefined);
if (connection != null) {
connection.disconnect(1000);
}

try {
const lobbyState = lobbyInfo.state as LobbyState | undefined;
const lobbyInitialConfig = lobbyInfo.initialConfig as InitialConfig | undefined;
const roomConfig = JSON.parse(lobbyInfo.roomConfig) as RoomConfig;

if (!lobbyState || !lobbyState.isGameEnd) {
if (!roomConfig.isGameEnd) {
const connect = new HathoraConnection(roomIdFromUrl, connectionInfo);
connect.onClose(async () => {
// If game has ended, we want updated lobby state
const updatedLobbyInfo = await lobbyClient.getLobbyInfo(appId, roomIdFromUrl);
const updatedLobbyState = updatedLobbyInfo.state as LobbyState | undefined;
const updatedLobbyInitialConfig = updatedLobbyInfo.initialConfig as InitialConfig | undefined;
const { lobbyV3: updatedLobbyInfo } = await hathoraSdk.lobbyV3.getLobbyInfoByRoomId(roomIdFromUrl);
if (updatedLobbyInfo == null) {
return;
}
const updatedRoomConfig = JSON.parse(updatedLobbyInfo.roomConfig) as RoomConfig;
setSessionMetadata({
serverUrl: `${connectionInfo.host}:${connectionInfo.port}`,
region: updatedLobbyInfo.region,
roomId: updatedLobbyInfo.roomId,
capacity: updatedLobbyInitialConfig?.capacity ?? 0,
winningScore: updatedLobbyInitialConfig?.winningScore ?? 99,
isGameEnd: !!updatedLobbyState?.isGameEnd,
winningPlayerId: updatedLobbyState?.winningPlayerId,
playerNicknameMap: updatedLobbyState?.playerNicknameMap || {},
capacity: updatedRoomConfig.capacity,
winningScore: updatedRoomConfig.winningScore,
isGameEnd: !!updatedRoomConfig.isGameEnd,
winningPlayerId: updatedRoomConfig.winningPlayerId,
playerNicknameMap: updatedRoomConfig.playerNicknameMap,
creatorId: updatedLobbyInfo.createdBy,
});
setFailedToConnect(true);
Expand All @@ -87,11 +86,11 @@ function App() {
serverUrl: `${connectionInfo.host}:${connectionInfo.port}`,
region: lobbyInfo.region,
roomId: lobbyInfo.roomId,
capacity: lobbyInitialConfig?.capacity ?? 0,
winningScore: lobbyInitialConfig?.winningScore ?? 99,
isGameEnd: lobbyState?.isGameEnd ?? false,
winningPlayerId: lobbyState?.winningPlayerId,
playerNicknameMap: lobbyState?.playerNicknameMap || {},
capacity: roomConfig.capacity,
winningScore: roomConfig.winningScore,
isGameEnd: roomConfig.isGameEnd,
winningPlayerId: roomConfig.winningPlayerId,
playerNicknameMap: roomConfig.playerNicknameMap,
creatorId: lobbyInfo.createdBy,
});
} catch (e) {
Expand Down Expand Up @@ -202,20 +201,20 @@ const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(<App />);

// Custom hook to access auth token
function useAuthToken(appId: string | undefined, googleIdToken: string | undefined): Token | undefined {
function useAuthToken(googleIdToken: string | undefined): Token | undefined {
const [token, setToken] = React.useState<Token | undefined>();
useEffect(() => {
if (appId != null) {
getToken(appId, authClient, googleIdToken).then(setToken);
getToken(googleIdToken).then(setToken);
}
}, [appId, googleIdToken]);
}, [googleIdToken]);
return token;
}

// 1. Check sessionStorage for existing token
// 2. If googleIdToken passed, use it for auth and store token
// 3. If none above, then use anonymous auth
async function getToken(appId: string, client: AuthV1Api, googleIdToken: string | undefined): Promise<Token> {
async function getToken(googleIdToken: string | undefined): Promise<Token> {
const maybeToken = sessionStorage.getItem("bullet-mania-token");
const maybeTokenType = sessionStorage.getItem("bullet-mania-token-type");
if (maybeToken !== null && maybeTokenType != null) {
Expand All @@ -225,13 +224,19 @@ async function getToken(appId: string, client: AuthV1Api, googleIdToken: string
} as Token;
}
if (googleIdToken == null) {
const { token } = await client.loginAnonymous(appId);
return { value: token, type: "anonymous" };
const { loginResponse } = await hathoraSdk.authV1.loginAnonymous();
if (loginResponse == null) {
throw new Error("Failed to login anonymously");
}
return { value: loginResponse.token, type: "anonymous" };
}
const { loginResponse } = await hathoraSdk.authV1.loginGoogle({ idToken: googleIdToken });
if (loginResponse == null) {
throw new Error("Failed to login with google");
}
const { token } = await client.loginGoogle(appId, { idToken: googleIdToken });
sessionStorage.setItem("bullet-mania-token", token);
sessionStorage.setItem("bullet-mania-token", loginResponse.token);
sessionStorage.setItem("bullet-mania-token-type", "google");
return { value: token, type: "google" };
return { value: loginResponse.token, type: "google" };
}

function getRoomIdFromUrl(): string | undefined {
Expand Down
39 changes: 26 additions & 13 deletions client/src/components/lobby/GameCreator.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import React from "react";
import { GoogleLogin } from "@react-oauth/google";
import { LobbyV2Api, RoomV1Api, Region } from "@hathora/hathora-cloud-sdk";
import { LobbyVisibility, Region } from "@hathora/cloud-sdk-typescript/dist/sdk/models/shared";
import { HathoraCloud } from "@hathora/cloud-sdk-typescript";

import { isReadyForConnect, Token } from "../../utils";
import { InitialConfig } from "../../../../common/types";
import { RoomConfig } from "../../../../common/types";

import { MultiSelect } from "./MultiSelect";
import { LobbyPageCard } from "./LobbyPageCard";
import { Header } from "./Header";
import { Dropdown } from "./Dropdown";
import { BulletButton } from "./BulletButton";

const lobbyClient = new LobbyV2Api();
const roomClient = new RoomV1Api();

interface GameCreatorProps {
appId: string;
playerToken: Token;
Expand All @@ -28,7 +26,8 @@ export function GameCreator(props: GameCreatorProps) {
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [error, setError] = React.useState<string>("");

const initialConfig: InitialConfig = { capacity, winningScore };
const hathoraSdk = new HathoraCloud({ appId });

return (
<LobbyPageCard className={"pb-1.5"}>
<Header className="mt-4 mb-2">Create Game</Header>
Expand Down Expand Up @@ -78,14 +77,28 @@ export function GameCreator(props: GameCreatorProps) {
}
setIsLoading(true);
try {
const lobby = await lobbyClient.createLobby(appId, playerToken.value, {
visibility,
region,
initialConfig,
});
const roomConfig: RoomConfig = {
capacity,
winningScore,
playerNicknameMap: {},
isGameEnd: false,
};
const { lobbyV3 } = await hathoraSdk.lobbyV3.createLobby(
{
createLobbyV3Params: {
region,
visibility: visibility as LobbyVisibility,
roomConfig: JSON.stringify(roomConfig),
},
},
{ playerAuth: playerToken.value }
);
if (lobbyV3 == null) {
throw new Error("Failed to create lobby");
}
// Wait until lobby connection details are ready before redirect player to match
await isReadyForConnect(appId, roomClient, lobbyClient, lobby.roomId);
window.location.href = `/${lobby.roomId}`; //update url
await isReadyForConnect(appId, lobbyV3.roomId, hathoraSdk);
window.location.href = `/${lobbyV3.roomId}`; //update url
} catch (e) {
setError(e instanceof Error ? e.toString() : typeof e === "string" ? e : "Unknown error");
} finally {
Expand Down
55 changes: 28 additions & 27 deletions client/src/components/lobby/PublicLobbyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import dayjs from "dayjs";
dayjs.extend(relativeTime);

import { ClockIcon, TrophyIcon, UserIcon, UsersIcon } from "@heroicons/react/24/outline";
import { LobbyV2Api, RoomV1Api, Region, Lobby } from "@hathora/hathora-cloud-sdk";
import { LobbyV3, Region } from "@hathora/cloud-sdk-typescript/dist/sdk/models/shared";
import { HathoraCloud } from "@hathora/cloud-sdk-typescript";

import { isReadyForConnect } from "../../utils";
import { InitialConfig, LobbyState } from "../../../../common/types";
import { RoomConfig } from "../../../../common/types";

import { LobbyPageCard } from "./LobbyPageCard";
import { Header } from "./Header";
import { BulletButton } from "./BulletButton";

const lobbyClient = new LobbyV2Api();
const roomClient = new RoomV1Api();
const hathoraSdk = new HathoraCloud({ appId: process.env.HATHORA_APP_ID });

interface PublicLobbyListProps {
appId: string;
Expand All @@ -25,15 +25,17 @@ export function PublicLobbyList(props: PublicLobbyListProps) {
const { appId } = props;
const lobbies = useLobbies(appId);
const [readyRooms, setReadyRooms] = React.useState<Set<string>>(new Set());

useEffect(() => {
lobbies.forEach(async (l) => {
// Ensure that lobby is ready for connections before adding to visible lobby list
await isReadyForConnect(appId, roomClient, lobbyClient, l.roomId);
await isReadyForConnect(appId, l.roomId, hathoraSdk);
setReadyRooms((prev) => {
return new Set([...prev, l.roomId]);
});
});
}, [lobbies]);
}, [lobbies, appId]);

return (
<LobbyPageCard className={""}>
<Header className="mt-4 mb-2">Join Public Game</Header>
Expand All @@ -57,8 +59,7 @@ export function PublicLobbyList(props: PublicLobbyListProps) {
.filter((l) => readyRooms.has(l.roomId))
.sort((a, b) => (new Date(b.createdAt).getTime() || 0) - (new Date(a.createdAt).getTime() || 0))
.map((lobby, index) => {
const lobbyState = lobby.state as LobbyState | undefined;
const lobbyInitialConfig = lobby.initialConfig as InitialConfig | undefined;
const roomConfig = JSON.parse(lobby.roomConfig) as RoomConfig;
return (
<tr
key={`lobby_${lobby.createdBy}_${lobby.createdAt}`}
Expand All @@ -76,9 +77,7 @@ export function PublicLobbyList(props: PublicLobbyListProps) {
>
<div className={"flex items-center justify-center gap-1"}>
<UsersIcon className="h-4 w-4 text-secondary-700" />
{`${lobbyState?.playerNicknameMap ? Object.keys(lobbyState.playerNicknameMap).length : 0}/${
lobbyInitialConfig?.capacity ?? "n/a"
}`}
{`${Object.keys(roomConfig.playerNicknameMap).length}/${roomConfig.capacity}`}
</div>
</td>
<td
Expand All @@ -98,40 +97,34 @@ export function PublicLobbyList(props: PublicLobbyListProps) {
</div>
<div className={"flex items-center"}>
<UserIcon className="h-4 w-4 text-secondary-700 text-xxs" />
{lobbyState && lobbyState.playerNicknameMap[lobby.createdBy] ? (
`${lobbyState.playerNicknameMap[lobby.createdBy]}`
{roomConfig.playerNicknameMap[lobby.createdBy] ? (
`${roomConfig.playerNicknameMap[lobby.createdBy]}`
) : (
<span className="italic">creator left</span>
)}
</div>
<div className={"flex items-center gap-1 text-xxs"}>
<TrophyIcon className="h-4 w-4 text-secondary-700" />
{`${lobbyInitialConfig?.winningScore ?? "n/a"} kills to win`}
{`${roomConfig.winningScore} kills to win`}
</div>
</div>
</td>
<td className={"w-20"}>
{lobbyState?.isGameEnd ? (
{roomConfig.isGameEnd ? (
<div className={"leading-4 mt-0.5"}>
<span>GAME ENDED</span>
</div>
) : (
<button
className={"mt-2"}
onClick={() => {
if (
!lobbyState ||
Object.keys(lobbyState.playerNicknameMap).length < (lobbyInitialConfig?.capacity ?? 0)
) {
if (Object.keys(roomConfig.playerNicknameMap).length < roomConfig.capacity) {
window.location.href = `/${lobby.roomId}`; //update url
}
}}
>
<BulletButton
disabled={
lobbyState &&
Object.keys(lobbyState.playerNicknameMap).length >= (lobbyInitialConfig?.capacity ?? 0)
}
disabled={Object.keys(roomConfig.playerNicknameMap).length >= roomConfig.capacity}
text={"JOIN!"}
/>
</button>
Expand All @@ -153,16 +146,24 @@ export function PublicLobbyList(props: PublicLobbyListProps) {
);
}

function useLobbies(appId: string): Lobby[] {
const [lobbies, setLobbies] = React.useState<Lobby[]>([]);
function useLobbies(appId: string): LobbyV3[] {
const [lobbies, setLobbies] = React.useState<LobbyV3[]>([]);
React.useEffect(() => {
if (appId) {
lobbyClient.listActivePublicLobbies(appId).then(setLobbies);
hathoraSdk.lobbyV3.listActivePublicLobbies().then(({ classes }) => {
if (classes != null) {
setLobbies(classes);
}
});
}
}, [appId]);
useInterval(() => {
if (appId) {
lobbyClient.listActivePublicLobbies(appId).then(setLobbies);
hathoraSdk.lobbyV3.listActivePublicLobbies().then(({ classes }) => {
if (classes != null) {
setLobbies(classes);
}
});
}
}, 2000);
return lobbies;
Expand Down
Loading

0 comments on commit 328190e

Please sign in to comment.