diff --git a/src/components/DoomCanvas/DoomCanvas.tsx b/src/components/DoomCanvas/DoomCanvas.tsx
index 9fc492c6..0d3176ab 100644
--- a/src/components/DoomCanvas/DoomCanvas.tsx
+++ b/src/components/DoomCanvas/DoomCanvas.tsx
@@ -3,9 +3,8 @@ import { EGameType, EmscriptenModule, NewGameResponse } from "../../types";
import { useAppContext } from "../../context/useAppContext";
import { HydraMultiplayer } from "../../utils/hydra-multiplayer";
import useKeys from "../../hooks/useKeys";
-import { getArgs } from "../../utils/game";
+import { getArgs, getBaseUrl } from "../../utils/game";
import { useMutation } from "@tanstack/react-query";
-import { SERVER_URL } from "../../constants";
import Card from "../Card";
import { FaRegCircleCheck } from "react-icons/fa6";
import { MdContentCopy } from "react-icons/md";
@@ -16,17 +15,19 @@ const DoomCanvas: React.FC = () => {
const isEffectRan = useRef(false);
const {
gameData: { code, petName, type },
+ region,
} = useAppContext();
const keys = useKeys();
const urlClipboard = useClipboard({ copiedTimeout: 1500 });
+ const baseUrl = getBaseUrl(region);
const { mutate: fetchGameData, data } = useMutation({
mutationKey: ["fetchGameData", keys.address, code, type],
mutationFn: async () => {
const url =
type === EGameType.HOST
- ? `${SERVER_URL}new_game?address=${keys.address}`
- : `${SERVER_URL}add_player?address=${keys.address}&id=${code}`;
+ ? `${baseUrl}/new_game?address=${keys.address}`
+ : `${baseUrl}/add_player?address=${keys.address}&id=${code}`;
const response = await fetch(url);
return response.json();
},
@@ -42,10 +43,10 @@ const DoomCanvas: React.FC = () => {
);
useEffect(() => {
- if (!keys.address) return;
+ if (!keys.address || !region) return;
fetchGameData();
- }, [fetchGameData, keys.address]);
+ }, [fetchGameData, keys.address, region]);
useEffect(() => {
if (!keys.address || !data?.ip) return;
diff --git a/src/components/InitialView/InitialView.tsx b/src/components/InitialView/InitialView.tsx
index 39914e3f..bd4657bf 100644
--- a/src/components/InitialView/InitialView.tsx
+++ b/src/components/InitialView/InitialView.tsx
@@ -2,7 +2,6 @@ import { FC, useEffect, useState } from "react";
import Button from "../Button";
import hydraText from "../../assets/images/hydra-text.png";
import Modal from "../Modal";
-import SelectContinentDialog from "../SelectContinentDialog";
import Layout from "../Layout";
import SetNameModal from "../SetNameModal";
import { useAppContext } from "../../context/useAppContext";
@@ -25,8 +24,6 @@ const InitialView: FC = ({ startGame }) => {
const [isNameModalOpen, setIsNameModalOpen] = useState(
pathSegments[0] === EGameType.JOIN,
);
- const [isSelectContinentModalOpen, setIsSelectContinentModalOpen] =
- useState(false);
const code = pathSegments[1];
useEffect(() => {
@@ -111,11 +108,6 @@ const InitialView: FC = ({ startGame }) => {
- setIsSelectContinentModalOpen(false)}
- isOpen={isSelectContinentModalOpen}
- startGame={startGame}
- />
setIsNameModalOpen(false)}
isOpen={isNameModalOpen}
diff --git a/src/components/SelectContinentDialog/SelectContinentDialog.tsx b/src/components/SelectContinentDialog/SelectContinentDialog.tsx
deleted file mode 100644
index e4504937..00000000
--- a/src/components/SelectContinentDialog/SelectContinentDialog.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { ChangeEventHandler, FC } from "react";
-import Modal from "../Modal";
-import Button from "../Button";
-import { REGIONS } from "../../constants";
-import { useAppContext } from "../../context/useAppContext";
-
-interface SelectContinentDialogProps {
- close: () => void;
- isOpen: boolean;
- startGame: () => void;
-}
-
-const SelectContinentDialog: FC = ({
- close,
- isOpen,
- startGame,
-}) => {
- const { region, setRegion } = useAppContext();
-
- const handleChange: ChangeEventHandler = (event) => {
- setRegion(REGIONS.find((r) => r.value === event.target.value)!);
- };
-
- return (
-
- Select your continent
-
-
- );
-};
-
-export default SelectContinentDialog;
diff --git a/src/components/SelectContinentDialog/index.ts b/src/components/SelectContinentDialog/index.ts
deleted file mode 100644
index f83ca1c1..00000000
--- a/src/components/SelectContinentDialog/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./SelectContinentDialog";
diff --git a/src/constants.ts b/src/constants.ts
index c91ec163..0230ff40 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -4,14 +4,5 @@ export const HANDLE_CACHE_KEY = "player-handle-cache";
export const HYDRA_DOOM_SESSION_KEY = "hydra-doom-session-key";
export const MAX_SPEED = 40;
export const REGION = import.meta.env.VITE_REGION;
-export const REGIONS = [
- { name: "Ohio, NA", value: "us-east-2" },
- { name: "Oregon, NA", value: "us-west-2" },
- { name: "Frankfurt, Europe", value: "eu-central-1" },
- { name: "Cape Town, Africa", value: "af-south-1" },
- { name: "Melbourne, Australia", value: "ap-southeast-4" },
- { name: "Seoul, Asia", value: "ap-northeast-2" },
- { name: "Sao Paulo, SA", value: "sa-east-1" },
-];
-export const SERVER_URL = import.meta.env.VITE_SERVER_URL;
+export const REGIONS = ["us-east-1"];
export const TIC_RATE_MAGIC = 35; // 35 is the ticrate in DOOM WASM they use to calculate time.
diff --git a/src/context/AppContextProvider.tsx b/src/context/AppContextProvider.tsx
index 7ab42a5f..6a735dcf 100644
--- a/src/context/AppContextProvider.tsx
+++ b/src/context/AppContextProvider.tsx
@@ -1,25 +1,24 @@
import { FC, PropsWithChildren, useMemo, useState } from "react";
import { AppContext } from "./useAppContext";
+import { EGameType } from "../types";
+import useBestRegion from "../hooks/useBestRegion";
import { REGIONS } from "../constants";
-import { EGameType, Region } from "../types";
const AppContextProvider: FC = ({ children }) => {
+ const { bestRegion } = useBestRegion(REGIONS);
const [gameData, setGameData] = useState({
code: "",
petName: "",
type: EGameType.SOLO,
});
- const [region, setRegion] = useState(REGIONS[0]);
-
const value = useMemo(
() => ({
gameData,
- region,
+ region: bestRegion,
setGameData,
- setRegion,
}),
- [gameData, region],
+ [gameData, bestRegion],
);
return {children};
diff --git a/src/context/useAppContext.ts b/src/context/useAppContext.ts
index a37d30ee..de53e3fc 100644
--- a/src/context/useAppContext.ts
+++ b/src/context/useAppContext.ts
@@ -1,19 +1,16 @@
import { createContext, Dispatch, useContext } from "react";
-import { EGameType, GameData, Region } from "../types";
-import { REGIONS } from "../constants";
+import { EGameType, GameData } from "../types";
interface AppContextInterface {
gameData: GameData;
- region: Region;
+ region: string | null;
setGameData: Dispatch>;
- setRegion: Dispatch>;
}
export const AppContext = createContext({
gameData: { petName: "", code: "", type: EGameType.SOLO },
- region: REGIONS[0],
+ region: null,
setGameData: () => {},
- setRegion: () => {},
});
export const useAppContext = () => {
diff --git a/src/hooks/useBestRegion.ts b/src/hooks/useBestRegion.ts
new file mode 100644
index 00000000..c8c07055
--- /dev/null
+++ b/src/hooks/useBestRegion.ts
@@ -0,0 +1,68 @@
+import { useState, useEffect } from "react";
+import { useQueries } from "@tanstack/react-query";
+
+interface ServerHealth {
+ region: string;
+ latency: number;
+ error?: string;
+}
+
+const useBestRegion = (regions: string[]) => {
+ const [bestRegion, setBestRegion] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isError, setIsError] = useState(false);
+
+ const checkServerHealth = async (region: string): Promise => {
+ const url = `http://api.${region}.hydra-doom.sundae.fi/health`;
+
+ try {
+ const start = performance.now();
+ const response = await fetch(url, { method: "HEAD" });
+ const end = performance.now();
+
+ if (!response.ok) throw new Error("Server not healthy");
+
+ return {
+ region,
+ latency: end - start,
+ };
+ } catch (error: any) {
+ return {
+ region,
+ latency: Infinity,
+ error: error.message,
+ };
+ }
+ };
+
+ const results = useQueries({
+ queries: regions.map((region) => ({
+ queryKey: ["serverHealth", region],
+ queryFn: () => checkServerHealth(region),
+ staleTime: Infinity,
+ cacheTime: 0,
+ })),
+ });
+
+ useEffect(() => {
+ if (results.every((result) => result.isSuccess || result.isError)) {
+ const successfulResults = results
+ .filter((result) => result.isSuccess && result.data)
+ .map((result) => result.data as ServerHealth);
+
+ if (successfulResults.length > 0) {
+ const sortedByLatency = successfulResults.sort(
+ (a, b) => a.latency - b.latency,
+ );
+ setBestRegion(sortedByLatency[0].region);
+ } else {
+ setIsError(true);
+ }
+ setIsLoading(false);
+ }
+ }, [results]);
+
+ return { bestRegion, isLoading, isError };
+};
+
+export default useBestRegion;
diff --git a/src/types.ts b/src/types.ts
index a2bae381..36a808b6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -34,11 +34,6 @@ export interface NewGameResponse {
player_state: string;
}
-export interface Region {
- name: string;
- value: string;
-}
-
interface FileSystem {
createPreloadedFile(
parent: string,
diff --git a/src/utils/game.ts b/src/utils/game.ts
index 39591637..c2901cd7 100644
--- a/src/utils/game.ts
+++ b/src/utils/game.ts
@@ -16,3 +16,8 @@ export const getArgs = ({ type, code, petName }: GameData) => {
return args;
};
+
+export const getBaseUrl = (region: string | null, local = false) => {
+ if (local) return "http://localhost:3000";
+ return `http://api.${region}.hydra-doom.sundae.fi`;
+};