1
0

Building but users still not listing

This commit is contained in:
James Ketr 2025-10-07 18:21:09 -07:00
parent 720c0aa143
commit 81d366286a
15 changed files with 296 additions and 309 deletions

View File

@ -87,7 +87,7 @@ const Activity: React.FC<ActivityProps> = ({ keep, activity }) => {
};
const Activities: React.FC = () => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [activities, setActivities] = useState<ActivityData[]>([]);
const [turn, setTurn] = useState<TurnData | undefined>(undefined);
const [color, setColor] = useState<string | undefined>(undefined);
@ -103,15 +103,16 @@ const Activities: React.FC = () => {
} else {
request = fields;
}
ws?.send(
JSON.stringify({
type: "get",
fields: request,
})
);
sendJsonMessage({
type: "get",
fields: request,
});
};
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data) as { type: string; update?: Record<string, unknown> };
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update": {
const ignoring: string[] = [],
@ -153,21 +154,8 @@ const Activities: React.FC = () => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage as EventListener);
return () => {
ws.removeEventListener("message", cbMessage as EventListener);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, activities, turn, players, timestamp, color, state, fields]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -3,7 +3,7 @@ import Paper from "@mui/material/Paper";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemText from "@mui/material/ListItemText";
import { formatDistanceToNow, formatDuration, intervalToDuration } from 'date-fns';
import { formatDistanceToNow, formatDuration, intervalToDuration } from "date-fns";
import TextField from "@mui/material/TextField";
import equal from "fast-deep-equal";
@ -34,10 +34,13 @@ const Chat: React.FC = () => {
return () => clearInterval(timer);
}, []);
const { ws, name, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, name, sendJsonMessage } = useContext(GlobalContext);
const fields = useMemo(() => ["chat", "startTime"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`chat - game update`);
@ -52,30 +55,13 @@ const Chat: React.FC = () => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
}, [lastJsonMessage, chat, startTime]);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
useEffect(() => {
if (!ws) {
return;
}
sendJsonMessage({
type: "get",
fields,
});
}, [ws, fields, sendJsonMessage]);
}, [fields, sendJsonMessage]);
const chatKeyPress = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
@ -84,13 +70,11 @@ const Chat: React.FC = () => {
setAutoScroll(true);
}
if (ws) {
sendJsonMessage({ type: "chat", message: (event.target as HTMLInputElement).value });
(event.target as HTMLInputElement).value = "";
}
sendJsonMessage({ type: "chat", message: (event.target as HTMLInputElement).value });
(event.target as HTMLInputElement).value = "";
}
},
[ws, setAutoScroll, autoScroll]
[setAutoScroll, autoScroll]
);
const chatScroll = (event: React.UIEvent<HTMLUListElement>) => {
@ -217,9 +201,7 @@ const Chat: React.FC = () => {
{item.color && <PlayerColor color={item.color} />}
<ListItemText
primary={message}
secondary={
item.color && formatDistanceToNow(new Date(item.date > now ? now : item.date))
}
secondary={item.color && formatDistanceToNow(new Date(item.date > now ? now : item.date))}
/>
</ListItem>
);
@ -248,7 +230,7 @@ const Chat: React.FC = () => {
const hours = duration.hours || 0;
const minutes = duration.minutes || 0;
const seconds = duration.seconds || 0;
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
})()}
</>
)

View File

