Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ choses :
- **Radio par profil** : une touche dédiée émet vers tous les joueurs
ayant le même profil que vous, peu importe leur canal et leur position.

### Diffusion globale (rôle broadcaster)
Un joueur à qui l'admin a accordé le rôle **broadcaster** dispose d'une
touche PTT supplémentaire qui diffuse sa voix sur **tous les canaux
radio simultanément**, peu importe le canal des destinataires. Utile
pour les modérateurs et organisateurs d'événements RP qui doivent
adresser tout le serveur sans imposer un canal commun.

L'admin gère la liste via la console admin (commandes
`grant_broadcaster <nom>` / `revoke_broadcaster <nom>` /
`list_broadcasters`). La capability est embarquée dans le ticket
d'authentification audio : la révocation devient effective au prochain
ticket (≤ 2 min en pratique). Un client connecté à un serveur trop
ancien (sans `server_caps: ["broadcast_all"]` dans le welcome) voit
simplement la touche sans effet — aucune trame n'est émise.

### Mode RP (Roleplay)
Quand activé, le filtre radio est appliqué sur la voip de proximité
**uniquement si les deux joueurs portent leur casque dans Star
Expand Down Expand Up @@ -138,6 +153,9 @@ Dans les paramètres du client, définissez :
- **Touche radio (PTT)** : à maintenir pour parler sur votre canal radio
- **Touche profile radio** : à maintenir pour parler à tous les joueurs
de votre profil
- **Touche diffusion globale (PTT)** : à maintenir pour diffuser à tous
les canaux (réservée aux broadcasters — sans le rôle, la touche reste
silencieuse côté serveur)
- **Cycle channel key** : pour changer rapidement de canal sans alt-tab

N'importe quelle touche ou combinaison de 2 touches clavier ou bouton
Expand Down
116 changes: 96 additions & 20 deletions client/circusvoip_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,14 @@
except Exception as e:
print(f"[BOOTSTRAP] ERREUR : pip indisponible dans le runtime "
f"Python ({e}).", flush=True)
print(f"[BOOTSTRAP] Installer les deps manuellement :", flush=True)

Check failure on line 188 in client/circusvoip_client.py

View workflow job for this annotation

GitHub Actions / ruff (non-bloquant)

Ruff (F541)

client/circusvoip_client.py:188:15: F541 f-string without any placeholders
deps_str = " ".join(p for _, p in missing)
print(f" py -m pip install {deps_str}", flush=True)
sys.exit(1)

# Installation pour chaque dep manquante
failed = []
for mod_name, pip_name in missing:

Check failure on line 195 in client/circusvoip_client.py

View workflow job for this annotation

GitHub Actions / ruff (non-bloquant)

Ruff (B007)

client/circusvoip_client.py:195:9: B007 Loop control variable `mod_name` not used within loop body
print(f"[BOOTSTRAP] pip install {pip_name}...", flush=True)
try:
# On utilise check=False pour pouvoir collecter les erreurs et
Expand Down Expand Up @@ -221,7 +221,7 @@
print(f"[BOOTSTRAP] {len(failed)} dependance(s) ont echoue :", flush=True)
for p in failed:
print(f" - {p}", flush=True)
print(f"[BOOTSTRAP] Tentez l'installation manuelle :", flush=True)

Check failure on line 224 in client/circusvoip_client.py

View workflow job for this annotation

GitHub Actions / ruff (non-bloquant)

Ruff (F541)

client/circusvoip_client.py:224:15: F541 f-string without any placeholders
print(f" py -m pip install {' '.join(failed)}", flush=True)
print("=" * 64, flush=True)
sys.exit(1)
Expand All @@ -236,39 +236,39 @@
_boot_log("apres _bootstrap_dependencies()")


from PySide6.QtCore import (
Qt, QTimer, QObject, Signal, Slot, QThread, QPoint, QRect,
)
from PySide6.QtGui import (
QGuiApplication, QScreen, QCursor, QPainter, QColor, QPen,
QFont, QKeyEvent, QMouseEvent, QIcon, QPixmap,
)
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QFrame,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMainWindow,
QMessageBox,
QProgressBar,
QPushButton,
QScrollArea,
QSizePolicy,
QSlider,
QStackedWidget,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)

Check failure on line 271 in client/circusvoip_client.py

View workflow job for this annotation

GitHub Actions / ruff (non-bloquant)

Ruff (I001)

client/circusvoip_client.py:239:1: I001 Import block is un-sorted or un-formatted
_boot_log("imports PySide6 termines")

