Notify a game opponent when a departing guest's game ends
All checks were successful
CI / check (push) Successful in 5m39s

This commit is contained in:
Jean Chevronnet 2026-07-21 10:40:27 +00:00
parent 9e8449b40d
commit ac021ec584
No known key found for this signature in database
9 changed files with 56 additions and 18 deletions

View file

@ -2465,8 +2465,11 @@ pub trait Service: Send {
}
// A user left the network (QUIT / KILL / netsplit). Lets a service drop any
// per-connection state it keyed on that departing uid before the uid is recycled
// to a new connection. Default: nothing to do.
fn on_user_quit(&mut self, _uid: &str) {}
// to a new connection, and return any notices to emit as a result (e.g. telling
// an opponent their game just ended). Default: nothing to do.
fn on_user_quit(&mut self, _uid: &str, _net: &dyn NetView, _store: &dyn Store) -> Vec<NetAction> {
Vec::new()
}
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, store: &mut dyn Store);
}

View file

@ -1372,5 +1372,6 @@
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Syntax: \u0002LOGIN <Nick> <Passwort>\u0002\nMeldet dich beim Konto von <Nick> an und setzt dich auf diesen Nick, wobei jede Sitzung, die ihn belegt, freigegeben wird.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nÜberträgt den Gründer oder ändert eine Kanaleinstellung.",
"Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Dein Konto ist ausstehend. Bitte ein bestehendes Mitglied, mit \u0002/msg NickServ VOUCH {account}\u0002 für dich zu bürgen.",
"You vouched for \u0002{account}\u0002; their account is now active.": "Du hast für \u0002{account}\u0002 gebürgt; das Konto ist jetzt aktiv."
"You vouched for \u0002{account}\u0002; their account is now active.": "Du hast für \u0002{account}\u0002 gebürgt; das Konto ist jetzt aktiv.",
"Your opponent left. Game \u0002#{id}\u0002 is over.": "Dein Gegner hat die Verbindung getrennt. Spiel \u0002#{id}\u0002 ist vorbei."
}

View file

@ -1372,5 +1372,6 @@
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxis: \u0002LOGIN <apodo> <contraseña>\u0002\nInicia sesión en la cuenta de <apodo> y te mueve a ese apodo, liberando cualquier sesión que ya lo esté usando.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxis: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfiere el fundador o cambia un ajuste del canal.",
"Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Tu cuenta está pendiente. Pedile a un miembro existente que te avale con \u0002/msg NickServ VOUCH {account}\u0002.",
"You vouched for \u0002{account}\u0002; their account is now active.": "Avalaste a \u0002{account}\u0002; su cuenta ya está activa."
"You vouched for \u0002{account}\u0002; their account is now active.": "Avalaste a \u0002{account}\u0002; su cuenta ya está activa.",
"Your opponent left. Game \u0002#{id}\u0002 is over.": "Tu oponente se fue. La partida \u0002#{id}\u0002 terminó."
}

View file

@ -1372,5 +1372,6 @@
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxis: \u0002LOGIN <apodo> <contraseña>\u0002\nInicia sesión en la cuenta de <apodo> y te mueve a ese apodo, liberando cualquier sesión que ya lo ocupe.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxis: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfiere el fundador o cambia un ajuste del canal.",
"Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Tu cuenta está pendiente. Pide a un miembro existente que te avale con \u0002/msg NickServ VOUCH {account}\u0002.",
"You vouched for \u0002{account}\u0002; their account is now active.": "Avalaste a \u0002{account}\u0002; su cuenta ya está activa."
"You vouched for \u0002{account}\u0002; their account is now active.": "Avalaste a \u0002{account}\u0002; su cuenta ya está activa.",
"Your opponent left. Game \u0002#{id}\u0002 is over.": "Tu oponente se fue. La partida \u0002#{id}\u0002 terminó."
}

View file

@ -1372,5 +1372,6 @@
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Syntaxe : \u0002LOGIN <pseudo> <mot de passe>\u0002\nVous connecte au compte de <pseudo> et vous place sur ce pseudo, en libérant toute session qui l'occupe déjà.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Syntaxe : \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfère le fondateur ou modifie un réglage du salon.",
"Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Votre compte est en attente. Demandez à un membre existant de vous parrainer avec \u0002/msg NickServ VOUCH {account}\u0002.",
"You vouched for \u0002{account}\u0002; their account is now active.": "Vous avez parrainé \u0002{account}\u0002 ; son compte est maintenant actif."
"You vouched for \u0002{account}\u0002; their account is now active.": "Vous avez parrainé \u0002{account}\u0002 ; son compte est maintenant actif.",
"Your opponent left. Game \u0002#{id}\u0002 is over.": "Ton adversaire est parti. La partie \u0002#{id}\u0002 est terminée."
}

View file

@ -1372,5 +1372,6 @@
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxe: \u0002LOGIN <apelido> <senha>\u0002\nEntra na conta de <apelido> e move você para esse apelido, liberando qualquer sessão que já o ocupe.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxe: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfere o fundador ou altera uma configuração do canal.",
"Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "Sua conta está pendente. Peça a um membro existente para avalizar você com \u0002/msg NickServ VOUCH {account}\u0002.",
"You vouched for \u0002{account}\u0002; their account is now active.": "Você avalizou \u0002{account}\u0002; a conta agora está ativa."
"You vouched for \u0002{account}\u0002; their account is now active.": "Você avalizou \u0002{account}\u0002; a conta agora está ativa.",
"Your opponent left. Game \u0002#{id}\u0002 is over.": "Seu oponente saiu. O jogo \u0002#{id}\u0002 acabou."
}

