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 // 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 // per-connection state it keyed on that departing uid before the uid is recycled
// to a new connection. Default: nothing to do. // to a new connection, and return any notices to emit as a result (e.g. telling
fn on_user_quit(&mut self, _uid: &str) {} // 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); 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: \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.", "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.", "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: \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.", "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.", "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: \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.", "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.", "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: \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.", "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.", "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: \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.", "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.", "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: \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.", "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.", "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) (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); 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) { 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, // Forget a user who left the network (QUIT or KILL): drop their membership,
// any half-finished SASL exchange, and their pending nick-protection timer. // 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) // Let services drop state keyed on this uid (e.g. GamesServ guest games)
// before the uid can be recycled to a new connection. // before the uid can be recycled to a new connection; collect any notices
for svc in self.services.iter_mut() { // they hand back (e.g. telling an opponent their game just ended).
svc.on_user_quit(uid); 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); let chans = self.network.channels_of(uid);
self.network.user_quit(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.sasl_source.remove(uid); // the captured SASL host/IP dies with the connection too
self.pending_enforce.retain(|p| p.uid != uid); self.pending_enforce.retain(|p| p.uid != uid);
self.forget_chatter_everywhere(uid); self.forget_chatter_everywhere(uid);
notices
} }
// Forget a channel once it's truly gone on the ircd — no members AND no resident // 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 } => { NetEvent::Quit { uid, reason } => {
// Match before we forget them, so their identity is still resolvable. // 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 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(); let mut out: Vec<NetAction> = self.notify_line('d', &uid, None, &what).into_iter().collect();
self.forget_user(&uid); out.extend(self.forget_user(&uid));
out out
} }
NetEvent::UserMode { uid, modes } => { NetEvent::UserMode { uid, modes } => {
@ -1641,8 +1647,7 @@ impl Engine {
let bots = self.reconcile_bots(); let bots = self.reconcile_bots();
return self.finish(out, bots); return self.finish(out, bots);
} }
self.forget_user(&uid); self.forget_user(&uid)
Vec::new()
} }
NetEvent::ServerLink { sid, parent } => { NetEvent::ServerLink { sid, parent } => {
self.network.server_link(&sid, &parent); self.network.server_link(&sid, &parent);
@ -1653,7 +1658,9 @@ impl Engine {
// forget every user behind the departed server and its descendants. // forget every user behind the departed server and its descendants.
for sid in self.network.server_split(&server) { for sid in self.network.server_split(&server) {
for uid in self.network.uids_on_server(&sid) { 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() Vec::new()