# Optional dependency : websockets pour la connexion serveur
Expand Down Expand Up @@ -1062,8 +1062,8 @@
"gamelog_path",
# Mode RP
"rp_mode",
# Hotkeys (8 raccourcis)
"radio_key", "profile_radio_key",
# Hotkeys (9 raccourcis)
"radio_key", "profile_radio_key", "broadcast_all_key",
"mute_mic_key", "mute_prox_key", "mute_radio_key", "mute_all_key",
"proximity_short_key", "cycle_channel_key",
})
Expand Down Expand Up @@ -1275,6 +1275,11 @@
last_radio_seen_ts: dict = {}
profile_radio_key = None
profile_radio_active = False
# [BROADCAST_ALL] PTT diffusion globale + capabilities serveur
broadcast_all_key = None
broadcast_all_active = False
server_supports_broadcast_all = False
is_broadcaster = False
cycle_channel_key = None
# Overlays
overlays_show = False
Expand Down Expand Up @@ -1432,13 +1437,32 @@
except Exception:
pass

# [BROADCASTER_AUTH] Si on a un token broadcaster sauvegarde
# pour ce serveur, on le presente. Sinon champ vide : le
# serveur ne donne can_broadcast=True que si nom + token
# correspondent. Le token a ete pushed par broadcaster_token_granted
# (cf _handle_message) lors d'un grant precedent. Per-server :
# indexe par "host:port" de sorte qu'un meme client puisse
# avoir des roles differents sur plusieurs serveurs.
bcast_token = ""
if _CORE_AVAILABLE:
try:
bcast_token = _core._get_broadcaster_token(server_ip, SERVER_PORT)
except Exception:
bcast_token = ""
# On garde aussi en memoire la cle serveur pour les push
# ulterieurs (granted/revoked), evite de re-deviner ip/port.
self._server_key_ip = server_ip
self._server_key_port = SERVER_PORT

# Envoi du join. channel=None car 2a ne gere pas les canaux
# (sera ajoute en 2c).
await ws.send(json.dumps({
"type": "join",
"name": name,
"token": token,
"channel": None,
"broadcaster_token": bcast_token,
}))

# Bug fix 56 : marquer connected=True UNIQUEMENT apres
Expand Down Expand Up @@ -1506,6 +1530,46 @@
if reason == "invalid_token":
self.sig_invalid_token.emit()
self._stop_requested = True
elif reason in ("name_in_use", "broadcaster_token_invalid"):
# Echec d'auth specifique : on log et on coupe. Pas de retry
# auto (l'utilisateur doit changer son setup : nom different,
# ou demander un re-grant a l'admin).
self._stop_requested = True
return

if msg_type == "broadcaster_token_granted":
# L'admin a accorde le role broadcaster a ce client. Le token clair
# est dans data["token"]. On le sauve indexe par le serveur courant
# pour qu'il soit represente automatiquement aux prochains join.
token = data.get("token", "") or ""
if token and _CORE_AVAILABLE:
try:
ip = getattr(self, "_server_key_ip", "")
port = getattr(self, "_server_key_port", SERVER_PORT)
_core._set_broadcaster_token(ip, port, token)
state.is_broadcaster = True
self.sig_log.emit(
"[NET] Role broadcaster accorde : token sauvegarde. "
"Reconnecte-toi pour activer la touche."
)
except Exception as e:
self.sig_log.emit(f"[NET] broadcaster grant : sauvegarde KO : {e}")
return

if msg_type == "broadcaster_revoked":
# L'admin a revoque le role. On efface le token local pour eviter
# un join refuse a la prochaine reconnexion (le nom redevient
# libre cote serveur). La revocation est aussi appliquee au
# ticket actuel a la prochaine emission par le serveur.
if _CORE_AVAILABLE:
try:
ip = getattr(self, "_server_key_ip", "")
port = getattr(self, "_server_key_port", SERVER_PORT)
_core._set_broadcaster_token(ip, port, "")
state.is_broadcaster = False
self.sig_log.emit("[NET] Role broadcaster revoque.")
except Exception as e:
self.sig_log.emit(f"[NET] broadcaster revoke : nettoyage KO : {e}")
return

if msg_type == "welcome":
Expand Down Expand Up @@ -1542,6 +1606,13 @@
# reconnecte, on obtient un nouveau ticket et l'ancien est
# ecrase (l'ancien ne vaut plus rien cote serveur).
state.audio_ticket = data.get("audio_ticket", "") or ""
# [BROADCAST_ALL] Capabilities serveur + role broadcaster.
# Le client n'active sa touche PTT diffusion globale que si
# le serveur l'annonce dans server_caps ET que l'admin a
# accorde le role a ce joueur. Sinon : touche grisee.
server_caps = data.get("server_caps") or []
state.server_supports_broadcast_all = "broadcast_all" in server_caps
state.is_broadcaster = bool(data.get("is_broadcaster", False))
except Exception as e:
if _CORE_AVAILABLE:
try:
Expand Down Expand Up @@ -4873,9 +4944,10 @@
parent_layout.addLayout(h)
return val_lbl

