Ajoute un ping serveur régulier comme troisième couche d'anti-triche sur Icare et la Corne
Build and deploy / deploy (push) Successful in 36s
Build and deploy / deploy (push) Successful in 36s
duration_ms/actions/merges restaient entièrement déclaratifs (calculés et envoyés par le client en une fois à la fin), donc falsifiables par un appel RPC direct. Le client envoie maintenant un ping toutes les 5s pendant la partie (ping_game_run), horodaté par le serveur, jamais par le client. submit_icarus_score/submit_melon_score corrèlent le nombre de pings et leur écart réel avec la durée annoncée (parties ≥ 15s uniquement) et signalent toute incohérence dans game_cheat_flags (missing_pings, ping_span_mismatch) — toujours en signalement silencieux, jamais de blocage, comme pour le reste de l'anti-triche. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Avatar } from "@/components/avatar";
|
||||
import { Podium } from "@/components/podium";
|
||||
import { IconClose, IconTrophy } from "@/components/icons";
|
||||
@@ -9,6 +9,15 @@ import { createClient, waitForRealtimeAuth } from "@/lib/supabase/client";
|
||||
import { IcarusGame, type IcarusRunTelemetry } from "./icarus-game";
|
||||
import type { IcarusScoreEntry } from "./page";
|
||||
|
||||
// Anti-triche par ping (complète le signalement d'Alexandre, voir
|
||||
// supabase/schema.sql section 18) : la durée/nombre d'actions envoyées à la
|
||||
// fin d'une partie sont déclaratives, un ping régulier horodaté par le
|
||||
// serveur PENDANT la partie donne une preuve indépendante. Rythme choisi
|
||||
// pour rester léger (pas de souci de charge à cette fréquence pour ~12
|
||||
// joueurs) tout en laissant peu de marge à une partie fabriquée sans
|
||||
// jamais avoir été réellement ouverte.
|
||||
const PING_INTERVAL_MS = 5000;
|
||||
|
||||
export function IcareView({
|
||||
initialLeaderboard,
|
||||
initialOwnBestScore,
|
||||
@@ -21,6 +30,20 @@ export function IcareView({
|
||||
const [leaderboard, setLeaderboard] = useState(initialLeaderboard);
|
||||
const [ownBestScore, setOwnBestScore] = useState(initialOwnBestScore);
|
||||
const [showRanking, setShowRanking] = useState(false);
|
||||
const sessionTokenRef = useRef<string | null>(null);
|
||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const stopPinging = useCallback(() => {
|
||||
if (pingIntervalRef.current !== null) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Coupe le ping si le joueur quitte /icare en pleine partie (sinon
|
||||
// l'intervalle continuerait d'appeler la RPC pour un composant démonté).
|
||||
useEffect(() => stopPinging, [stopPinging]);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
const supabase = createClient();
|
||||
const { data: scores } = await supabase
|
||||
@@ -66,9 +89,28 @@ export function IcareView({
|
||||
};
|
||||
}, [refetch]);
|
||||
|
||||
async function handleFinish(score: number, run: IcarusRunTelemetry) {
|
||||
function handleStart() {
|
||||
const supabase = createClient();
|
||||
const { data, error } = await supabase.rpc("submit_icarus_score", { p_score: score, p_run: run });
|
||||
const token = crypto.randomUUID();
|
||||
sessionTokenRef.current = token;
|
||||
|
||||
stopPinging();
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
// Best-effort : un ping manqué (réseau, onglet en arrière-plan) est
|
||||
// sans conséquence, submit_icarus_score reste tolérant (marge de 15s
|
||||
// avant le premier ping attendu, voir schema.sql section 18).
|
||||
supabase.rpc("ping_game_run", { p_game: "icarus", p_session_token: token }).then(
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
}, PING_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function handleFinish(score: number, run: IcarusRunTelemetry) {
|
||||
stopPinging();
|
||||
const supabase = createClient();
|
||||
const runWithToken = { ...run, session_token: sessionTokenRef.current };
|
||||
const { data, error } = await supabase.rpc("submit_icarus_score", { p_score: score, p_run: runWithToken });
|
||||
if (!error && data) {
|
||||
setOwnBestScore(data.best_score);
|
||||
refetch();
|
||||
@@ -97,7 +139,7 @@ export function IcareView({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<IcarusGame onFinish={handleFinish} />
|
||||
<IcarusGame onStart={handleStart} onFinish={handleFinish} />
|
||||
|
||||
{showRanking && (
|
||||
<div className="fixed inset-0 z-30 flex items-center justify-center bg-ink/80 p-4 backdrop-blur-sm">
|
||||
|
||||
Reference in New Issue
Block a user