-
Notifications
You must be signed in to change notification settings - Fork 128
pull #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pull #81
Changes from all commits
d172354
2ec8a1d
56c368a
a2c2d47
fa97726
ed8e656
076f9ee
0f39af8
879ba22
410dff0
ec56163
3475a80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| # misc | ||
| .DS_Store | ||
| *.pem | ||
| tmpclaude* | ||
| logs | ||
|
|
||
| #visual studio | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,6 +118,7 @@ export default function Home({ }) { | |
| const [dismissedNameChangeBanner, setDismissedNameChangeBanner] = useState(false) | ||
| const [dismissedBanBanner, setDismissedBanBanner] = useState(false) | ||
| const [timeOffset, setTimeOffset] = useState(0) | ||
| const timeSyncRef = useRef({ bestRtt: Infinity, lastSyncAt: 0, lastServerNow: 0 }) | ||
| const [loginQueued, setLoginQueued] = useState(false); | ||
| const [options, setOptions] = useState({ | ||
| }); | ||
|
|
@@ -967,6 +968,37 @@ export default function Home({ }) { | |
| const [multiplayerChatOpen, setMultiplayerChatOpen] = useState(false); | ||
| const [multiplayerChatEnabled, setMultiplayerChatEnabled] = useState(false); | ||
|
|
||
| const updateTimeOffsetFromSync = (serverNow, clientSentAt) => { | ||
| if (!serverNow || !clientSentAt) return; | ||
| const now = Date.now(); | ||
| const rtt = Math.max(0, now - clientSentAt); | ||
| const offset = serverNow - (clientSentAt + rtt / 2); | ||
| const sync = timeSyncRef.current; | ||
| const tooOld = now - sync.lastSyncAt > 60000; | ||
| const betterRtt = rtt <= sync.bestRtt + 25; | ||
| if (sync.lastSyncAt === 0 || betterRtt || tooOld) { | ||
| const prevBestRtt = sync.bestRtt; | ||
| sync.bestRtt = Math.min(sync.bestRtt, rtt); | ||
| sync.lastSyncAt = now; | ||
| sync.lastServerNow = serverNow; | ||
| if (window.debugTimeSync) { | ||
| console.log("[TimeSync] update", { | ||
| offset, | ||
| rtt, | ||
| serverNow, | ||
| clientSentAt, | ||
| prevBestRtt | ||
| }); | ||
| } | ||
| setTimeOffset(offset); | ||
| } | ||
| }; | ||
|
|
||
| const sendTimeSync = () => { | ||
| if (!ws || ws.readyState !== WebSocket.OPEN) return; | ||
| ws.send(JSON.stringify({ type: "timeSync", clientSentAt: Date.now() })); | ||
| }; | ||
|
|
||
|
|
||
| // Auto-close connection error modal when connected | ||
| useEffect(() => { | ||
|
|
@@ -985,6 +1017,27 @@ export default function Home({ }) { | |
| } | ||
| }, [session?.token?.secret, ws]) | ||
|
|
||
| useEffect(() => { | ||
| if (!ws) return; | ||
| timeSyncRef.current = { bestRtt: Infinity, lastSyncAt: 0, lastServerNow: 0 }; | ||
| setTimeOffset(0); | ||
| sendTimeSync(); | ||
| const interval = setInterval(() => { | ||
| sendTimeSync(); | ||
| }, 30000); | ||
| return () => clearInterval(interval); | ||
| }, [ws]) | ||
|
|
||
| useEffect(() => { | ||
| const handleVisibility = () => { | ||
| if (document.visibilityState === "visible") { | ||
| sendTimeSync(); | ||
| } | ||
| }; | ||
| document.addEventListener("visibilitychange", handleVisibility); | ||
| return () => document.removeEventListener("visibilitychange", handleVisibility); | ||
| }, [ws]) | ||
|
Comment on lines
+1020
to
+1039
|
||
|
|
||
| const { t: text } = useTranslation("common"); | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -1371,10 +1424,23 @@ export default function Home({ }) { | |
| } | ||
| if (data.type === "t") { | ||
| const offset = data.t - Date.now(); | ||
| if (Math.abs(offset) > 1000 && ((Math.abs(offset) < Math.abs(timeOffset)) || !timeOffset)) { | ||
| const sync = timeSyncRef.current; | ||
| const now = Date.now(); | ||
| const useFallback = sync.lastSyncAt === 0 || (now - sync.lastSyncAt) > 60000; | ||
| if (useFallback && Math.abs(offset) < 300000) { | ||
| if (window.debugTimeSync) { | ||
| console.log("[TimeSync] fallback", { | ||
| offset, | ||
| serverNow: data.t, | ||
| lastSyncAt: sync.lastSyncAt | ||
| }); | ||
| } | ||
| setTimeOffset(offset) | ||
| } | ||
| } | ||
| if (data.type === "timeSync") { | ||
| updateTimeOffsetFromSync(data.serverNow, data.clientSentAt); | ||
| } | ||
|
|
||
| if (data.type === "elo") { | ||
| setEloData((prev) => ({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -90,6 +90,7 @@ export default class Game { | |||||
| eloChanges: this.eloChanges, | ||||||
| pIds: this.pIds, | ||||||
| accountIds: this.accountIds, | ||||||
| oldElos: this.oldElos, | ||||||
| gameCount: this.gameCount, | ||||||
| location: this.location, | ||||||
| saveInProgress: this.saveInProgress, | ||||||
|
|
@@ -414,7 +415,6 @@ export default class Game { | |||||
| // For ranked duels: if someone leaves during "getready" (countdown before first round), | ||||||
| // cancel the game without ELO penalties - no actual gameplay has happened yet | ||||||
| const isPreGameLeave = this.public && this.duel && this.state === 'getready'; | ||||||
|
|
||||||
| // Track disconnection for ranked duels (only if actual gameplay has started) | ||||||
| if(this.public && this.duel && !isPreGameLeave) { | ||||||
| this.disconnectedPlayer = tag; | ||||||
|
|
@@ -462,7 +462,7 @@ export default class Game { | |||||
| console.log('Cannot start game: not in waiting state', this.state); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| if (Object.keys(this.players).length < 2) { | ||||||
| console.log('Cannot start game: not enough players', Object.keys(this.players).length); | ||||||
| if (hostPlayer) { | ||||||
|
|
@@ -474,7 +474,7 @@ export default class Game { | |||||
| } | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| if (this.rounds !== this.locations.length) { | ||||||
| console.log('Cannot start game: locations not loaded', this.rounds, this.locations.length); | ||||||
| if (hostPlayer) { | ||||||
|
|
@@ -555,7 +555,7 @@ export default class Game { | |||||
| } | ||||||
|
|
||||||
|
|
||||||
| if (allFinal && (this.nextEvtTime - Date.now()) > 5000) { | ||||||
| if (allFinal && (this.nextEvtTime - Date.now()) > 1000) { | ||||||
| this.nextEvtTime = Date.now() + 1000; | ||||||
| this.sendStateUpdate(); | ||||||
| } | ||||||
|
|
@@ -694,7 +694,7 @@ export default class Game { | |||||
| p.send(json); | ||||||
| } | ||||||
| } | ||||||
| end(leftUser) { | ||||||
| async end(leftUser) { | ||||||
| console.log(`Ending game ${this.id} - duel: ${this.duel}, public: ${this.public}, players: ${Object.keys(this.players).length}`); | ||||||
| // For duels, only save the final round if it was actually completed (all players made guesses) | ||||||
| // For regular games, save if the round was started but not yet saved | ||||||
|
|
@@ -773,21 +773,26 @@ export default class Game { | |||||
| } | ||||||
|
|
||||||
|
|
||||||
| let p1NewElo = null; | ||||||
| let p2NewElo = null; | ||||||
|
|
||||||
| let p1OldElo = p1obj?.elo || null; | ||||||
| let p2OldElo = p2obj?.elo || null; | ||||||
| const p1EloResult = await User.findById(this.accountIds.p1).select('elo').lean(); | ||||||
| const p2EloResult = await User.findById(this.accountIds.p2).select('elo').lean(); | ||||||
|
|
||||||
| // Use DB value if available, otherwise fall back to stored oldElos from game creation | ||||||
| // This prevents null ELO bugs while still handling external ELO updates | ||||||
| let p1OldElo = p1EloResult?.elo ?? this.oldElos?.p1 ?? null; | ||||||
| let p2OldElo = p2EloResult?.elo ?? this.oldElos?.p2 ?? null; | ||||||
|
|
||||||
| let p1NewElo = p1OldElo; | ||||||
| let p2NewElo = p2OldElo; | ||||||
| // elo changes | ||||||
| if(this.eloChanges) { | ||||||
| if(this.eloChanges && p1OldElo && p2OldElo) { | ||||||
|
||||||
| if(draw) { | ||||||
|
|
||||||
| const changes = this.eloChanges.draw; | ||||||
| // { newRating1, newRating2 } | ||||||
|
|
||||||
| p1NewElo = changes.newRating1; | ||||||
| p2NewElo = changes.newRating2; | ||||||
| p1NewElo += changes.newRating1; | ||||||
| p2NewElo += changes.newRating2; | ||||||
|
|
||||||
| if(p1obj) { | ||||||
|
|
||||||
|
|
@@ -797,27 +802,27 @@ export default class Game { | |||||
| } | ||||||
|
|
||||||
| if(p2obj) { | ||||||
| p2obj.setElo(changes.newRating2, { draw: true, oldElo: p2OldElo }); | ||||||
| p2obj.setElo(p2NewElo, { draw: true, oldElo: p2OldElo }); | ||||||
| } else { | ||||||
| setElo(this.accountIds.p2, changes.newRating2, { draw: true, oldElo: p2OldElo }); | ||||||
| setElo(this.accountIds.p2, p2NewElo, { draw: true, oldElo: p2OldElo }); | ||||||
| } | ||||||
|
||||||
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
updateTimeOffsetFromSyncfunction is called from the WebSocket message handler (line 1442), but it's not wrapped inuseCallback. This means a new function instance is created on every render. While this doesn't directly break functionality because it's called synchronously from the message handler, it's inconsistent with best practices. Consider wrapping it inuseCallbackfor consistency and to prevent potential issues if it's used elsewhere in the future.