self.lbl_radio_key = _make_key_row(v_radio, "Radio canal (PTT) :", "radio")
self.lbl_profile_key = _make_key_row(v_radio, "Radio profil (PTT) :", "profile")
self.lbl_mute_mic_key = _make_key_row(v_radio, "Mute micro :", "mute_mic")
self.lbl_radio_key = _make_key_row(v_radio, "Radio canal (PTT) :", "radio")
self.lbl_profile_key = _make_key_row(v_radio, "Radio profil (PTT) :", "profile")
self.lbl_broadcast_all_key = _make_key_row(v_radio, "Diffusion globale (PTT) :", "broadcast_all")
self.lbl_mute_mic_key = _make_key_row(v_radio, "Mute micro :", "mute_mic")
self.lbl_mute_prox_key = _make_key_row(v_radio, "Mute audio proximite :", "mute_prox")
self.lbl_mute_radio_key = _make_key_row(v_radio, "Mute audio radio :", "mute_radio")
self.lbl_mute_all_key = _make_key_row(v_radio, "Mute tout :", "mute_all")
Expand Down Expand Up @@ -5525,14 +5597,15 @@
return
# Liste de tuples (attr_label, attr_state)
rows = [
("lbl_radio_key", "radio_key"),
("lbl_profile_key", "profile_radio_key"),
("lbl_mute_mic_key", "mute_mic_key"),
("lbl_mute_prox_key", "mute_prox_key"),
("lbl_mute_radio_key", "mute_radio_key"),
("lbl_mute_all_key", "mute_all_key"),
("lbl_prox_short_key", "proximity_short_key"),
("lbl_cycle_ch_key", "cycle_channel_key"),
("lbl_radio_key", "radio_key"),
("lbl_profile_key", "profile_radio_key"),
("lbl_broadcast_all_key", "broadcast_all_key"),
("lbl_mute_mic_key", "mute_mic_key"),
("lbl_mute_prox_key", "mute_prox_key"),
("lbl_mute_radio_key", "mute_radio_key"),
("lbl_mute_all_key", "mute_all_key"),
("lbl_prox_short_key", "proximity_short_key"),
("lbl_cycle_ch_key", "cycle_channel_key"),
]
for lbl_attr, state_attr in rows:
lbl = getattr(self, lbl_attr, None)
Expand Down Expand Up @@ -5727,13 +5800,14 @@
return
# kind -> (label dialog, attribut state, cle config)
kinds = {
"radio": ("Radio canal (PTT)", "radio_key", "radio_key"),
"profile": ("Radio profil (PTT)", "profile_radio_key", "profile_radio_key"),
"mute_mic": ("Mute micro (toggle)", "mute_mic_key", "mute_mic_key"),
"mute_prox": ("Mute audio proximite", "mute_prox_key", "mute_prox_key"),
"mute_radio": ("Mute audio radio", "mute_radio_key", "mute_radio_key"),
"mute_all": ("Mute tout", "mute_all_key", "mute_all_key"),
"prox_short": ("Proximite 30m / 5m", "proximity_short_key", "proximity_short_key"),
"radio": ("Radio canal (PTT)", "radio_key", "radio_key"),
"profile": ("Radio profil (PTT)", "profile_radio_key", "profile_radio_key"),
"broadcast_all": ("Diffusion globale (PTT)", "broadcast_all_key", "broadcast_all_key"),
"mute_mic": ("Mute micro (toggle)", "mute_mic_key", "mute_mic_key"),
"mute_prox": ("Mute audio proximite", "mute_prox_key", "mute_prox_key"),
"mute_radio": ("Mute audio radio", "mute_radio_key", "mute_radio_key"),
"mute_all": ("Mute tout", "mute_all_key", "mute_all_key"),
"prox_short": ("Proximite 30m / 5m", "proximity_short_key", "proximity_short_key"),
"cycle_channel": ("Cycle canal radio", "cycle_channel_key", "cycle_channel_key"),
}
if kind not in kinds:
Expand Down Expand Up @@ -7504,6 +7578,7 @@
return k # fallback : laisser la valeur brute
state.radio_key = _canon(core_cfg.get("radio_key"))
state.profile_radio_key = _canon(core_cfg.get("profile_radio_key"))
state.broadcast_all_key = _canon(core_cfg.get("broadcast_all_key"))
state.mute_mic_key = _canon(core_cfg.get("mute_mic_key"))
state.mute_prox_key = _canon(core_cfg.get("mute_prox_key"))
state.mute_radio_key = _canon(core_cfg.get("mute_radio_key"))
Expand Down Expand Up @@ -7987,6 +8062,7 @@
"zone_source",
"radio_key",
"profile_radio_key",
"broadcast_all_key",
"mute_mic_key",
"mute_prox_key",
"mute_radio_key",
Expand Down
Loading
Loading