Files
the-cycle/SQUAD-EMULATION.md
T
2026-07-16 08:28:58 +00:00

7.1 KiB

Étude — Émulation PlayFab Groups/Party pour l'invite de squad

Objectif : permettre à un joueur d'inviter un ami à faire équipe (squad) sur le serveur privé. Étude du mécanisme, de l'état actuel, d'une conception d'émulation et d'un plan. Branche : game-server.

1. État actuel (ce qui existe déjà)

Roster de squad en mémoireServices/Squad/SquadService.cs :

  • Squads clés par un squadId fourni par le client (JoinOrCreate(squadId, userId, …)).
  • Membre = profil (playerId, displayName), onlineState, isReady, selectedMap, isLeader.
  • _userToSquad permet à TryGetCompleteSquadInfo (qui ne porte pas de squadId) de retrouver la squad du joueur.

Fonctions CloudScript squad présentes : TryGetCompleteSquadInfo, SquadMemberReadyForMatch (pousse déjà via SignalR), SquadMemberSelectedMap, SquadMemberStartingDeployFlow, GetFriendList, ClientsideFriendsImport.

Ce qui MANQUE (cœur du problème) : aucune fonction de création de squad, d'invitation, d'acceptation/refus, de join/leave/kick. Le flux actuel suppose que le client connaît déjà un squadId (party formée ailleurs) et se contente d'y rattacher les membres.

Infra de push temps-réel DISPONIBLEHubs/CycleHub.cs + Hubs/SignalRConnectionRegistry.cs : le client se connecte au hub avec ?uid=<playerId> ; le registry mappe uid → connectionId. Le serveur peut donc pousser un message ciblé à un joueur précis (registry.GetConnection(uid) + IHubContext<CycleHub>.Clients.Client(conn).SendAsync(<event>, payload)). C'est déjà utilisé par SquadMemberReadyForMatch et par le signal « travel to match » du matchmaking. → Le canal de livraison de l'invite existe déjà.

2. Comment The Cycle forme-t-il les squads ? (hypothèses + preuves)

Le squadId étant fourni par le client, la party est créée là où le client obtient cet id. Quatre mécanismes candidats côté PlayFab/Steam :

# Mécanisme Ce que ça implique côté serveur Indice
A PlayFab Entity Groups (/Group/CreateGroup, /Group/InviteToGroup, /Group/AcceptGroupInvitation, /Group/ListGroupMembers…) Implémenter le sous-ensemble Groups (entités déjà émulées). squadId = group entity id. Fort : squads persistantes chez PlayFab passent par Groups ; l'id opaque partagé colle.
B PlayFab Lobby (Multiplayer : CreateLobby/JoinLobby/InviteToLobby via connection string) Implémenter l'API Lobby. squadId = lobbyId. Moyen : plutôt matchmaking S3+.
C Lobby Steam (invite via overlay Steam) + backend qui suit Rien à créer pour l'invite (100% Steam) ; le serveur ne fait que suivre le squadId rapporté. Moyen : S2 = Steam-only ; explique le squadId client. Mais alors l'invite n'est pas pilotable serveur.
D Fonctions CloudScript d'invite (non observées) Implémenter InvitePlayerToSquad/RespondToSquadInvite/… Faible : aucune trace.

Preuve manquante (bloquant) : les logs prod de la session à 2 ont été écrasés par nos redéploiements. Impossible aujourd'hui de voir l'appel exact d'invite. De plus, l'émulateur peut 404 silencieusement un endpoint PlayFab natif non routé (ex. /Group/CreateGroup) → un tel appel n'apparaît pas comme fonction CloudScript. Il faut donc capturer l'appel réel (voir §5, Phase 0) — d'autant que le bouton « inviter » n'apparaît pas tant que la tuile ami est vide (bug de forme GetFriendList, corrigé PR #15, à valider en jeu). Tant que la liste d'amis ne s'affiche pas, aucune invite ne peut même être tentée.

3. Conception d'émulation proposée (indépendante du mécanisme exact)

Quel que soit A/B/C/D, les briques serveur à construire sont les mêmes :

3.1 Étendre SquadService (états d'invite) :

  • CreateSquad(leaderUserId) → squadId (GUID serveur si le client n'en impose pas).
  • Invite(squadId, fromUserId, toUserId) → enregistre une invitation en attente {squadId, from, to, expiresAt}.
  • RespondToInvite(toUserId, squadId, accept) → si accept : JoinOrCreate ; sinon purge.
  • Leave / Kick(byLeader) / transfert de leadership à la sortie du leader.
  • GetPendingInvites(userId) (fallback poll).

3.2 Surface d'API — deux options selon la capture (§5) :

  • Si Groups (A) : router POST /Group/CreateGroup, /Group/InviteToGroup, /Group/AcceptGroupInvitation, /Group/ListGroupMembers, /Group/RemoveMembers vers SquadService (les EntityToken sont déjà émulés). C'est le plus « natif ».
  • Si CloudScript (D) / custom : ajouter les fonctions [CloudScriptFunction(...)] correspondantes (noms/shapes calqués sur la capture).
  • Si Steam (C) : rien pour l'invite (Steam) ; s'assurer seulement que le roster se peuple bien quand les deux clients rapportent le même squadId (déjà géré par SquadService).

3.3 Livraison temps-réel de l'invite (réutilise l'infra existante) :

  • À l'invitation, pousser à l'invité : registry.GetConnection(toUserId) + hub.Clients.Client(conn).SendAsync("<SquadInviteReceived>", payload).
  • Nom d'event + payload = à confirmer côté client (le client doit écouter cet event). Fallback : GetPendingSquadInvites (CloudScript) que le client poll à l'ouverture du menu social.

3.4 Persistance : le roster peut rester en mémoire (petit serveur), mais les invitations gagnent à être persistées (UserData PendingSquadInvites) pour survivre à un reload/redeploy et au cas « ami à la station » — combiné au push SignalR.

4. Limite structurelle importante (à dire clairement)

Émuler l'invite permet de former une squad et de se mettre prêt ensemble (lobby/ready-up). Mais déployer réellement ensemble dans le même raid nécessite le serveur de jeu dédié (Prospect.Server.Game), qui est bloqué au mur DTLS-PSK (cf. reste de cette branche). Les raids S2 sont hébergés côté client (solo) : sans serveur dédié, deux joueurs ne peuvent pas partager la même instance de raid. Donc :

  • Invite + squad + ready-up : émulable via l'API (cette étude).
  • Co-op en raid : dépend du serveur dédié (autre chantier, DTLS-PSK).

5. Plan par phases

  • Phase 0 — Capture (débloque tout) : valider le fix GetFriendList en jeu (la tuile ami doit afficher le pseudo) → le bouton « inviter » réapparaît → tenter une invite et capturer les appels (ajouter un logging catch-all des requêtes non routées + corps, pour voir un éventuel /Group/… 404). Résultat : on sait A/B/C/D.
  • Phase 1 — Invite : implémenter la surface identifiée + états SquadService + push SignalR ; persister les invitations.
  • Phase 2 — Ready-up/déploiement groupé : compléter (déjà amorcé) ; le déploiement co-op réel reste gated par le serveur dédié.

6. Prochaine action concrète

Repro en jeu (liste d'amis corrigée) + logging catch-all → capturer l'appel d'invite. Sans cette capture, toute implémentation d'invite serait une supposition sur les noms/shapes attendus par le client (paks chiffrés → pas de RE statique).