View file

@ -1372,5 +1372,6 @@
"Syntax: \u0002LOGIN <nick> <password>\u0002\nLogs you in to <nick>'s account and moves you onto that nick, freeing any session already holding it.": "Sintaxe: \u0002LOGIN <alcunha> <palavra-passe>\u0002\nInicia sessão na conta de <alcunha> e move-o para essa alcunha, libertando qualquer sessão que já a ocupe.",
"Syntax: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfers the founder or changes a channel setting.": "Sintaxe: \u0002SET <#channel> FOUNDER <account> | DESC <text> | URL [address] | EMAIL [address] | SUCCESSOR <account>|OFF | SIGNKICK|PRIVATE|PEACE|SECUREOPS|SECUREVOICES|RESTRICTED|AUTOOP|KEEPTOPIC|TOPICLOCK {ON|OFF}\u0002\nTransfere o fundador ou altera uma definição do canal.",
"Your account is pending. Ask an existing member to vouch for you with \u0002/msg NickServ VOUCH {account}\u0002.": "A sua conta está pendente. Peça a um membro existente que o avalize com \u0002/msg NickServ VOUCH {account}\u0002.",
"You vouched for \u0002{account}\u0002; their account is now active.": "Avalizou \u0002{account}\u0002; a conta está agora ativa."
"You vouched for \u0002{account}\u0002; their account is now active.": "Avalizou \u0002{account}\u0002; a conta está agora ativa.",
"Your opponent left. Game \u0002#{id}\u0002 is over.": "O teu adversário saiu. O jogo \u0002#{id}\u0002 acabou."
}

View file

@ -499,8 +499,30 @@ impl Service for GameServ {
(BLURB, TOPICS)
}
fn on_user_quit(&mut self, uid: &str) {
fn on_user_quit(&mut self, uid: &str, net: &dyn NetView, store: &dyn Store) -> Vec<NetAction> {
// Tell each still-online opponent of an ACTIVE game that the departing guest
// ended it (in the opponent's language), then drop the guest's games.
let me = self.uid.clone();
let mut out = Vec::new();
for g in self.games.values() {
if g.status != Status::Active {
continue;
}
let opp = if g.acc_a == uid {
&g.nick_b
} else if g.acc_b == uid {
&g.nick_a
} else {
continue;
};
if let Some(ouid) = net.uid_by_nick(opp).map(str::to_string) {
let lang = net.account_of(&ouid).and_then(|a| store.language_of(a)).unwrap_or_else(|| store.default_language());
let text = echo_api::render(&lang, "Your opponent left. Game \x02#{id}\x02 is over.", &[("id", g.id.to_string())]);
out.push(NetAction::Notice { from: me.clone(), to: ouid, text });
}
}
self.drop_guest_games(uid);
out
}
fn on_command(&mut self, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, net: &dyn NetView, _store: &mut dyn Store) {

View file

@ -736,11 +736,16 @@ impl Engine {
// Forget a user who left the network (QUIT or KILL): drop their membership,
// any half-finished SASL exchange, and their pending nick-protection timer.
fn forget_user(&mut self, uid: &str) {
fn forget_user(&mut self, uid: &str) -> Vec<NetAction> {
// Let services drop state keyed on this uid (e.g. GamesServ guest games)
// before the uid can be recycled to a new connection.
for svc in self.services.iter_mut() {
svc.on_user_quit(uid);
// before the uid can be recycled to a new connection; collect any notices
// they hand back (e.g. telling an opponent their game just ended).
let mut notices = Vec::new();
{
let Self { services, network, db, .. } = self;
for svc in services.iter_mut() {
notices.extend(svc.on_user_quit(uid, &*network, &*db));
}
}
let chans = self.network.channels_of(uid);
self.network.user_quit(uid);
@ -751,6 +756,7 @@ impl Engine {
self.sasl_source.remove(uid); // the captured SASL host/IP dies with the connection too
self.pending_enforce.retain(|p| p.uid != uid);
self.forget_chatter_everywhere(uid);
notices
}
// Forget a channel once it's truly gone on the ircd — no members AND no resident
@ -1607,8 +1613,8 @@ impl Engine {
NetEvent::Quit { uid, reason } => {
// Match before we forget them, so their identity is still resolvable.
let what = if reason.is_empty() { "disconnected".to_string() } else { format!("disconnected ({reason})") };
let out: Vec<NetAction> = self.notify_line('d', &uid, None, &what).into_iter().collect();
self.forget_user(&uid);
let mut out: Vec<NetAction> = self.notify_line('d', &uid, None, &what).into_iter().collect();
out.extend(self.forget_user(&uid));
out
}
NetEvent::UserMode { uid, modes } => {
@ -1641,8 +1647,7 @@ impl Engine {
let bots = self.reconcile_bots();
return self.finish(out, bots);
}
self.forget_user(&uid);
Vec::new()
self.forget_user(&uid)
}
NetEvent::ServerLink { sid, parent } => {
self.network.server_link(&sid, &parent);
@ -1653,7 +1658,9 @@ impl Engine {
// forget every user behind the departed server and its descendants.
for sid in self.network.server_split(&server) {
for uid in self.network.uids_on_server(&sid) {
self.forget_user(&uid);
// Drop guest games as usual, but swallow the opponent notices:
// a netsplit is a mass event and must not spray "opponent left".
let _ = self.forget_user(&uid);
}
}
Vec::new()