= {
2 | Two: "2",
3 | Three: "3",
4 | Four: "4",
5 | Five: "5",
6 | Six: "6",
7 | Seven: "7",
8 | Eight: "8",
9 | Nine: "9",
10 | Ten: "T",
11 | Jack: "J",
12 | Queen: "Q",
13 | King: "K",
14 | Ace: "A",
15 | };
16 |
--------------------------------------------------------------------------------
/examples/poker/client/web/src/hooks/useAutoJoinGame.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | import { useHathoraContext } from "../context/GameContext";
4 |
5 | export default function useAutoJoinGame(gameId?: string) {
6 | const { disconnect, joinGame, playerState, token, user, login } = useHathoraContext();
7 |
8 | useEffect(() => {
9 | // auto join the game once on this page
10 | if (gameId && token && !playerState?.players?.find((p) => p.id === user?.id)) {
11 | joinGame(gameId).catch(console.error);
12 | }
13 |
14 | if (!token) {
15 | // log the user in if they aren't already logged in
16 | login();
17 | }
18 | return disconnect;
19 | }, [gameId, token]);
20 | }
21 |
--------------------------------------------------------------------------------
/examples/poker/client/web/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | }
13 |
14 | code {
15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
16 | monospace;
17 | }
18 |
--------------------------------------------------------------------------------
/examples/poker/client/web/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "react-toastify/dist/ReactToastify.css";
4 |
5 | import App from "./App";
6 | import "./index.css";
7 | import HathoraContextProvider from "./context/GameContext";
8 |
9 | ReactDOM.createRoot(document.getElementById("root")!).render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/examples/poker/client/web/src/pages/Game.tsx:
--------------------------------------------------------------------------------
1 | import { useHathoraContext } from "../context/GameContext";
2 | import { Link, useParams } from "react-router-dom";
3 | import useAutoJoinGame from "../hooks/useAutoJoinGame";
4 | import Lobby from "../components/Lobby";
5 | import ActiveGame from "../components/ActiveGame";
6 | import { RoundStatus } from "../../../../api/types";
7 | import Loader from "../components/PageLoader";
8 | import Logo from "../assets/hathora-hammer-logo-light.png";
9 |
10 | export default function Game() {
11 | const { gameId } = useParams();
12 | const { playerState } = useHathoraContext();
13 |
14 | useAutoJoinGame(gameId);
15 |
16 | const RenderGame = () => {
17 | if (playerState?.roundStatus === RoundStatus.WAITING) {
18 | return ;
19 | } else if (playerState?.roundStatus === RoundStatus.ACTIVE || playerState?.roundStatus === RoundStatus.COMPLETED) {
20 | return ;
21 | }
22 |
23 | return ;
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |

32 |
33 |
34 | Powered By{" "}
35 |
36 | Hathora
37 |
38 |
39 |
40 |
41 | {RenderGame()}
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/examples/poker/client/web/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/poker/client/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | module.exports = {
3 | content: ["./index.html"]
4 | .map((str) => path.relative(process.cwd(), path.resolve(__dirname, str)))
5 | .concat(`${path.relative(process.cwd(), path.resolve(__dirname, "src"))}/**/*.{jsx,ts,js,tsx}`),
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [require("@tailwindcss/forms")],
10 | };
11 |
--------------------------------------------------------------------------------
/examples/poker/client/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/poker/client/web/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/poker/client/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | build: { target: "esnext" },
8 | define: {
9 | "process.env": {
10 | COORDINATOR_HOST: process.env.COORDINATOR_HOST,
11 | MATCHMAKER_HOST: process.env.MATCHMAKER_HOST,
12 | },
13 | },
14 | server: { host: "0.0.0.0" },
15 | clearScreen: false,
16 | });
17 |
--------------------------------------------------------------------------------
/examples/poker/hathora.yml:
--------------------------------------------------------------------------------
1 | types:
2 | PlayerStatus:
3 | - WAITING
4 | - FOLDED
5 | - PLAYED
6 | - WON
7 | - LOST
8 | RoundStatus:
9 | - WAITING
10 | - ACTIVE
11 | - COMPLETED
12 | Card:
13 | rank: string
14 | suit: string
15 | PlayerInfo:
16 | id: UserId
17 | chipCount: int
18 | chipsInPot: int
19 | cards: Card[]
20 | status: PlayerStatus
21 | PlayerState:
22 | players: PlayerInfo[]
23 | dealer: UserId?
24 | activePlayer: UserId?
25 | revealedCards: Card[]
26 | roundStatus: RoundStatus
27 |
28 | methods:
29 | joinGame:
30 | startGame:
31 | startingChips: int
32 | startingBlind: int
33 | startRound:
34 | fold:
35 | call:
36 | raise:
37 | amount: int
38 |
39 | auth:
40 | anonymous: {}
41 |
42 | userState: PlayerState
43 | error: string
44 |
--------------------------------------------------------------------------------
/examples/poker/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "poker-server",
3 | "version": "0.0.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "poker-server",
9 | "version": "0.0.1",
10 | "dependencies": {
11 | "@pairjacks/poker-cards": "^0.3.0"
12 | },
13 | "devDependencies": {
14 | "typescript": "^4.5.2"
15 | }
16 | },
17 | "node_modules/@pairjacks/poker-cards": {
18 | "version": "0.3.0",
19 | "resolved": "https://registry.npmjs.org/@pairjacks/poker-cards/-/poker-cards-0.3.0.tgz",
20 | "integrity": "sha512-HsLeWXLLk9INPwb+eVm0yjJbqjvniiaFenTmj/Hy/z4VDfzxS5621gjF41P+0dplYWXQWuvL40VoHZXUGk61vg==",
21 | "engines": {
22 | "node": ">=12.0.0"
23 | }
24 | },
25 | "node_modules/typescript": {
26 | "version": "4.5.2",
27 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
28 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
29 | "dev": true,
30 | "bin": {
31 | "tsc": "bin/tsc",
32 | "tsserver": "bin/tsserver"
33 | },
34 | "engines": {
35 | "node": ">=4.2.0"
36 | }
37 | }
38 | },
39 | "dependencies": {
40 | "@pairjacks/poker-cards": {
41 | "version": "0.3.0",
42 | "resolved": "https://registry.npmjs.org/@pairjacks/poker-cards/-/poker-cards-0.3.0.tgz",
43 | "integrity": "sha512-HsLeWXLLk9INPwb+eVm0yjJbqjvniiaFenTmj/Hy/z4VDfzxS5621gjF41P+0dplYWXQWuvL40VoHZXUGk61vg=="
44 | },
45 | "typescript": {
46 | "version": "4.5.2",
47 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
48 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
49 | "dev": true
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/examples/poker/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "poker-server",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "dependencies": {
6 | "@pairjacks/poker-cards": "^0.3.0"
7 | },
8 | "devDependencies": {
9 | "typescript": "^4.5.2"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/examples/poker/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/pong/.gitignore:
--------------------------------------------------------------------------------
1 | .hathora
2 | node_modules
3 | dist
4 | .env
5 | /api
6 | /data/*
7 | !/data/saves
8 | /client/prototype-ui/*
9 | !/client/prototype-ui/plugins
10 |
--------------------------------------------------------------------------------
/examples/pong/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.18-alpine as build
2 |
3 | WORKDIR /app
4 |
5 | RUN apk add --update git
6 |
7 | COPY . .
8 |
9 | RUN npm i -g hathora@0.12.1
10 | RUN npx hathora build --only server
11 |
12 | FROM node:18.18-alpine
13 |
14 | WORKDIR /app
15 |
16 | # https://github.com/uNetworking/uWebSockets.js/discussions/346#discussioncomment-1137301
17 | RUN apk add --no-cache libc6-compat
18 | RUN ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2
19 |
20 | COPY --from=build /app/server/dist server/dist
21 |
22 | ENV NODE_ENV=production
23 | ENV DATA_DIR=/app/data
24 |
25 | CMD ["node", "server/dist/index.mjs"]
26 |
--------------------------------------------------------------------------------
/examples/pong/README.md:
--------------------------------------------------------------------------------
1 | Try it at: https://hathora-pong.surge.sh/
2 |
3 | 
4 |
5 | To run locally:
6 |
7 | - install hathora (`npm install -g hathora`)
8 | - clone or download this repo
9 | - cd into this directory
10 | - run `hathora dev`
11 | - visit http://localhost:3000 in your browser
12 |
--------------------------------------------------------------------------------
/examples/pong/client/prototype-ui/plugins/PlayerState/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "interpolation-buffer": "^1.2.3",
5 | "lit": "^2.0.2"
6 | },
7 | "devDependencies": {
8 | "typescript": "^4.5.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/pong/client/prototype-ui/plugins/PlayerState/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "esModuleInterop": true,
5 | "module": "esnext",
6 | "strict": true,
7 | "target": "esnext",
8 | "moduleResolution": "node",
9 | "isolatedModules": true,
10 | "useDefineForClassFields": false
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/pong/hathora.yml:
--------------------------------------------------------------------------------
1 | types:
2 | Direction:
3 | - NONE
4 | - UP
5 | - DOWN
6 | Point:
7 | x: float
8 | y: float
9 | Player:
10 | paddle: float
11 | score: int
12 | PlayerState:
13 | playerA: Player
14 | playerB: Player
15 | ball: Point
16 |
17 | methods:
18 | setDirection:
19 | direction: Direction
20 |
21 | auth:
22 | anonymous: {}
23 |
24 | userState: PlayerState
25 | error: string
26 | tick: 50
27 |
--------------------------------------------------------------------------------
/examples/pong/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pong-server",
3 | "version": "0.0.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "pong-server",
9 | "version": "0.0.1",
10 | "devDependencies": {
11 | "typescript": "^4.5.2"
12 | }
13 | },
14 | "node_modules/typescript": {
15 | "version": "4.5.2",
16 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
17 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
18 | "dev": true,
19 | "bin": {
20 | "tsc": "bin/tsc",
21 | "tsserver": "bin/tsserver"
22 | },
23 | "engines": {
24 | "node": ">=4.2.0"
25 | }
26 | }
27 | },
28 | "dependencies": {
29 | "typescript": {
30 | "version": "4.5.2",
31 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
32 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
33 | "dev": true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/pong/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pong-server",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "devDependencies": {
6 | "typescript": "^4.5.2"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/pong/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/.gitignore:
--------------------------------------------------------------------------------
1 | .hathora
2 | node_modules
3 | dist
4 | .env
5 | /api
6 | /data/*
7 | !/data/saves
8 | /client/prototype-ui/*
9 | !/client/prototype-ui/plugins
10 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.18-alpine as build
2 |
3 | WORKDIR /app
4 |
5 | RUN apk add --update git
6 |
7 | COPY . .
8 |
9 | RUN npm i -g hathora@0.12.1
10 | RUN npx hathora build --only server
11 |
12 | FROM node:18.18-alpine
13 |
14 | WORKDIR /app
15 |
16 | # https://github.com/uNetworking/uWebSockets.js/discussions/346#discussioncomment-1137301
17 | RUN apk add --no-cache libc6-compat
18 | RUN ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2
19 |
20 | COPY --from=build /app/server/dist server/dist
21 |
22 | ENV NODE_ENV=production
23 | ENV DATA_DIR=/app/data
24 |
25 | CMD ["node", "server/dist/index.mjs"]
26 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/README.md:
--------------------------------------------------------------------------------
1 | Try it at: https://hathora-rock-paper-scissor.surge.sh/
2 |
3 | 
4 |
5 | To run locally:
6 |
7 | - install hathora (`npm install -g hathora`)
8 | - clone or download this repo
9 | - cd into this directory
10 | - run `hathora dev`
11 | - visit http://localhost:3000 in your browser
12 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/hathora.yml:
--------------------------------------------------------------------------------
1 | types:
2 | Gesture:
3 | - ROCK
4 | - PAPER
5 | - SCISSOR
6 | PlayerInfo:
7 | id: UserId
8 | score: int
9 | gesture: Gesture?
10 | PlayerState:
11 | round: int
12 | player1: PlayerInfo?
13 | player2: PlayerInfo?
14 |
15 | methods:
16 | joinGame:
17 | chooseGesture:
18 | gesture: Gesture
19 | nextRound:
20 |
21 | auth:
22 | anonymous: {}
23 |
24 | userState: PlayerState
25 | error: string
26 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rock-paper-scissor-server",
3 | "version": "0.0.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "rock-paper-scissor-server",
9 | "version": "0.0.1",
10 | "devDependencies": {
11 | "typescript": "^4.5.2"
12 | }
13 | },
14 | "node_modules/typescript": {
15 | "version": "4.5.2",
16 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
17 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
18 | "dev": true,
19 | "bin": {
20 | "tsc": "bin/tsc",
21 | "tsserver": "bin/tsserver"
22 | },
23 | "engines": {
24 | "node": ">=4.2.0"
25 | }
26 | }
27 | },
28 | "dependencies": {
29 | "typescript": {
30 | "version": "4.5.2",
31 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
32 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
33 | "dev": true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rock-paper-scissor-server",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "devDependencies": {
6 | "typescript": "^4.5.2"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/rock-paper-scissor/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/uno/.gitignore:
--------------------------------------------------------------------------------
1 | .hathora
2 | node_modules
3 | dist
4 | .env
5 | /api
6 | /data/*
7 | !/data/saves
8 | /client/prototype-ui/*
9 | !/client/prototype-ui/plugins
10 |
--------------------------------------------------------------------------------
/examples/uno/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.18-alpine as build
2 |
3 | WORKDIR /app
4 |
5 | RUN apk add --update git
6 |
7 | COPY . .
8 |
9 | RUN npm i -g hathora@0.12.1
10 | RUN npx hathora build --only server
11 |
12 | FROM node:18.18-alpine
13 |
14 | WORKDIR /app
15 |
16 | # https://github.com/uNetworking/uWebSockets.js/discussions/346#discussioncomment-1137301
17 | RUN apk add --no-cache libc6-compat
18 | RUN ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2
19 |
20 | COPY --from=build /app/server/dist server/dist
21 |
22 | ENV NODE_ENV=production
23 | ENV DATA_DIR=/app/data
24 |
25 | CMD ["node", "server/dist/index.mjs"]
26 |
--------------------------------------------------------------------------------
/examples/uno/README.md:
--------------------------------------------------------------------------------
1 | Try it at: https://hathora-uno.surge.sh/ (prototype UI) or https://material-suit.surge.sh/ (custom UI)
2 |
3 | 
4 |
5 | To run locally:
6 |
7 | - install hathora (`npm install -g hathora`)
8 | - clone or download this repo
9 | - cd into this directory
10 | - run `hathora dev`
11 | - visit http://localhost:3000 in your browser
12 |
--------------------------------------------------------------------------------
/examples/uno/client/prototype-ui/plugins/Card/index.ts:
--------------------------------------------------------------------------------
1 | import { LitElement, html, css } from "lit";
2 | import { property } from "lit/decorators.js";
3 | import { styleMap } from "lit/directives/style-map.js";
4 | import { Card, Color } from "../../../../api/types";
5 | import { HathoraConnection } from "../../../.hathora/client";
6 |
7 | let DISPLAY_COLORS = {
8 | [Color.RED]: "#e16c6c",
9 | [Color.BLUE]: "#6c91d9",
10 | [Color.GREEN]: "#70bd56",
11 | [Color.YELLOW]: "#fcda49",
12 | };
13 |
14 | export default class CardComponent extends LitElement {
15 | @property() val!: Card;
16 | @property() client!: HathoraConnection;
17 |
18 | static get styles() {
19 | return css`
20 | .game-main {
21 | font-family: "Patua One", sans-serif;
22 | }
23 | `;
24 | }
25 |
26 | render() {
27 | return html` {
45 | const res = await this.client.playCard({ card: this.val });
46 | if (res.type === "error") {
47 | this.dispatchEvent(new CustomEvent("error", { detail: res.error }));
48 | }
49 | }}"
50 | >
51 | ${this.val.value}
52 |
`;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/uno/client/prototype-ui/plugins/Card/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "lit": "^2.0.2"
5 | },
6 | "devDependencies": {
7 | "typescript": "^4.5.2"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/uno/client/prototype-ui/plugins/Card/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true,
9 | "experimentalDecorators": true,
10 | "useDefineForClassFields": false
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/uno/client/web/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/examples/uno/client/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "start": "vite",
7 | "build": "vite build"
8 | },
9 | "dependencies": {
10 | "@headlessui/react": "^1.5.0",
11 | "@heroicons/react": "^1.0.6",
12 | "react": "^18.0.0",
13 | "react-copy-to-clipboard": "^5.1.0",
14 | "react-dom": "^18.0.0",
15 | "react-qr-code": "^2.0.7",
16 | "react-router-dom": "^6.3.0",
17 | "react-toastify": "^8.2.0",
18 | "rooks": "^5.11.0",
19 | "styled-components": "^5.3.5"
20 | },
21 | "devDependencies": {
22 | "@tailwindcss/forms": "^0.5.0",
23 | "@types/react": "^18.0.0",
24 | "@types/react-copy-to-clipboard": "^5.0.2",
25 | "@types/react-dom": "^18.0.0",
26 | "@types/react-router-dom": "^5.3.3",
27 | "@types/styled-components": "^5.1.25",
28 | "@vitejs/plugin-react": "^1.3.0",
29 | "autoprefixer": "^10.4.4",
30 | "postcss": "^8.4.12",
31 | "prettier": "^2.6.2",
32 | "tailwindcss": "^3.0.24",
33 | "typescript": "^4.6.3",
34 | "vite": "^2.9.5"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/uno/client/web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require("tailwindcss")(require("./tailwind.config.js")), require("autoprefixer")],
3 | };
4 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
2 | import Home from "./pages/Home";
3 | import Game from "./pages/Game";
4 |
5 | export default function App() {
6 | return (
7 |
8 |
9 | } />
10 | } />
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/assets/hathora-hammer-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hathora/builder/a4182519ea1e4381def6484ba46a09a16f930769/examples/uno/client/web/src/assets/hathora-hammer-logo-light.png
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/ActiveGame.tsx:
--------------------------------------------------------------------------------
1 | import { useHathoraContext } from "../context/GameContext";
2 | import CardPile from "./CardPile";
3 | import GameHand from "./GameHand";
4 | import OpponentHand from "./OpenentHand";
5 |
6 | export default function ActiveGame() {
7 | const { playerState, user, getUserName } = useHathoraContext();
8 | const currentUserIndex = playerState?.players.findIndex((p) => p.id === user?.id);
9 |
10 | const players = [
11 | ...(playerState?.players.slice(currentUserIndex || 0, playerState.players.length) || []),
12 | ...(playerState?.players.slice(0, currentUserIndex) || []),
13 | ];
14 |
15 | return (
16 | <>
17 |
18 | {players.map((player) =>
19 | player.id === user?.id ? (
20 |
21 | ) : (
22 |
29 | )
30 | )}
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/BaseCard.ts:
--------------------------------------------------------------------------------
1 | import { css } from "styled-components";
2 |
3 | export const BaseCard = css<{ cursor?: string }>`
4 | line-height: 75px;
5 | width: 120px;
6 | height: 160px;
7 | text-align: center;
8 | cursor: ${({ cursor }) => cursor ?? "pointer"};
9 | border: 2px solid white;
10 | display: flex;
11 | border-radius: 8px;
12 | justify-content: center;
13 | align-items: center;
14 | font-size: 3rem;
15 |
16 | @media (max-width: 800px) {
17 | width: 80px;
18 | height: 120px;
19 | font-size: 1.75rem;
20 | }
21 |
22 | color: white;
23 | text-shadow: 1px 2px #000000;
24 | box-shadow: 2px 2px 0px 0px black;
25 | `;
26 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/CardPile.tsx:
--------------------------------------------------------------------------------
1 | import { useHathoraContext } from "../context/GameContext";
2 | import UnoCard from "./UnoCard";
3 | import SideDownUno from "./SideDownUno";
4 |
5 | export default function CardPile() {
6 | const { playerState, drawCard } = useHathoraContext();
7 |
8 | const pile = playerState?.pile;
9 |
10 | return (
11 |
12 |
13 | {pile?.color !== undefined && pile?.value && (
14 |
18 | )}
19 | {
20 |
24 | }
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/GameHand.tsx:
--------------------------------------------------------------------------------
1 | import { useHathoraContext } from "../context/GameContext";
2 | import UnoCard from "./UnoCard";
3 |
4 | export default function GameHand() {
5 | const { playerState, playCard, user, getUserName } = useHathoraContext();
6 |
7 | return (
8 |
9 |
10 | {user?.id === playerState?.turn && ➡️} {user?.id && getUserName(user?.id)}{" "}
11 | (You)
12 |
13 |
14 |
15 | {playerState?.hand?.map((card) => (
16 | playCard(card)}
20 | color={card.color}
21 | value={card.value}
22 | />
23 | ))}
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/Lobby.tsx:
--------------------------------------------------------------------------------
1 | import { useParams } from "react-router-dom";
2 | import { CopyToClipboard } from "react-copy-to-clipboard";
3 | import { toast } from "react-toastify";
4 | import QRCode from "react-qr-code";
5 | import { ClipboardCopyIcon } from "@heroicons/react/outline";
6 | import { useHathoraContext } from "../context/GameContext";
7 | import PlayerList from "./PlayerList";
8 |
9 | export default function Lobby() {
10 | const { gameId } = useParams();
11 | const { startGame } = useHathoraContext();
12 | return (
13 | <>
14 |
15 |
Invite Friends
16 |
toast.success("Copied room link to clipboard!")}>
17 |
18 |
19 |
20 |
21 | Room Code: {gameId}
22 |
23 |
24 |
25 |
Players in lobby
26 |
27 |
33 |
34 | >
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/MiniCard.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const CardWrapper = styled.div`
4 | line-height: 75px;
5 | width: 0.5em;
6 | height: 0.75em;
7 | text-align: center;
8 | cursor: pointer;
9 | border: 1px solid white;
10 | outline: 1px solid black;
11 | display: flex;
12 | border-radius: 2px;
13 | justify-content: center;
14 | align-items: center;
15 | font-size: 2rem;
16 | color: white;
17 | text-shadow: 1px 2px #000000;
18 | box-shadow: 2px 2px 0px 0px black;
19 | background-color: black;
20 | `;
21 |
22 | const MiniCardsRow = ({ count }: { count: number }) => {
23 | const card = new Array(count).fill(null, 0, count).map((_, i) => i);
24 | return (
25 |
26 | {card.map((i) => (
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default MiniCardsRow;
34 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/OpenentHand.tsx:
--------------------------------------------------------------------------------
1 | import SideDownUno from "./SideDownUno";
2 |
3 | export default function OpponentHand({
4 | cardCount,
5 | name,
6 | active,
7 | disabled,
8 | }: {
9 | cardCount: number;
10 | name: string;
11 | active?: boolean;
12 | disabled?: boolean;
13 | }) {
14 | return (
15 |
16 |
17 | {active && ➡️} {name}
18 |
19 |
20 |
21 | {new Array(cardCount).fill(null, 0, cardCount)?.map((_, i) => (
22 |
23 | ))}
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/PlayerList.tsx:
--------------------------------------------------------------------------------
1 | import { useHathoraContext } from "../context/GameContext";
2 | import MiniCardsRow from "./MiniCard";
3 |
4 | export default function PlayerList() {
5 | const { playerState, user, getUserName } = useHathoraContext();
6 | return (
7 |
8 | {playerState?.players.map((player) => (
9 |
15 | {getUserName(player.id)} {player.id === user?.id ? "(You)" : ""}
16 |
17 |
18 |
19 |
20 | {player.id === playerState?.turn ? "Current player" : ""}
21 |
22 |
23 | ))}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/SideDownUno.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { BaseCard } from "./BaseCard";
3 |
4 | const CardWrapper = styled.div<{ disabled?: boolean; active?: boolean }>`
5 | background-color: ${({ active }) => (active ? "green" : "gray")};
6 | opacity: ${({ disabled }) => (disabled ? 0.5 : "initial")};
7 | ${BaseCard}
8 | `;
9 |
10 | const SideDownUno = ({
11 | disabled,
12 | active,
13 | label,
14 | onClick,
15 | }: {
16 | disabled?: boolean;
17 | active?: boolean;
18 | label?: string;
19 | onClick?: () => void;
20 | }) => {
21 | return (
22 | <>
23 |
24 | {label}
25 |
26 | >
27 | );
28 | };
29 |
30 | export default SideDownUno;
31 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/components/UnoCard.tsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import { Color } from "../../../../api/types";
3 | import { BaseCard } from "./BaseCard";
4 |
5 | let DISPLAY_COLORS = {
6 | [Color.RED]: "#e16c6c",
7 | [Color.BLUE]: "#6c91d9",
8 | [Color.GREEN]: "#70bd56",
9 | [Color.YELLOW]: "#fcda49",
10 | };
11 |
12 | const CardWrapper = styled.div<{ colorV: Color; disabled?: boolean; cursor?: string }>`
13 | background-color: ${({ colorV }) => DISPLAY_COLORS[colorV]};
14 | opacity: ${({ disabled }) => (disabled ? 0.5 : "initial")};
15 | ${BaseCard}
16 | `;
17 |
18 | const CardValue = ({
19 | color,
20 | value,
21 | onClick,
22 | disabled,
23 | cursor,
24 | }: {
25 | disabled?: boolean;
26 | color: Color;
27 | value: number;
28 | onClick?: () => void;
29 | cursor?: string;
30 | }) => {
31 | return (
32 |
39 | {value}
40 |
41 | );
42 | };
43 |
44 | export default CardValue;
45 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | }
13 |
14 | code {
15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
16 | monospace;
17 | }
18 |
19 |
20 | .hide-scroll-bar {
21 | -ms-overflow-style: none; /* Internet Explorer 10+ */
22 | scrollbar-width: none;
23 | }
24 |
25 | .hide-scroll-bar::-webkit-scrollbar {
26 | display: none; /* Safari and Chrome */
27 | }
28 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/main.tsx:
--------------------------------------------------------------------------------
1 | import ReactDOM from "react-dom/client";
2 | import "react-toastify/dist/ReactToastify.css";
3 | import HathoraContextProvider from "./context/GameContext";
4 |
5 | import App from "./App";
6 | import "./index.css";
7 |
8 | ReactDOM.createRoot(document.getElementById("root")!).render(
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/examples/uno/client/web/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/examples/uno/client/web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | module.exports = {
3 | content: ["./index.html"]
4 | .map((str) => path.relative(process.cwd(), path.resolve(__dirname, str)))
5 | .concat(`${path.relative(process.cwd(), path.resolve(__dirname, "src"))}/**/*.{jsx,ts,js,tsx}`),
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [require("@tailwindcss/forms")],
10 | };
11 |
--------------------------------------------------------------------------------
/examples/uno/client/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx"
18 | },
19 | "include": ["src"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/uno/client/web/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "esnext",
5 | "moduleResolution": "node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/examples/uno/client/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | build: { target: "esnext" },
8 | define: {
9 | "process.env": {
10 | COORDINATOR_HOST: process.env.COORDINATOR_HOST,
11 | MATCHMAKER_HOST: process.env.MATCHMAKER_HOST,
12 | },
13 | },
14 | server: { host: "0.0.0.0" },
15 | clearScreen: false,
16 | });
17 |
--------------------------------------------------------------------------------
/examples/uno/hathora.yml:
--------------------------------------------------------------------------------
1 | types:
2 | Color:
3 | - RED
4 | - BLUE
5 | - GREEN
6 | - YELLOW
7 | Card:
8 | value: int
9 | color: Color
10 | Player:
11 | id: UserId
12 | numCards: int
13 | PlayerState:
14 | hand: Card[]
15 | players: Player[]
16 | turn: UserId?
17 | pile: Card?
18 | winner: UserId?
19 |
20 | methods:
21 | joinGame:
22 | startGame:
23 | playCard:
24 | card: Card
25 | drawCard:
26 |
27 | auth:
28 | anonymous: {}
29 |
30 | userState: PlayerState
31 | error: string
32 |
--------------------------------------------------------------------------------
/examples/uno/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uno-server",
3 | "version": "0.0.1",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "uno-server",
9 | "version": "0.0.1",
10 | "devDependencies": {
11 | "typescript": "^4.5.2"
12 | }
13 | },
14 | "node_modules/typescript": {
15 | "version": "4.5.2",
16 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
17 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
18 | "dev": true,
19 | "bin": {
20 | "tsc": "bin/tsc",
21 | "tsserver": "bin/tsserver"
22 | },
23 | "engines": {
24 | "node": ">=4.2.0"
25 | }
26 | }
27 | },
28 | "dependencies": {
29 | "typescript": {
30 | "version": "4.5.2",
31 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
32 | "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
33 | "dev": true
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/examples/uno/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uno-server",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "devDependencies": {
6 | "typescript": "^4.5.2"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/uno/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/node-loader.config.mjs:
--------------------------------------------------------------------------------
1 | import os from "os";
2 |
3 | import * as tsNode from "ts-node/esm";
4 | import * as hotEsm from "hot-esm";
5 |
6 | export default {
7 | loaders: os.platform() !== "win32" ? [hotEsm, tsNode] : [tsNode],
8 | };
9 |
--------------------------------------------------------------------------------
/scripts/run-all-examples.sh:
--------------------------------------------------------------------------------
1 | echo "Testing all examples..."
2 | cd examples;
3 | success=0;
4 | for example in `ls`; do
5 | echo "Testing $example...";
6 | cd $example;
7 | ts-node ../../src/cli.ts generate && \
8 | ts-node ../../src/cli.ts install && \
9 | npx tsc server/.hathora/store.ts --esModuleInterop --target esnext --module esnext --moduleResolution node --noEmit
10 | if [ $? -ne 0 ]; then
11 | echo "Failed to run example: $example";
12 | success=1;
13 | fi;
14 | cd ..
15 | done
16 | exit $success;
17 |
--------------------------------------------------------------------------------
/scripts/test-example.sh:
--------------------------------------------------------------------------------
1 | example="$1";
2 | success=0;
3 |
4 | if [ -z $1 ]; then
5 | echo "Usage: Provide the name of the example project you want to test.";
6 | echo "\nAvailable examples:";
7 | echo $(ls examples);
8 | exit 1;
9 | fi;
10 |
11 | cd examples;
12 |
13 | echo "Testing $example...";
14 | cd $example;
15 | ts-node ../../src/cli.ts generate && \
16 | ts-node ../../src/cli.ts install && \
17 | npx tsc server/.hathora/store.ts --esModuleInterop --target esnext --module esnext --moduleResolution node --noEmit
18 | if [ $? -ne 0 ]; then
19 | echo "Failed to run example: $example";
20 | success=1;
21 | fi;
22 | cd ..;
23 | exit $success;
24 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { join } from "path";
4 | import os from "os";
5 | import fs from "fs";
6 |
7 | import yargs from "yargs/yargs";
8 | import { hideBin } from "yargs/helpers";
9 | import { MiddlewareFunction } from "yargs";
10 | import updateNotifier from "update-notifier";
11 | import chalk from "chalk";
12 |
13 | updateNotifier({ pkg: require("../package.json") }).notify({ defer: false, isGlobal: true });
14 |
15 | const cloudMiddleware: MiddlewareFunction = (argv) => {
16 | if (argv._[0] !== "cloud") {
17 | return;
18 | }
19 |
20 | if (!(argv._[1] === "login" || "token" in argv)) {
21 | const tokenFile = join(os.homedir(), ".config", "hathora", "token");
22 | if (!fs.existsSync(tokenFile)) {
23 | console.log(chalk.redBright(`Missing token file, run ${chalk.underline("hathora cloud login")} first`));
24 | return;
25 | }
26 |
27 | argv.token = fs.readFileSync(tokenFile).toString();
28 | }
29 |
30 | if (!("cloudApiBase" in argv)) {
31 | argv.cloudApiBase = "https://cloud.hathora.com/v1";
32 | }
33 | };
34 |
35 | yargs(hideBin(process.argv))
36 | .scriptName("hathora")
37 | .middleware(cloudMiddleware, true)
38 | .commandDir("commands", { extensions: ["js", "ts"] })
39 | .demandCommand()
40 | .recommendCommands()
41 | .completion()
42 | .wrap(72)
43 | .parse();
44 |
--------------------------------------------------------------------------------
/src/commands/cloud.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 |
3 | const cmd: CommandModule = {
4 | command: "cloud",
5 | aliases: ["c"],
6 | describe: "Interact with Hathora Cloud",
7 | builder: (yargs) => yargs.commandDir("cloud", { extensions: ["js", "ts"] }),
8 | handler() {
9 | console.log("Please use one of the subcommands (run with --help for full list.");
10 | },
11 | };
12 |
13 | module.exports = cmd;
14 |
--------------------------------------------------------------------------------
/src/commands/cloud/deploy.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 | import tar from "tar";
3 | import FormData from "form-data";
4 |
5 | import { getDirs, makeCloudApiRequest } from "../../utils";
6 |
7 | const cmd: CommandModule = {
8 | command: "deploy",
9 | aliases: ["d"],
10 | describe: "Deploys application to Hathora Cloud",
11 | builder: {
12 | appName: { type: "string", demandOption: true },
13 | token: { type: "string", demandOption: true, hidden: true },
14 | cloudApiBase: { type: "string", demandOption: true, hidden: true },
15 | },
16 | async handler(argv) {
17 | let rootDir: string;
18 | try {
19 | rootDir = getDirs().rootDir;
20 | } catch (e) {
21 | rootDir = process.cwd();
22 | }
23 | const tarFile = tar.create(
24 | {
25 | cwd: rootDir,
26 | gzip: true,
27 | filter: (path) =>
28 | !path.startsWith("./api") &&
29 | !path.startsWith("./data") &&
30 | !path.startsWith("./client") &&
31 | !path.includes(".hathora") &&
32 | !path.includes("node_modules") &&
33 | !path.includes(".git"),
34 | },
35 | ["."]
36 | );
37 | const form = new FormData();
38 | form.append("appName", argv.appName);
39 | form.append("file", tarFile, "bundle.tar.gz");
40 | await makeCloudApiRequest(argv.cloudApiBase as string, "/deploy", argv.token as string, "POST", form);
41 | },
42 | };
43 |
44 | module.exports = cmd;
45 |
--------------------------------------------------------------------------------
/src/commands/cloud/destroy.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 | import prompts from "prompts";
3 |
4 | import { makeCloudApiRequest } from "../../utils";
5 |
6 | const cmd: CommandModule = {
7 | command: "destroy",
8 | describe: "Destroy a Hathora Cloud application",
9 | builder: {
10 | yes: { type: "boolean", describe: "Accept all confirmations", default: false },
11 | appName: { type: "string", demandOption: true },
12 | token: { type: "string", demandOption: true, hidden: true },
13 | cloudApiBase: { type: "string", demandOption: true, hidden: true },
14 | },
15 | async handler(argv) {
16 | if (!argv.yes) {
17 | const userInput = await prompts({
18 | type: "confirm",
19 | name: "value",
20 | message: `Are you sure you want to destroy ${argv.appName}? This action is irreversible.`,
21 | });
22 |
23 | if (!userInput.value) {
24 | return;
25 | }
26 | }
27 | await makeCloudApiRequest(argv.cloudApiBase as string, `/app/${argv.appName}`, argv.token as string, "DELETE");
28 | },
29 | };
30 |
31 | module.exports = cmd;
32 |
--------------------------------------------------------------------------------
/src/commands/cloud/info.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 |
3 | import { makeCloudApiRequest } from "../../utils";
4 |
5 | const cmd: CommandModule = {
6 | command: "info",
7 | aliases: ["i"],
8 | describe: "Get details about a Hathora Cloud application",
9 | builder: {
10 | appName: { type: "string", demandOption: true },
11 | token: { type: "string", demandOption: true, hidden: true },
12 | cloudApiBase: { type: "string", demandOption: true, hidden: true },
13 | },
14 | async handler(argv) {
15 | await makeCloudApiRequest(argv.cloudApiBase as string, `/app/${argv.appName}`, argv.token as string);
16 | },
17 | };
18 |
19 | module.exports = cmd;
20 |
--------------------------------------------------------------------------------
/src/commands/cloud/list.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 |
3 | import { makeCloudApiRequest } from "../../utils";
4 |
5 | const cmd: CommandModule = {
6 | command: "list",
7 | aliases: ["ls"],
8 | describe: "List Hathora Cloud applications",
9 | builder: {
10 | token: { type: "string", demandOption: true, hidden: true },
11 | cloudApiBase: { type: "string", demandOption: true, hidden: true },
12 | },
13 | async handler(argv) {
14 | await makeCloudApiRequest(argv.cloudApiBase as string, "/list", argv.token as string);
15 | },
16 | };
17 |
18 | module.exports = cmd;
19 |
--------------------------------------------------------------------------------
/src/commands/cloud/login.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 | import os from "os";
3 | import { existsSync } from "fs";
4 |
5 | import { CommandModule } from "yargs";
6 | import prompts from "prompts";
7 | import { Issuer } from "openid-client";
8 | import open from "open";
9 | import { outputFileSync } from "fs-extra";
10 | import chalk from "chalk";
11 |
12 | const cmd: CommandModule = {
13 | command: "login",
14 | aliases: "l",
15 | describe: "Login to Hathora Cloud",
16 | async handler() {
17 | const tokenPath = join(os.homedir(), ".config", "hathora", "token");
18 | if (existsSync(tokenPath)) {
19 | console.log(
20 | chalk.red(
21 | `Token file already present at ${tokenPath}. If you'd like to get a new one, please remove this file.`
22 | )
23 | );
24 | return;
25 | }
26 |
27 | const auth0 = await Issuer.discover("https://auth.hathora.com");
28 | const client = new auth0.Client({
29 | client_id: "tWjDhuzPmuIWrI8R9s3yV3BQVw2tW0yq",
30 | token_endpoint_auth_method: "none",
31 | id_token_signed_response_alg: "RS256",
32 | grant_type: "refresh_token",
33 | });
34 | const handle = await client.deviceAuthorization({
35 | scope: "openid email offline_access",
36 | audience: "https://cloud.hathora.com",
37 | });
38 |
39 | const userInput = await prompts({
40 | type: "confirm",
41 | name: "value",
42 | message: `Open browser for login? You should see the following code: ${handle.user_code}.`,
43 | initial: true,
44 | });
45 |
46 | if (!userInput.value) {
47 | return;
48 | }
49 |
50 | open(handle.verification_uri_complete);
51 | const tokens = await handle.poll();
52 | outputFileSync(tokenPath, tokens.refresh_token);
53 | console.log(chalk.green(`Successfully logged in! Saved credentials to ${tokenPath}`));
54 | },
55 | };
56 |
57 | module.exports = cmd;
58 |
--------------------------------------------------------------------------------
/src/commands/cloud/logs.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 |
3 | import { makeCloudApiRequest } from "../../utils";
4 |
5 | const cmd: CommandModule = {
6 | command: "logs",
7 | describe: "Get logs from a Hathora Cloud application",
8 | builder: {
9 | appName: { type: "string", demandOption: true },
10 | token: { type: "string", demandOption: true, hidden: true },
11 | cloudApiBase: { type: "string", demandOption: true, hidden: true },
12 | },
13 | async handler(argv) {
14 | await makeCloudApiRequest(argv.cloudApiBase as string, `/app/${argv.appName}/logs`, argv.token as string);
15 | },
16 | };
17 |
18 | module.exports = cmd;
19 |
--------------------------------------------------------------------------------
/src/commands/create-client.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 |
3 | import { CommandModule } from "yargs";
4 |
5 | import { getDirs } from "../utils";
6 | import { generate } from "../generate";
7 |
8 | const cmd: CommandModule = {
9 | command: "create-client ",
10 | describe: "Creates a client subdirectory",
11 | handler(argv) {
12 | const { rootDir } = getDirs();
13 | generate(rootDir, join("client", argv.template as string), { name: argv.name as string });
14 | },
15 | };
16 |
17 | module.exports = cmd;
18 |
--------------------------------------------------------------------------------
/src/commands/create-plugin.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 |
3 | import { CommandModule } from "yargs";
4 |
5 | import { getDirs } from "../utils";
6 | import { generate } from "../generate";
7 |
8 | const cmd: CommandModule = {
9 | command: "create-plugin ",
10 | describe: "Creates a plugin",
11 | handler(argv) {
12 | const { rootDir } = getDirs();
13 | generate(rootDir, join("plugin", argv.lib as string), { val: argv.type as string });
14 | },
15 | };
16 |
17 | module.exports = cmd;
18 |
--------------------------------------------------------------------------------
/src/commands/dev.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 | import { existsSync } from "fs";
3 |
4 | import { CommandModule } from "yargs";
5 | import chalk from "chalk";
6 |
7 | import { generateLocal, getDirs, install, start } from "../utils";
8 |
9 | const cmd: CommandModule = {
10 | command: "dev",
11 | aliases: ["development", "d"],
12 | describe: "Starts the server in development mode",
13 | builder: { only: { choices: ["client", "server"] } },
14 | async handler(argv) {
15 | const { serverDir } = getDirs();
16 | if (!existsSync(join(serverDir, "impl.ts"))) {
17 | console.error(
18 | `${chalk.red("Missing impl.ts, make sure to run")}` +
19 | `${chalk.blue.bold(" hathora init ")}` +
20 | `${chalk.red("first")}`
21 | );
22 | return;
23 | }
24 | await generateLocal();
25 | install(argv.only as "server" | "client" | undefined);
26 | start(argv.only as "server" | "client" | undefined);
27 | },
28 | };
29 |
30 | module.exports = cmd;
31 |
--------------------------------------------------------------------------------
/src/commands/generate.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 | import { existsSync } from "fs";
3 |
4 | import { CommandModule } from "yargs";
5 | import chalk from "chalk";
6 |
7 | import { generateLocal, getDirs } from "../utils";
8 |
9 | const cmd: CommandModule = {
10 | command: "generate",
11 | aliases: ["gen", "g"],
12 | describe: "Regenerates the types from hathora.yml",
13 | async handler() {
14 | const { serverDir } = getDirs();
15 | if (!existsSync(join(serverDir, "impl.ts"))) {
16 | console.error(
17 | `${chalk.red("Missing impl.ts, make sure to run")}` +
18 | `${chalk.blue.bold(" hathora init ")}` +
19 | `${chalk.red("first")}`
20 | );
21 | } else {
22 | await generateLocal();
23 | }
24 | },
25 | };
26 |
27 | module.exports = cmd;
28 |
--------------------------------------------------------------------------------
/src/commands/init.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 | import { existsSync } from "fs";
3 |
4 | import { CommandModule } from "yargs";
5 | import chalk from "chalk";
6 |
7 | import { generateLocal, getDirs } from "../utils";
8 | import { generate } from "../generate";
9 |
10 | const cmd: CommandModule = {
11 | command: "init",
12 | aliases: ["initialize", "initialise"],
13 | describe: "Creates a new Hathora project",
14 | async handler() {
15 | const { rootDir, serverDir } = getDirs();
16 |
17 | if (existsSync(join(serverDir, "impl.ts"))) {
18 | console.error(
19 | `${chalk.red("Cannot init inside existing project, delete ")}` +
20 | `${chalk.blue.underline("impl.ts")}` +
21 | `${chalk.red(" to regenerate")}`
22 | );
23 | } else {
24 | generate(rootDir, "bootstrap");
25 | await generateLocal();
26 | }
27 | },
28 | };
29 |
30 | module.exports = cmd;
31 |
--------------------------------------------------------------------------------
/src/commands/install.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 |
3 | import { install } from "../utils";
4 |
5 | const cmd: CommandModule = {
6 | command: "install",
7 | aliases: ["i"],
8 | describe: "Install hathora dependencies",
9 | builder: { only: { choices: ["client", "server"] } },
10 | handler(argv) {
11 | install(argv.only as "server" | "client" | undefined);
12 | },
13 | };
14 |
15 | module.exports = cmd;
16 |
--------------------------------------------------------------------------------
/src/commands/save.ts:
--------------------------------------------------------------------------------
1 | import { join } from "path";
2 |
3 | import { CommandModule } from "yargs";
4 | import { copySync } from "fs-extra";
5 |
6 | import { getDirs } from "../utils";
7 |
8 | const cmd: CommandModule = {
9 | command: "save ",
10 | aliases: ["gamesave", "sv"],
11 | describe: "Creates a named save game from a specific state id",
12 | handler(argv) {
13 | const { rootDir } = getDirs();
14 | copySync(join(rootDir, "data", argv.stateId as string), join(rootDir, "data", "saves", argv.saveName as string));
15 | },
16 | };
17 |
18 | module.exports = cmd;
19 |
--------------------------------------------------------------------------------
/src/commands/start.ts:
--------------------------------------------------------------------------------
1 | import { CommandModule } from "yargs";
2 |
3 | import { start } from "../utils";
4 |
5 | const cmd: CommandModule = {
6 | command: "start",
7 | aliases: ["up", "s"],
8 | describe: "Starts the Hathora server",
9 | builder: { only: { choices: ["client", "server"] } },
10 | handler(argv) {
11 | start(argv.only as "server" | "client" | undefined);
12 | },
13 | };
14 |
15 | module.exports = cmd;
16 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import Handlebars from "handlebars";
2 |
3 | Handlebars.registerHelper("concat", (...arr) => arr.splice(0, arr.length - 1).join(""));
4 | Handlebars.registerHelper("eq", (a, b) => a === b);
5 | Handlebars.registerHelper("ne", (a, b) => a !== b);
6 | Handlebars.registerHelper("stringify", JSON.stringify);
7 | Handlebars.registerHelper("len", (x) => (Array.isArray(x) ? x.length : Object.keys(x).length));
8 | Handlebars.registerHelper("isArray", Array.isArray);
9 | Handlebars.registerHelper("isObject", (x) => typeof x === "object");
10 | Handlebars.registerHelper("isEmpty", (x) => typeof x === "object" && Object.keys(x).length === 0);
11 | Handlebars.registerHelper("capitalize", capitalize);
12 | Handlebars.registerHelper("uppercase", (x) =>
13 | x
14 | .split(/(?=[A-Z])/)
15 | .join("_")
16 | .toUpperCase()
17 | );
18 | Handlebars.registerHelper("makeRequestName", (x) => "I" + capitalize(x) + "Request");
19 | Handlebars.registerHelper("makePluginName", (x) => x.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase() + "-plugin");
20 | // eslint-disable-next-line @typescript-eslint/no-var-requires
21 | Handlebars.registerHelper("hathoraVersion", () => require("../package.json").version);
22 |
23 | function capitalize(s: string) {
24 | return s.charAt(0).toUpperCase() + s.slice(1);
25 | }
26 |
--------------------------------------------------------------------------------
/templates/base/Dockerfile.hbs:
--------------------------------------------------------------------------------
1 | FROM node:18.18-alpine as build
2 |
3 | WORKDIR /app
4 |
5 | RUN apk add --update git
6 |
7 | COPY . .
8 |
9 | RUN npm i -g hathora@{{ hathoraVersion }}
10 | RUN npx hathora build --only server
11 |
12 | FROM node:18.18-alpine
13 |
14 | WORKDIR /app
15 |
16 | # https://github.com/uNetworking/uWebSockets.js/discussions/346#discussioncomment-1137301
17 | RUN apk add --no-cache libc6-compat
18 | RUN ln -s /lib/libc.musl-x86_64.so.1 /lib/ld-linux-x86-64.so.2
19 |
20 | COPY --from=build /app/server/dist server/dist
21 |
22 | ENV NODE_ENV=production
23 | ENV DATA_DIR=/app/data
24 |
25 | CMD ["node", "server/dist/index.mjs"]
26 |
--------------------------------------------------------------------------------
/templates/base/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "axios": "0.24.0",
5 | "bin-serde": "1.2.2"
6 | },
7 | "devDependencies": {
8 | "@types/node": "18.7.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/base/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/base/client/.hathora/failures.ts:
--------------------------------------------------------------------------------
1 | export enum ConnectionFailureType {
2 | STATE_NOT_FOUND = "STATE_NOT_FOUND",
3 | NO_AVAILABLE_STORES = "NO_AVAILABLE_STORES",
4 | INVALID_USER_DATA = "INVALID_USER_DATA",
5 | INVALID_STATE_ID = "INVALID_STATE_ID",
6 | GENERIC_FAILURE = "GENERIC_FAILURE",
7 | }
8 |
9 | export interface ConnectionFailure {
10 | type: ConnectionFailureType,
11 | message: string;
12 | }
13 |
14 | export const transformCoordinatorFailure = (e: {code: number, reason: string}): ConnectionFailure => {
15 | return {
16 | message: e.reason,
17 | type: (function(code) {
18 | switch (code) {
19 | case 4000:
20 | return ConnectionFailureType.STATE_NOT_FOUND;
21 | case 4001:
22 | return ConnectionFailureType.NO_AVAILABLE_STORES;
23 | case 4002:
24 | return ConnectionFailureType.INVALID_USER_DATA;
25 | case 4003:
26 | return ConnectionFailureType.INVALID_STATE_ID;
27 | default:
28 | return ConnectionFailureType.GENERIC_FAILURE;
29 | }
30 | })(e.code)
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/templates/base/client/.hathora/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "@hathora/client-sdk": "^1.1.0",
5 | "bin-serde": "1.2.2",
6 | "get-random-values": "1.2.2",
7 | "isomorphic-ws": "4.0.1",
8 | "net": "1.0.2",
9 | "ws": "8.3.0"
10 | },
11 | "devDependencies": {
12 | "@types/varint": "6.0.0",
13 | "@types/ws": "8.2.2",
14 | "typescript": "4.5.4"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/templates/base/client/.hathora/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/context.ts.hbs:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { UserData } from "../../api/base";
3 | import { {{userState}} as UserState } from "../../api/types";
4 | import { HathoraConnection } from "../.hathora/client";
5 |
6 | type HathoraContext = {
7 | user: UserData;
8 | connection: HathoraConnection;
9 | state: UserState;
10 | updatedAt: number;
11 | pluginsAsObjects: boolean;
12 | };
13 |
14 | export const HathoraContext = React.createContext(undefined);
15 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/package.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "@headlessui/react": "1.4.3",
5 | "@heroicons/react": "1.0.5",
6 | "react": "17.0.2",
7 | "react-dom": "17.0.2",
8 | {{#if auth.google}}
9 | "react-google-login": "5.2.2",
10 | {{/if}}
11 | "react-router-dom": "6.2.1",
12 | "react-toastify": "8.1.0"
13 | },
14 | "devDependencies": {
15 | "@tailwindcss/forms": "0.4.0",
16 | "@types/react": "17.0.37",
17 | "@types/react-dom": "17.0.11",
18 | "@types/react-router-dom": "5.3.2",
19 | "autoprefixer": "10.4.2",
20 | "postcss": "8.4.5",
21 | "tailwindcss": "3.0.18",
22 | "typescript": "4.5.4",
23 | "vite": "2.9.9"
24 | },
25 | "browserslist": [
26 | "Chrome > 50"
27 | ],
28 | "scripts": {
29 | "start": "vite",
30 | "build": "vite build"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require("tailwindcss")(require("./tailwind.config.cjs")), require("autoprefixer")],
3 | };
4 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/public/hathora-hammer-logo-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hathora/builder/a4182519ea1e4381def6484ba46a09a16f930769/templates/base/client/prototype-ui/public/hathora-hammer-logo-light.png
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hathora/builder/a4182519ea1e4381def6484ba46a09a16f930769/templates/base/client/prototype-ui/public/logo.png
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/styles.css:
--------------------------------------------------------------------------------
1 | #app {
2 | display: flex;
3 | justify-content: space-between;
4 | }
5 |
6 | .state-display > div {
7 | width: 100%;
8 | max-width: none;
9 | }
10 |
11 | .array-max-height {
12 | max-height: 300px;
13 | }
14 |
15 | .array-item {
16 | max-width: 375px;
17 | min-width: 64px;
18 | margin-right: 6px;
19 | }
20 |
21 | .array-item-object {
22 | margin-right: 6px;
23 | }
24 |
25 | .kv-display {
26 | padding-bottom: 2px;
27 | padding-right: 5px;
28 | white-space: nowrap;
29 | }
30 |
31 | .object-display > div {
32 | margin-left: 8px;
33 | }
34 |
35 | .array-display > div {
36 | margin-left: 12px;
37 | }
38 |
39 | .enum-display {
40 | color: #66B9A0;
41 | }
42 |
43 | .boolean-display {
44 | color: #e7701a;
45 | }
46 |
47 | .string-display {
48 | color: #B399EA;
49 | }
50 |
51 | .int-display {
52 | color: #e7701a;
53 | }
54 |
55 | .float-display {
56 | color: #e7701a;
57 | }
58 |
59 | .user-display {
60 | color: #329afa;
61 | }
62 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true,
9 | "experimentalDecorators": true,
10 | "jsx": "react"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/templates/base/client/prototype-ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | export default defineConfig({
4 | build: { target: "esnext" },
5 | define: {
6 | "process.env": {
7 | COORDINATOR_HOST: process.env.COORDINATOR_HOST,
8 | MATCHMAKER_HOST: process.env.MATCHMAKER_HOST,
9 | },
10 | },
11 | server: { host: "0.0.0.0" },
12 | clearScreen: false,
13 | });
14 |
--------------------------------------------------------------------------------
/templates/base/server/.hathora/methods.ts.hbs:
--------------------------------------------------------------------------------
1 | import { Options } from "on-change";
2 | import { Chance } from "chance";
3 | import { RoomId } from "@hathora/server-sdk";
4 | import { Response } from "../../api/base";
5 | import {
6 | UserId,
7 | {{userState}} as UserState,
8 | IInitializeRequest,
9 | {{#each methods}}
10 | {{makeRequestName @key}},
11 | {{/each}}
12 | HathoraEventTypes,
13 | HathoraEventPayloads,
14 | } from "../../api/types";
15 |
16 | export interface Context {
17 | roomId: RoomId
18 | chance: ReturnType;
19 | time: number;
20 | sendEvent: (
21 | event: EventType,
22 | data: HathoraEventPayloads[EventType],
23 | to: UserId
24 | ) => void;
25 | broadcastEvent: (
26 | event: EventType,
27 | data: HathoraEventPayloads[EventType]
28 | ) => void;
29 | }
30 | export interface Methods {
31 | stateChangeOptions?: Options;
32 | initialize(ctx: Context, request: IInitializeRequest): T;
33 | {{#each methods}}
34 | {{@key}}(state: T, userId: UserId, ctx: Context, request: {{makeRequestName @key}}): Response;
35 | {{/each}}
36 | getUserState(state: T, userId: UserId): UserState;
37 | {{#if tick}}
38 | onTick(state: T, ctx: Context, timeDelta: number): void;
39 | {{/if}}
40 | }
41 |
--------------------------------------------------------------------------------
/templates/base/server/.hathora/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "bin-serde": "1.2.2",
5 | "chance": "1.1.8",
6 | "@hathora/log-store": "^0.0.3",
7 | "@hathora/server-sdk": "^1.1.0",
8 | "on-change": "4.0.1",
9 | "uuid": "8.3.2"
10 | },
11 | "devDependencies": {
12 | "@types/chance": "1.1.3",
13 | "@types/node": "17.0.12",
14 | "@types/uuid": "8.3.4",
15 | "@types/varint": "6.0.0",
16 | "typescript": "4.5.5"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/templates/base/server/.hathora/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true,
9 | "resolveJsonModule": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/templates/bootstrap/.gitignore.hbs:
--------------------------------------------------------------------------------
1 | .hathora
2 | node_modules
3 | dist
4 | .env
5 | /api
6 | /data/*
7 | !/data/saves
8 | /client/prototype-ui/*
9 | !/client/prototype-ui/plugins
10 |
--------------------------------------------------------------------------------
/templates/bootstrap/server/impl.ts.hbs:
--------------------------------------------------------------------------------
1 | import { Methods, Context } from "./.hathora/methods";
2 | import { Response } from "../api/base";
3 | import {
4 | {{#each types}}
5 | {{@key}},
6 | {{/each}}
7 | IInitializeRequest,
8 | {{#each methods}}
9 | {{makeRequestName @key}},
10 | {{/each}}
11 | } from "../api/types";
12 |
13 | type InternalState = {{userState}};
14 |
15 | export class Impl implements Methods {
16 | initialize(ctx: Context, request: IInitializeRequest): InternalState {
17 | {{#with (lookup types userState)}}
18 | {{#if (eq type "object")}}
19 | return {
20 | {{#each properties}}
21 | {{@key}}: {{> renderDefault}},
22 | {{/each}}
23 | };
24 | {{else}}
25 | return {{> renderDefault}};
26 | {{/if}}
27 | {{/with}}
28 | }
29 | {{#each methods}}
30 | {{@key}}(state: InternalState, userId: UserId, ctx: Context, request: {{makeRequestName @key}}): Response {
31 | return Response.error("Not implemented");
32 | }
33 | {{/each}}
34 | getUserState(state: InternalState, userId: UserId): {{userState}} {
35 | return state;
36 | }
37 | {{#if tick}}
38 | onTick(state: InternalState, ctx: Context, timeDelta: number): void {}
39 | {{/if}}
40 | }
41 | {{#*inline "renderDefault"}}
42 | {{#if (eq type "array")}}
43 | []
44 | {{~else if (eq type "int")}}
45 | 0
46 | {{~else if (eq type "float")}}
47 | 0.0
48 | {{~else if (eq type "string")}}
49 | ""
50 | {{~else if (eq type "enum")}}
51 | 0
52 | {{~else if (eq type "boolean")}}
53 | false
54 | {{~else if (eq type "optional")}}
55 | undefined
56 | {{~else if (eq type "object")}}
57 | {{typeString}}.default()
58 | {{~else if (eq type "union")}}
59 | {{#each options}}
60 | {{#if @first}}
61 | { type: "{{@key}}", val: {{> renderDefault}} }
62 | {{~/if}}
63 | {{/each}}
64 | {{~else if (eq type "plugin")}}
65 | {{> renderDefault item}}
66 | {{/if}}
67 | {{/inline}}
68 |
--------------------------------------------------------------------------------
/templates/bootstrap/server/package.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "name": "{{appName}}-server",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "devDependencies": {
6 | "typescript": "^4.5.2"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/templates/bootstrap/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/client/phaser/client/{{name}}/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hathora Phaser Template
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/templates/client/phaser/client/{{name}}/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "interpolation-buffer": "^1.2.5",
5 | "phaser": "^3.55.2"
6 | },
7 | "devDependencies": {
8 | "@types/node": "^18.0.0",
9 | "typescript": "^4.3.4",
10 | "vite": "^2.9.10"
11 | },
12 | "scripts": {
13 | "start": "vite",
14 | "build": "vite build"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/templates/client/phaser/client/{{name}}/src/app.ts:
--------------------------------------------------------------------------------
1 | import Phaser from "phaser";
2 |
3 | import { HathoraClient, HathoraConnection, StateId } from "../../.hathora/client";
4 |
5 | const client = new HathoraClient();
6 |
7 | export class GameScene extends Phaser.Scene {
8 | private connection!: HathoraConnection;
9 |
10 | constructor() {
11 | super("game");
12 | }
13 |
14 | preload() {}
15 |
16 | init() {
17 | getToken().then(async (token) => {
18 | const stateId = await getStateId(token);
19 | this.connection = await client.connect(
20 | token,
21 | stateId,
22 | ({ state, updatedAt }) => {
23 | console.log("State update", state, updatedAt);
24 | },
25 | (err) => console.error("Error occured", err.message)
26 | );
27 | await this.connection.joinGame({});
28 | });
29 | }
30 |
31 | create() {
32 | this.add.text(this.scale.width / 2, this.scale.height / 2, "Hello, World!").setOrigin(0.5);
33 | }
34 |
35 | update() {}
36 | }
37 |
38 | async function getToken(): Promise {
39 | const storedToken = sessionStorage.getItem(client.appId);
40 | if (storedToken !== null) {
41 | return storedToken;
42 | }
43 | const token = await client.loginAnonymous();
44 | sessionStorage.setItem(client.appId, token);
45 | return token;
46 | }
47 |
48 | async function getStateId(token: string): Promise {
49 | if (location.pathname.length > 1) {
50 | return location.pathname.split("/").pop()!;
51 | }
52 | const stateId = await client.create(token, {});
53 | history.pushState({}, "", `/${stateId}`);
54 | return stateId;
55 | }
56 |
57 | new Phaser.Game({
58 | type: Phaser.AUTO,
59 | width: 800,
60 | height: 600,
61 | scene: [GameScene],
62 | parent: "phaser-container",
63 | });
64 |
--------------------------------------------------------------------------------
/templates/client/phaser/client/{{name}}/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/client/phaser/client/{{name}}/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | export default defineConfig({
4 | build: { target: "esnext" },
5 | publicDir: "src/assets",
6 | define: {
7 | "process.env": {
8 | COORDINATOR_HOST: process.env.COORDINATOR_HOST,
9 | MATCHMAKER_HOST: process.env.MATCHMAKER_HOST,
10 | },
11 | },
12 | server: { host: "0.0.0.0" },
13 | clearScreen: false,
14 | });
15 |
--------------------------------------------------------------------------------
/templates/plugin/lit/client/prototype-ui/plugins/{{val}}/index.ts.hbs:
--------------------------------------------------------------------------------
1 | import { LitElement, html } from "lit";
2 | import { property } from "lit/decorators.js";
3 | import { {{val}}, {{userState}} } from "../../../../api/types";
4 | import { HathoraConnection } from "../../../.hathora/client";
5 |
6 | export default class {{val}}Plugin extends LitElement {
7 | @property() val!: {{val}};
8 | @property() state!: {{userState}};
9 | @property() client!: HathoraConnection;
10 |
11 | render() {
12 | return html`Hello world!`;
13 | }
14 |
15 | firstUpdated() {}
16 |
17 | updated() {}
18 | }
19 |
--------------------------------------------------------------------------------
/templates/plugin/lit/client/prototype-ui/plugins/{{val}}/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "lit": "^2.0.2"
5 | },
6 | "devDependencies": {
7 | "typescript": "^4.5.2"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/templates/plugin/lit/client/prototype-ui/plugins/{{val}}/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true,
9 | "experimentalDecorators": true,
10 | "useDefineForClassFields": false
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/templates/plugin/native/client/prototype-ui/plugins/{{val}}/index.ts.hbs:
--------------------------------------------------------------------------------
1 | import { {{val}}, {{userState}} } from "../../../../api/types";
2 | import { HathoraConnection } from "../../../.hathora/client";
3 |
4 | export default class {{val}}Plugin extends HTMLElement {
5 | val!: {{val}};
6 | state!: {{userState}};
7 | client!: HathoraConnection;
8 |
9 | constructor() {
10 | super();
11 | this.attachShadow({ mode: "open" });
12 | }
13 |
14 | connectedCallback() {}
15 | }
16 |
--------------------------------------------------------------------------------
/templates/plugin/native/client/prototype-ui/plugins/{{val}}/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "devDependencies": {
4 | "typescript": "^4.5.2"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/templates/plugin/native/client/prototype-ui/plugins/{{val}}/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/templates/plugin/react/client/prototype-ui/plugins/{{val}}/index.tsx.hbs:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDom from "react-dom";
3 | //@ts-ignore
4 | import reactToWebComponent from "react-to-webcomponent";
5 | import { {{val}}, {{userState}} } from "../../../../api/types";
6 | import { HathoraConnection } from "../../../.hathora/client";
7 |
8 | function {{val}}Component({ val, state, client }: { val: {{val}}; state: {{userState}}; client: HathoraConnection }) {
9 | return <>Hello world!>;
10 | }
11 |
12 | export default reactToWebComponent({{val}}Component, React, ReactDom);
13 |
--------------------------------------------------------------------------------
/templates/plugin/react/client/prototype-ui/plugins/{{val}}/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "dependencies": {
4 | "react": "^17.0.2",
5 | "react-dom": "^17.0.2",
6 | "react-to-webcomponent": "^1.5.1"
7 | },
8 | "devDependencies": {
9 | "@types/react": "^17.0.37",
10 | "@types/react-dom": "^17.0.11",
11 | "typescript": "^4.5.4"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/templates/plugin/react/client/prototype-ui/plugins/{{val}}/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "esnext",
5 | "strict": true,
6 | "target": "esnext",
7 | "moduleResolution": "node",
8 | "isolatedModules": true,
9 | "jsx": "react"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true,
4 | "module": "commonjs",
5 | "strict": true,
6 | "target": "esnext",
7 | "outDir": "lib"
8 | },
9 | "include": ["src"]
10 | }
11 |
--------------------------------------------------------------------------------