@ -12,15 +12,19 @@ import { GlobalContext } from "./GlobalContext";
/* eslint-disable @typescript-eslint/no-explicit-any */
const ChooseCard: React.FC = () => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [turn, setTurn] = useState<any>(undefined);
const [color, setColor] = useState<string | undefined>(undefined);
const [state, setState] = useState<string | undefined>(undefined);
const [cards, setCards] = useState<string[]>([]);
const fields = useMemo(() => ["turn", "color", "state"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`choose-card - game-update: `, data.update);
@ -37,21 +41,7 @@ const ChooseCard: React.FC = () => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, turn, color, state]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -21,13 +21,13 @@ interface PlayerItem {
}
const GameOrder: React.FC = () => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [players, setPlayers] = useState<{ [key: string]: any }>({});
const [color, setColor] = useState<string | undefined>(undefined);
const fields = useMemo(() => ["players", "color"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`GameOrder game-update: `, data.update);
@ -41,21 +41,7 @@ const GameOrder: React.FC = () => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, players, color]);
useEffect(() => {
if (!sendJsonMessage) {
return;
@ -66,12 +52,8 @@ const GameOrder: React.FC = () => {
});
}, [sendJsonMessage, fields]);
const sendMessage = (data: any) => {
ws!.send(JSON.stringify(data));
};
const rollClick = () => {
sendMessage({ type: "roll" });
sendJsonMessage({ type: "roll" });
};
let hasRolled = true;

View File

@ -34,7 +34,7 @@ interface HandProps {
}
const Hand: React.FC<HandProps> = ({ buildActive, setBuildActive, setCardActive }) => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [priv, setPriv] = useState<any>(undefined);
const [color, setColor] = useState<string | undefined>(undefined);
const [turn, setTurn] = useState<any>(undefined);
@ -49,8 +49,12 @@ const Hand: React.FC<HandProps> = ({ buildActive, setBuildActive, setCardActive
() => ["private", "turn", "color", "longestRoad", "largestArmy", "mostPorts", "mostDeveloped"],
[]
);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`hand - game-update: `, data.update);
@ -79,21 +83,7 @@ const Hand: React.FC<HandProps> = ({ buildActive, setBuildActive, setCardActive
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, priv, turn, color, longestRoad, largestArmy, mostPorts, mostDeveloped]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -247,14 +247,17 @@ interface HouseRulesProps {
}
const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive, setHouseRulesActive }) => {
const { ws, name, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, name, sendJsonMessage } = useContext(GlobalContext);
const [rules, setRules] = useState<any>({});
const [state, setState] = useState<any>({});
const [gameState, setGameState] = useState<string>("");
const fields = useMemo(() => ["state", "rules"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`house-rules - game-update: `, data.update);
@ -268,21 +271,7 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive, setHouseRules
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, rules, gameState]);
useEffect(() => {
if (!sendJsonMessage) {
return;
@ -477,7 +466,7 @@ const HouseRules: React.FC<HouseRulesProps> = ({ houseRulesActive, setHouseRules
),
},
].sort((a, b) => a.category.localeCompare(b.category)),
[rules, setRules, state, ws, setRule, name, gameState]
[rules, setRules, state, setRule, name, gameState]
);
if (!houseRulesActive) {

View File

@ -1123,9 +1123,14 @@ const MediaAgent = (props: MediaAgentProps) => {
if (media && joinStatus.status === "Not joined" && readyState === ReadyState.OPEN) {
console.log(`media-agent - Initiating media join for ${session.name}`);
setJoinStatus({ status: "Joining" });
sendJsonMessage({ type: "join", data: {} });
sendJsonMessage({
type: "join",
data: {
has_media: session.has_media !== false, // Default to true for backward compatibility
}
});
}
}, [media, joinStatus.status, sendJsonMessage, readyState, session.name]);
}, [media, joinStatus.status, sendJsonMessage, readyState, session.name, session.has_media]);
// Update local peer in peers list
useEffect(() => {

View File

@ -1,46 +0,0 @@
import React, { useState, useContext, useEffect, useRef } from "react";
import { GlobalContext } from "./GlobalContext";
import "./PingPong.css";
const PingPong: React.FC = () => {
const [count, setCount] = useState<number>(0);
const global = useContext(GlobalContext);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data as string);
switch (data.type) {
case "ping":
if (global.ws) {
global.ws.send(JSON.stringify({ type: "pong", timestamp: data.ping }));
}
setCount(count + 1);
break;
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!global.ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
global.ws.addEventListener("message", cbMessage);
return () => {
global.ws.removeEventListener("message", cbMessage);
};
}, [global.ws, refWsMessage]);
return (
<div className="PingPong">
Game {global.gameId}: {global.name} {global.ws ? "has socket" : "no socket"} {count} pings
</div>
);
};
export { PingPong };

View File

@ -16,13 +16,7 @@ type PlacardProps = {
};
const Placard: React.FC<PlacardProps> = ({ type, disabled, count, buildActive, setBuildActive, className, sx }) => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const sendMessage = useCallback(
(data: Record<string, unknown>) => {
sendJsonMessage(data);
},
[sendJsonMessage]
);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const dismissClicked = () => {
setBuildActive && setBuildActive(false);
@ -37,19 +31,19 @@ const Placard: React.FC<PlacardProps> = ({ type, disabled, count, buildActive, s
};
const roadClicked = () => {
sendMessage({ type: "buy-road" });
sendJsonMessage({ type: "buy-road" });
setBuildActive && setBuildActive(false);
};
const settlementClicked = () => {
sendMessage({ type: "buy-settlement" });
sendJsonMessage({ type: "buy-settlement" });
setBuildActive && setBuildActive(false);
};
const cityClicked = () => {
sendMessage({ type: "buy-city" });
sendJsonMessage({ type: "buy-city" });
setBuildActive && setBuildActive(false);
};
const developmentClicked = () => {
sendMessage({ type: "buy-development" });
sendJsonMessage({ type: "buy-development" });
setBuildActive && setBuildActive(false);
};

View File

@ -60,38 +60,35 @@ const PlayerList: React.FC = () => {
}
const data: any = lastJsonMessage;
switch (data.type) {
case "players": {
type RoomStateData = {
participants: Player[];
};
const room_state = data as RoomStateData;
console.log(`Players - room_state`, room_state.participants);
room_state.participants.forEach((player) => {
player.local = player.session_id === session.id;
});
room_state.participants.sort(sortPlayers);
setPlayers(room_state.participants);
// Initialize peers with remote mute/video state
setPeers((prevPeers) => {
const updated: Record<string, Peer> = { ...prevPeers };
room_state.participants.forEach((player) => {
// Only update remote peers, never overwrite local peer object
if (!player.local && updated[player.session_id]) {
updated[player.session_id] = {
...updated[player.session_id],
muted: player.muted ?? false,
video_on: player.video_on ?? true,
};
}
case "game-update": {
console.log(`PlayerList - game-update:`, data.update);
// Handle participants list
if ("participants" in data.update && data.update.participants) {
const participantsList: Player[] = data.update.participants;
console.log(`PlayerList - participants:`, participantsList);
participantsList.forEach((player) => {
player.local = player.session_id === session?.id;
});
participantsList.sort(sortPlayers);
setPlayers(participantsList);
// Initialize peers with remote mute/video state
setPeers((prevPeers) => {
const updated: Record<string, Peer> = { ...prevPeers };
participantsList.forEach((player) => {
// Only update remote peers, never overwrite local peer object
if (!player.local && updated[player.session_id]) {
updated[player.session_id] = {
...updated[player.session_id],
muted: player.muted ?? false,
video_on: player.video_on ?? true,
};
}
});
return updated;
});
return updated;
});
break;
}
case "update_name": {
// Update local session name immediately
if (data && typeof data.name === "string") {
session.name = data.name;
}
break;
}
@ -99,12 +96,12 @@ const PlayerList: React.FC = () => {
// Update peer state in peers, but do not override local mute
setPeers((prevPeers) => {
const updated = { ...prevPeers };
const peerId = data.peer_id;
const peerId = data.data?.peer_id || data.peer_id;
if (peerId && updated[peerId]) {
updated[peerId] = {
...updated[peerId],
muted: data.muted,
video_on: data.video_on,
muted: data.data?.muted ?? data.muted,
video_on: data.data?.video_on ?? data.video_on,
};
}
return updated;
@ -117,11 +114,13 @@ const PlayerList: React.FC = () => {
}, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]);
useEffect(() => {
if (players !== null) {
if (players !== null || !sendJsonMessage) {
return;
}
// Request participants list
sendJsonMessage({
type: "list_Players",
type: "get",
fields: ["participants"],
});
}, [players, sendJsonMessage]);

View File

@ -133,7 +133,7 @@ interface PlayersStatusProps {
}
const PlayersStatus: React.FC<PlayersStatusProps> = ({ active }) => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [players, setPlayers] = useState<any>(undefined);
const [color, setColor] = useState<string | undefined>(undefined);
const [largestArmy, setLargestArmy] = useState<string | undefined>(undefined);
@ -141,8 +141,12 @@ const PlayersStatus: React.FC<PlayersStatusProps> = ({ active }) => {
const [mostPorts, setMostPorts] = useState<string | undefined>(undefined);
const [mostDeveloped, setMostDeveloped] = useState<string | undefined>(undefined);
const fields = useMemo(() => ["players", "color", "longestRoad", "largestArmy", "mostPorts", "mostDeveloped"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`players-status - game-update: `, data.update);
@ -168,21 +172,7 @@ const PlayersStatus: React.FC<PlayersStatusProps> = ({ active }) => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, players, color, longestRoad, largestArmy, mostPorts, mostDeveloped]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -11,13 +11,16 @@ import { GlobalContext } from "./GlobalContext";
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
const SelectPlayer: React.FC = () => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [turn, setTurn] = useState<any>(undefined);
const [color, setColor] = useState<string | undefined>(undefined);
const fields = useMemo(() => ["turn", "color"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`select-players - game-update: `, data.update);
@ -31,21 +34,7 @@ const SelectPlayer: React.FC = () => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, turn, color]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -16,13 +16,16 @@ interface ViewCardProps {
}
const ViewCard: React.FC<ViewCardProps> = ({ cardActive, setCardActive }) => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [priv, setPriv] = useState<any>(undefined);
const [turns, setTurns] = useState<number>(0);
const [rules, setRules] = useState<any>({});
const fields = useMemo(() => ["private", "turns", "rules"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data as string);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`view-card - game update`);
@ -39,21 +42,7 @@ const ViewCard: React.FC<ViewCardProps> = ({ cardActive, setCardActive }) => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, priv, turns, rules]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -17,12 +17,16 @@ interface WinnerProps {
/* eslint-disable @typescript-eslint/no-explicit-any */
const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed }) => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
const [winner, setWinner] = useState<any>(undefined);
const [state, setState] = useState<string | undefined>(undefined);
const fields = useMemo(() => ["winner", "state"], []);
const onWsMessage = (event: MessageEvent) => {
const data: { type: string; update: any } = JSON.parse(event.data);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`winner - game update`, data.update);
@ -40,21 +44,7 @@ const Winner: React.FC<WinnerProps> = ({ winnerDismissed, setWinnerDismissed })
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
ws.removeEventListener("message", cbMessage);
};
}, [ws, refWsMessage]);
}, [lastJsonMessage, winner, state, setWinnerDismissed]);
useEffect(() => {
if (!sendJsonMessage) {
return;

View File

@ -1163,6 +1163,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und
});
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
participants: getParticipants(game),
unselected: getFilteredUnselected(game),
chat: game.chat,
});
@ -1282,6 +1283,7 @@ const setPlayerColor = (game: Game, session: Session, color: string): string | u
const update: any = {
players: getFilteredPlayers(game),
participants: getParticipants(game),
chat: game.chat,
};
@ -3407,19 +3409,79 @@ const resetDisconnectCheck = (_game: any, req: any): void => {
//req.disconnectCheck = setTimeout(() => { wsInactive(game, req) }, 20000);
};
const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boolean; hasAudio?: boolean }): void => {
const join = (peers: any, session: any, { hasVideo, hasAudio, has_media }: { hasVideo?: boolean; hasAudio?: boolean; has_media?: boolean }): void => {
const ws = session.ws;
if (!session.name) {
console.error(`${session.id}: <- join - No name set yet. Audio not available.`);
ws.send(JSON.stringify({
type: "join_status",
status: "Error",
message: "No name set yet. Audio not available."
}));
return;
}
console.log(`${session.id}: <- join - ${session.name}`);
console.log(`${all}: -> addPeer - ${session.name}`);
// Determine media capability - prefer has_media if provided, otherwise derive from hasVideo/hasAudio
const peerHasMedia = has_media !== undefined ? has_media : (hasVideo || hasAudio);
if (session.name in peers) {
console.log(`${session.id}:${session.name} - Already joined to Audio.`);
console.log(`${session.id}:${session.name} - Already joined to Audio, updating WebSocket reference.`);
// Update the WebSocket reference in case of reconnection
peers[session.name].ws = ws;
peers[session.name].has_media = peerHasMedia;
peers[session.name].hasAudio = hasAudio;
peers[session.name].hasVideo = hasVideo;
// Send join status to reconnected client
ws.send(JSON.stringify({
type: "join_status",
status: "Joined",
message: "Reconnected"
}));
// Notify the reconnected client about all existing peers
for (const peer in peers) {
if (peer === session.name) continue; // Skip self
ws.send(
JSON.stringify({
type: "addPeer",
data: {
peer_id: peer,
peer_name: peer,
has_media: peers[peer].has_media,
should_create_offer: true,
hasAudio: peers[peer].hasAudio,
hasVideo: peers[peer].hasVideo,
},
})
);
}
// Notify all other peers about the reconnected peer (with updated connection)
for (const peer in peers) {
if (peer === session.name) continue; // Skip self
peers[peer].ws.send(
JSON.stringify({
type: "addPeer",
data: {
peer_id: session.name,
peer_name: session.name,
has_media: peerHasMedia,
should_create_offer: false,
hasAudio,
hasVideo,
},
})
);
}
return;
}
@ -3430,6 +3492,8 @@ const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boo
type: "addPeer",
data: {
peer_id: session.name,
peer_name: session.name,
has_media: peers[session.name]?.has_media ?? peerHasMedia,
should_create_offer: false,
hasAudio,
hasVideo,
@ -3443,6 +3507,8 @@ const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boo
type: "addPeer",
data: {
peer_id: peer,
peer_name: peer,
has_media: peers[peer].has_media,
should_create_offer: true,
hasAudio: peers[peer].hasAudio,
hasVideo: peers[peer].hasVideo,
@ -3456,7 +3522,15 @@ const join = (peers: any, session: any, { hasVideo, hasAudio }: { hasVideo?: boo
ws,
hasAudio,
hasVideo,
has_media: peerHasMedia,
};
/* Send join success status */
ws.send(JSON.stringify({
type: "join_status",
status: "Joined",
message: "Successfully joined"
}));
};
const part = (peers: any, session: any): void => {
@ -3483,13 +3557,19 @@ const part = (peers: any, session: any): void => {
peers[peer].ws.send(
JSON.stringify({
type: "removePeer",
data: { peer_id: session.name },
data: {
peer_id: session.name,
peer_name: session.name
},
})
);
ws.send(
JSON.stringify({
type: "removePeer",
data: { peer_id: session.name },
data: {
peer_id: peer,
peer_name: peer
},
})
);
}
@ -3856,6 +3936,49 @@ const getFilteredPlayers = (game: any): Record<string, any> => {
return filtered;
};
/**
* Get participants list for the game room
* Uses the reusable room helper and adds game-specific data (color)
*
* This demonstrates how to extend the base participant list with app-specific data
*/
const getParticipants = (game: any): any[] => {
// Use the reusable room helper for base participant data
// If you were using the new architecture, this would be:
// import { getParticipants as getBaseParticipants } from './room/helpers';
// const baseParticipants = getBaseParticipants(game.sessions);
const participants: any[] = [];
for (let id in game.sessions) {
const session = game.sessions[id];
if (!session) continue;
// Base participant data (reusable across any application)
const baseParticipant = {
name: session.name || null,
session_id: session.id,
live: session.live || false,
protected: session.protected || false,
has_media: session.has_media !== false,
bot_run_id: session.bot_run_id || null,
bot_provider_id: session.bot_provider_id || null,
bot_instance_id: session.bot_instance_id || null,
muted: session.muted || false,
video_on: session.video_on !== false,
};
// Game-specific data (in metadata layer)
// This is the ONLY game-specific code in this function
const gameSpecific = {
color: session.color || null, // Game-specific: player color
// In the new architecture, this would be: session.metadata?.color
};
participants.push({ ...baseParticipant, ...gameSpecific });
}
return participants;
};
const calculatePoints = (game: any, update: any): void => {
if (game.state === "winner") {
return;
@ -4225,6 +4348,16 @@ router.ws("/ws/:id", async (ws, req) => {
// If there was a previous websocket and it's a different object, try to
// close it to avoid stale sockets lingering in memory.
if (previousWs && previousWs !== ws) {
// Clean up peer from audio registry before replacing WebSocket
if (gameId in audio) {
try {
part(audio[gameId], session);
console.log(`${short}: Cleaned up peer ${session.name} from audio registry during reconnection`);
} catch (e) {
console.warn(`${short}: Error cleaning up peer during reconnection:`, e);
}
}
try {
previousWs.close();
} catch (e) {
@ -4282,7 +4415,11 @@ router.ws("/ws/:id", async (ws, req) => {
message = JSON.stringify({
type: "iceCandidate",
data: { peer_id: getName(session), candidate: candidate },
data: {
peer_id: getName(session),
peer_name: getName(session),
candidate: candidate
},
}) as any;
if (peer_id in audio[gameId]) {
@ -4312,7 +4449,11 @@ router.ws("/ws/:id", async (ws, req) => {
);
message = JSON.stringify({
type: "sessionDescription",
data: { peer_id: getName(session), session_description: session_description },
data: {
peer_id: getName(session),
peer_name: getName(session),
session_description: session_description
},
}) as any;
if (peer_id in audio[gameId]) {
try {
@ -4350,7 +4491,12 @@ router.ws("/ws/:id", async (ws, req) => {
const messagePayload = JSON.stringify({
type: "peer_state_update",
data: { peer_id: session.name, muted, video_on },
data: {
peer_id: session.name,
peer_name: session.name,
muted,
video_on
},
});
// Send to all other peers
@ -4488,6 +4634,9 @@ router.ws("/ws/:id", async (ws, req) => {
case "players":
batchedUpdate.players = getFilteredPlayers(game);
break;
case "participants":
batchedUpdate.participants = getParticipants(game);
break;
case "color":
console.log(`${session.id}: -> Returning color as ${session.color} for ${getName(session)}`);
batchedUpdate.color = session.color;
@ -4771,6 +4920,7 @@ router.ws("/ws/:id", async (ws, req) => {
if (session.name) {
sendUpdateToPlayers(game, {
players: getFilteredPlayers(game),
participants: getParticipants(game),
unselected: getFilteredUnselected(game),
});
}
@ -5326,7 +5476,13 @@ router.get("/", (req, res /*, next*/) => {
// Mark this response as coming from the backend API to aid debugging
res.setHeader("X-Backend", "games");
return res.status(200).send({ player: playerId });
return res.status(200).send({
id: playerId,
player: playerId,
name: null,
lobbies: [],
has_media: true // Default to true for regular users
});
});
router.post("/:id?", async (req, res /*, next*/) => {