feat(client): tell clients WHY a session ended, not just that it did
A session ending was a single bit. A player quitting their game, an operator ending the session from the console, a stop the client itself asked for, a host crashing and a Wi-Fi drop all arrived as the same "closed" — so every client had to write one message covering all of them, and every client picked an error. That is how quitting your own game came to be reported as trouble on all three. The information was already there and thrown away: the host closes with APP_EXITED when a launched game exits, with 0 when it ends the session cleanly and 1 when it fails, and a link that simply dies never closes at all. The connection watcher now classifies that into a PunktfunkEndReason — local, game exited, host ended, host error, lost — and latches it before the shutdown flag, since the two are read by different threads and the reason must never arrive second. Exposed as punktfunk_connection_end_reason. This replaces the game-exited flag added a moment ago rather than joining it: that question is one row of this table, and it was never released. Still additive to any embedder that ignores it, and the host sends the same bytes either way, so the wire is untouched. `is_normal()` is the question nearly every caller actually has, so both the Rust and C surfaces answer it directly rather than making each client re-derive which of five values are worth alarming a user about.
This commit is contained in:
@@ -18,6 +18,11 @@ parse_deps = false
|
||||
# undefined and the C harness fails to compile: the Apple batched recv (transport/udp.rs
|
||||
# `recvmsg_x` + `MsghdrX`) and the Android bionic mmsg bindings (`android_mmsg` module).
|
||||
exclude = ["MsghdrX", "recvmsg_x", "mmsghdr", "sendmmsg", "recvmmsg"]
|
||||
# Reached by no exported SIGNATURE, so cbindgen's sweep misses it — but a C embedder needs the
|
||||
# vocabulary: `punktfunk_connection_end_reason` writes one of these as a bare byte (deliberately,
|
||||
# so the JNI/Swift sides can marshal a `u8` rather than an enum), which without this would leave
|
||||
# the header documenting names it never defines.
|
||||
include = ["PunktfunkEndReason"]
|
||||
|
||||
[export.rename]
|
||||
"InputEvent" = "PunktfunkInputEvent"
|
||||
|
||||
@@ -2273,24 +2273,27 @@ pub unsafe extern "C" fn punktfunk_connection_audio_channels(
|
||||
})
|
||||
}
|
||||
|
||||
/// Did this session end because **the game the host launched for it exited**? `*out` is set to 1
|
||||
/// when it did and 0 otherwise; the return status reports only whether the handle was usable.
|
||||
/// WHY this session ended: `*out` receives a [`PunktfunkEndReason`] byte
|
||||
/// (`PUNKTFUNK_END_REASON_*`). The return status reports only whether the handle was usable.
|
||||
///
|
||||
/// A refinement of "the session ended", never a substitute — read it only once a plane has
|
||||
/// returned [`PunktfunkStatus::Closed`] (or the embedder's own end-of-session signal fired), and
|
||||
/// treat 0 as "ended for some other reason" (user stop, host gone, network loss, idle timeout).
|
||||
/// It latches, so it is still readable while the connection is being torn down, and a client that
|
||||
/// never calls it behaves exactly as it did before this existed.
|
||||
/// Read it once a plane has returned [`PunktfunkStatus::Closed`] (or the embedder's own
|
||||
/// end-of-session signal fired); before that it reads `NONE`. It latches, so it is still readable
|
||||
/// while the connection is torn down, and a client that never calls it behaves exactly as it did
|
||||
/// before this existed.
|
||||
///
|
||||
/// The point is that a game ending is a normal finish, not a failure: a launcher client can send
|
||||
/// the player back to the host's library — one tap from the next title — rather than reporting an
|
||||
/// error and dropping to host selection for something the player just did on purpose.
|
||||
/// **Most endings are not failures.** Before this, a client had no way to tell a player quitting
|
||||
/// their game from a host falling off the network, so every client wrote one message for all of
|
||||
/// them and every client chose an error. Use `LOCAL`/`GAME_EXITED`/`HOST_ENDED` to stay quiet (and
|
||||
/// `GAME_EXITED` to return to the library the title was launched from), and keep the alarming copy
|
||||
/// for `HOST_ERROR` and `LOST`.
|
||||
///
|
||||
/// Treat an unrecognized value as `NONE` — this crosses an ABI and the core may be newer than you.
|
||||
///
|
||||
/// # Safety
|
||||
/// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
|
||||
#[cfg(feature = "quic")]
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn punktfunk_connection_game_exited(
|
||||
pub unsafe extern "C" fn punktfunk_connection_end_reason(
|
||||
c: *mut PunktfunkConnection,
|
||||
out: *mut u8,
|
||||
) -> PunktfunkStatus {
|
||||
@@ -2303,7 +2306,7 @@ pub unsafe extern "C" fn punktfunk_connection_game_exited(
|
||||
};
|
||||
if !out.is_null() {
|
||||
// SAFETY: `out` is non-null and the caller guarantees it is writable for one `u8`.
|
||||
unsafe { *out = u8::from(c.inner.ended_because_game_exited()) };
|
||||
unsafe { *out = c.inner.end_reason() as u8 };
|
||||
}
|
||||
PunktfunkStatus::Ok
|
||||
})
|
||||
|
||||
@@ -110,6 +110,91 @@ pub struct MicUplinkStats {
|
||||
/// the control task is wedged, which callers treat as a closed session.
|
||||
const CTRL_QUEUE: usize = 32;
|
||||
|
||||
/// Why a session ended — [`NativeClient::end_reason`], and `punktfunk_connection_end_reason` on the
|
||||
/// C surface.
|
||||
///
|
||||
/// The distinction that matters to a UI is **normal vs alarming**, and it is not a spectrum: a
|
||||
/// player quitting their game and a host falling off the network both arrive as "the session
|
||||
/// ended", and a client with no way to separate them has to word all of them the same. Every client
|
||||
/// worded them as failures.
|
||||
///
|
||||
/// Ordered loosely from "the user did this on purpose" to "something went wrong". Values are part
|
||||
/// of the C ABI: append only, never renumber.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PunktfunkEndReason {
|
||||
/// Not ended (or ended before a reason could be observed). Also what an unknown future value
|
||||
/// decodes to, so an older client reading a newer core degrades to "no opinion".
|
||||
None = 0,
|
||||
/// **This client** closed the session — the user pressed stop, or the handle was dropped.
|
||||
/// Nothing to report: the UI already knows, it initiated it.
|
||||
Local = 1,
|
||||
/// The host's launched game exited ([`crate::quic::APP_EXITED_CLOSE_CODE`]). A normal finish,
|
||||
/// and the one reason a launcher client can act on: go back to the library the title was
|
||||
/// launched from rather than all the way out to host selection.
|
||||
GameExited = 2,
|
||||
/// The host ended the session cleanly and deliberately — an operator "End" in the console, or
|
||||
/// the session simply finishing. Normal; say so plainly or say nothing.
|
||||
HostEnded = 3,
|
||||
/// The host closed reporting a failure of its own. Worth showing, and the host's log has the
|
||||
/// detail.
|
||||
HostError = 4,
|
||||
/// The connection died rather than being closed: idle timeout, reset, the network going away.
|
||||
/// This — and only this — is the "the host may be asleep, wake it" case.
|
||||
Lost = 5,
|
||||
}
|
||||
|
||||
impl PunktfunkEndReason {
|
||||
/// Decode the wire/ABI byte. Unknown values become [`Self::None`] rather than panicking: this
|
||||
/// crosses an ABI where the writer may be newer than the reader.
|
||||
pub fn from_u8(v: u8) -> Self {
|
||||
match v {
|
||||
1 => Self::Local,
|
||||
2 => Self::GameExited,
|
||||
3 => Self::HostEnded,
|
||||
4 => Self::HostError,
|
||||
5 => Self::Lost,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this ending is an ordinary outcome rather than something to alarm the user about.
|
||||
///
|
||||
/// The single question nearly every client actually asks. `Local`, `GameExited` and `HostEnded`
|
||||
/// are all things that were *meant* to happen; only a host-side failure or a dead connection
|
||||
/// are not. [`Self::None`] counts as normal — no evidence of trouble is not evidence of it.
|
||||
pub fn is_normal(self) -> bool {
|
||||
!matches!(self, Self::HostError | Self::Lost)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quic")]
|
||||
impl From<&quinn::ConnectionError> for PunktfunkEndReason {
|
||||
/// Classify the QUIC close.
|
||||
///
|
||||
/// Only two application codes ever arrive from a host at session end: `APP_EXITED` when the
|
||||
/// game it launched quit, and the teardown's own `0` (clean) / `1` (the session returned an
|
||||
/// error) from `native.rs`. Anything else with an application code is a deliberate host-side
|
||||
/// close we do not have a name for, which is still closer to "the host ended it" than to a
|
||||
/// dead link — but a code we have never issued is more likely a fault than a courtesy, so it
|
||||
/// lands in `HostError` where it will at least be visible.
|
||||
fn from(e: &quinn::ConnectionError) -> Self {
|
||||
match e {
|
||||
quinn::ConnectionError::LocallyClosed => Self::Local,
|
||||
quinn::ConnectionError::ApplicationClosed(ac) => {
|
||||
match u32::try_from(u64::from(ac.error_code)) {
|
||||
Ok(crate::quic::APP_EXITED_CLOSE_CODE) => Self::GameExited,
|
||||
Ok(0) => Self::HostEnded,
|
||||
_ => Self::HostError,
|
||||
}
|
||||
}
|
||||
// TimedOut, Reset, VersionMismatch, TransportError, CidsExhausted, and the peer's
|
||||
// transport-level close: the link failed, nobody said goodbye.
|
||||
_ => Self::Lost,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NativeClient {
|
||||
// Each plane's receiver sits behind its own mutex so `NativeClient` is `Sync` and Rust
|
||||
// embedders can share one `Arc<NativeClient>` across their plane threads (the same
|
||||
@@ -180,9 +265,9 @@ pub struct NativeClient {
|
||||
/// Speed-test accumulator, shared with the data-plane pump + control task.
|
||||
probe: Arc<Mutex<ProbeState>>,
|
||||
shutdown: Arc<AtomicBool>,
|
||||
/// Set with `shutdown` when the host's close carried [`crate::quic::APP_EXITED_CLOSE_CODE`] —
|
||||
/// see [`NativeClient::ended_because_game_exited`].
|
||||
game_exited: Arc<AtomicBool>,
|
||||
/// A [`PunktfunkEndReason`] as `u8`, latched with `shutdown` — see
|
||||
/// [`NativeClient::end_reason`].
|
||||
end_reason: Arc<AtomicU8>,
|
||||
/// Deliberate-quit flag: [`NativeClient::disconnect_quit`] sets it, so the worker closes the QUIC
|
||||
/// connection with [`crate::quic::QUIT_CLOSE_CODE`] (a user "stop") instead of code 0 — telling the
|
||||
/// host to skip the keep-alive linger. A plain drop leaves it false → an unwanted-disconnect close.
|
||||
@@ -451,7 +536,7 @@ impl NativeClient {
|
||||
std::sync::mpsc::sync_channel::<crate::quic::CursorState>(CURSOR_STATE_QUEUE);
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<Negotiated>>();
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let game_exited = Arc::new(AtomicBool::new(false));
|
||||
let end_reason = Arc::new(AtomicU8::new(PunktfunkEndReason::None as u8));
|
||||
let quit = Arc::new(AtomicBool::new(false));
|
||||
let mode_slot = Arc::new(std::sync::Mutex::new(mode));
|
||||
let probe = Arc::new(Mutex::new(ProbeState::default()));
|
||||
@@ -467,7 +552,7 @@ impl NativeClient {
|
||||
let host = host.to_string();
|
||||
let frame_chan_w = frame_chan.clone();
|
||||
let shutdown_w = shutdown.clone();
|
||||
let game_exited_w = game_exited.clone();
|
||||
let end_reason_w = end_reason.clone();
|
||||
let quit_w = quit.clone();
|
||||
let mode_slot_w = mode_slot.clone();
|
||||
let probe_w = probe.clone();
|
||||
@@ -543,7 +628,7 @@ impl NativeClient {
|
||||
clip_cmd_rx,
|
||||
ready_tx,
|
||||
shutdown: shutdown_w,
|
||||
game_exited: game_exited_w,
|
||||
end_reason: end_reason_w,
|
||||
quit: quit_w,
|
||||
mode_slot: mode_slot_w,
|
||||
probe: probe_w,
|
||||
@@ -597,7 +682,7 @@ impl NativeClient {
|
||||
host_caps: negotiated.host_caps,
|
||||
probe,
|
||||
shutdown,
|
||||
game_exited,
|
||||
end_reason,
|
||||
quit,
|
||||
worker: Some(worker),
|
||||
frames_dropped,
|
||||
@@ -816,22 +901,27 @@ impl NativeClient {
|
||||
self.shutdown.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Whether the session ended because **the game the host launched for it exited** — the host
|
||||
/// closed with [`crate::quic::APP_EXITED_CLOSE_CODE`] rather than dropping out.
|
||||
/// WHY the session ended — see [`PunktfunkEndReason`].
|
||||
///
|
||||
/// A refinement of [`is_session_ended`](Self::is_session_ended), never a substitute: it is only
|
||||
/// ever true once that is, and false covers every other ending (user stop, host gone, network
|
||||
/// loss, idle timeout) — so a client that ignores it behaves exactly as before.
|
||||
/// A refinement of [`is_session_ended`](Self::is_session_ended), never a substitute: it stays
|
||||
/// [`PunktfunkEndReason::None`] until that is true, and every client that ignores it behaves
|
||||
/// exactly as it did before this existed.
|
||||
///
|
||||
/// What it is FOR: a game ending is a normal, expected finish, not a failure. A launcher client
|
||||
/// can read this and go back to the host's library — where the player is one tap from the next
|
||||
/// title — instead of showing "session ended by <host>" and dropping to host selection, which
|
||||
/// reads as an error for something the player just did on purpose.
|
||||
/// What it is FOR: **most endings are not failures.** A client that cannot tell them apart has
|
||||
/// to pick one wording for all of them, and every such client picked an error — "Session ended
|
||||
/// by <host>", "Connection lost — the host may be asleep" — including when the player quit the
|
||||
/// game themselves. This is the discriminator that lets each client stay quiet for a normal
|
||||
/// finish, return to its library when a launched game exits, and reserve the alarming copy for
|
||||
/// an ending that actually deserves it.
|
||||
///
|
||||
/// Poll it after the session ends (a `Closed` on any plane, or `is_session_ended`); it latches,
|
||||
/// so it is still readable while the connection is being torn down.
|
||||
/// Latches, so it is still readable while the connection is being torn down.
|
||||
pub fn end_reason(&self) -> PunktfunkEndReason {
|
||||
PunktfunkEndReason::from_u8(self.end_reason.load(Ordering::SeqCst))
|
||||
}
|
||||
|
||||
/// Shorthand for the single most actionable reason: the host's launched game exited.
|
||||
pub fn ended_because_game_exited(&self) -> bool {
|
||||
self.game_exited.load(Ordering::SeqCst)
|
||||
self.end_reason() == PunktfunkEndReason::GameExited
|
||||
}
|
||||
|
||||
/// Register the calling thread as latency-critical so a later
|
||||
|
||||
@@ -65,7 +65,7 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
clip_cmd_rx,
|
||||
ready_tx,
|
||||
shutdown,
|
||||
game_exited,
|
||||
end_reason,
|
||||
quit,
|
||||
mode_slot,
|
||||
probe,
|
||||
@@ -195,22 +195,17 @@ pub(super) async fn run_pump(args: WorkerArgs) {
|
||||
clip_cmd_rx,
|
||||
));
|
||||
|
||||
// Watch for connection close → stop the pump, and record WHY if the host said so.
|
||||
// Watch for connection close → stop the pump, and classify WHY.
|
||||
{
|
||||
let shutdown = shutdown.clone();
|
||||
let game_exited = game_exited.clone();
|
||||
let end_reason = end_reason.clone();
|
||||
let conn = conn.clone();
|
||||
tokio::spawn(async move {
|
||||
let why = conn.closed().await;
|
||||
// The host closes with APP_EXITED when the game it launched for this session exited.
|
||||
// Latch that before `shutdown`, so any client that reacts to the shutdown flag can
|
||||
// already read the reason — the two are observed by different threads.
|
||||
if let quinn::ConnectionError::ApplicationClosed(ac) = &why {
|
||||
if u32::try_from(u64::from(ac.error_code)) == Ok(crate::quic::APP_EXITED_CLOSE_CODE)
|
||||
{
|
||||
game_exited.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
// Latch the reason BEFORE `shutdown`: the two are observed by different threads, and a
|
||||
// client that reacts to the shutdown flag must never find the reason still unset.
|
||||
let reason = crate::client::PunktfunkEndReason::from(&why);
|
||||
end_reason.store(reason as u8, Ordering::SeqCst);
|
||||
shutdown.store(true, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -68,10 +68,9 @@ pub(crate) struct WorkerArgs {
|
||||
pub(crate) clip_cmd_rx: tokio::sync::mpsc::UnboundedReceiver<ClipCommand>,
|
||||
pub(crate) ready_tx: std::sync::mpsc::Sender<Result<Negotiated>>,
|
||||
pub(crate) shutdown: Arc<AtomicBool>,
|
||||
/// Set alongside `shutdown` when the HOST's close carried
|
||||
/// [`crate::quic::APP_EXITED_CLOSE_CODE`] — the launched game exited (see
|
||||
/// [`NativeClient::ended_because_game_exited`]).
|
||||
pub(crate) game_exited: Arc<AtomicBool>,
|
||||
/// A [`crate::client::PunktfunkEndReason`] as `u8`, classified from the connection's close and
|
||||
/// latched alongside `shutdown` (see [`NativeClient::end_reason`]).
|
||||
pub(crate) end_reason: Arc<AtomicU8>,
|
||||
/// Deliberate-quit flag (see [`NativeClient::quit`]): the worker closes with the quit code if set.
|
||||
pub(crate) quit: Arc<AtomicBool>,
|
||||
pub(crate) mode_slot: Arc<std::sync::Mutex<Mode>>,
|
||||
|
||||
@@ -138,12 +138,13 @@ pub use stats::Stats;
|
||||
/// capability-gated end to end: the wire grows a new datagram tag (0xD1) an old client never
|
||||
/// receives (double-gated caps), a new 0xCD kind (0x06, dropped as unknown by old clients) and
|
||||
/// arrival flag bits 8/9 sent only toward a capable host, so [`WIRE_VERSION`] is unchanged.
|
||||
/// v17: added `punktfunk_connection_game_exited` — asks, once a session has ended, whether it
|
||||
/// ended because the game the host launched for it EXITED (the host's close carried
|
||||
/// [`quic::APP_EXITED_CLOSE_CODE`], which it has sent since long before this bump; nothing
|
||||
/// consumed it). Purely a read of state the core already had: no new call is required of an
|
||||
/// embedder, a client that never calls it is unchanged, and the host sends exactly the same bytes
|
||||
/// either way, so [`WIRE_VERSION`] is unchanged.
|
||||
/// v17: added `punktfunk_connection_end_reason` + the `PUNKTFUNK_END_REASON_*` vocabulary — asks,
|
||||
/// once a session has ended, WHY: this client closed it, the host's launched game exited (its close
|
||||
/// carried [`quic::APP_EXITED_CLOSE_CODE`], which the host has sent since long before this bump
|
||||
/// with nothing consuming it), the host ended it cleanly, the host reported a failure, or the
|
||||
/// connection was simply lost. Purely a read of state the core already had: no new call is required
|
||||
/// of an embedder, a client that never calls it is unchanged, and the host sends exactly the same
|
||||
/// bytes either way, so [`WIRE_VERSION`] is unchanged.
|
||||
pub const ABI_VERSION: u32 = 17;
|
||||
|
||||
/// The punktfunk/1 **wire** version — what `Hello`/`Welcome` carry and hosts equality-check.
|
||||
|
||||
+78
-18
@@ -76,12 +76,13 @@
|
||||
// capability-gated end to end: the wire grows a new datagram tag (0xD1) an old client never
|
||||
// receives (double-gated caps), a new 0xCD kind (0x06, dropped as unknown by old clients) and
|
||||
// arrival flag bits 8/9 sent only toward a capable host, so [`WIRE_VERSION`] is unchanged.
|
||||
// v17: added `punktfunk_connection_game_exited` — asks, once a session has ended, whether it
|
||||
// ended because the game the host launched for it EXITED (the host's close carried
|
||||
// [`quic::APP_EXITED_CLOSE_CODE`], which it has sent since long before this bump; nothing
|
||||
// consumed it). Purely a read of state the core already had: no new call is required of an
|
||||
// embedder, a client that never calls it is unchanged, and the host sends exactly the same bytes
|
||||
// either way, so [`WIRE_VERSION`] is unchanged.
|
||||
// v17: added `punktfunk_connection_end_reason` + the `PUNKTFUNK_END_REASON_*` vocabulary — asks,
|
||||
// once a session has ended, WHY: this client closed it, the host's launched game exited (its close
|
||||
// carried [`quic::APP_EXITED_CLOSE_CODE`], which the host has sent since long before this bump
|
||||
// with nothing consuming it), the host ended it cleanly, the host reported a failure, or the
|
||||
// connection was simply lost. Purely a read of state the core already had: no new call is required
|
||||
// of an embedder, a client that never calls it is unchanged, and the host sends exactly the same
|
||||
// bytes either way, so [`WIRE_VERSION`] is unchanged.
|
||||
#define PUNKTFUNK_ABI_VERSION 17
|
||||
|
||||
// The punktfunk/1 **wire** version — what `Hello`/`Welcome` carry and hosts equality-check.
|
||||
@@ -1622,6 +1623,63 @@ typedef uint8_t PunktfunkInputKind;
|
||||
#endif // __STDC_VERSION__ >= 202311L
|
||||
#endif // __cplusplus
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Why a session ended — [`NativeClient::end_reason`], and `punktfunk_connection_end_reason` on the
|
||||
// C surface.
|
||||
//
|
||||
// The distinction that matters to a UI is **normal vs alarming**, and it is not a spectrum: a
|
||||
// player quitting their game and a host falling off the network both arrive as "the session
|
||||
// ended", and a client with no way to separate them has to word all of them the same. Every client
|
||||
// worded them as failures.
|
||||
//
|
||||
// Ordered loosely from "the user did this on purpose" to "something went wrong". Values are part
|
||||
// of the C ABI: append only, never renumber.
|
||||
enum PunktfunkEndReason
|
||||
#if defined(__cplusplus) || __STDC_VERSION__ >= 202311L
|
||||
: uint8_t
|
||||
#endif // defined(__cplusplus) || __STDC_VERSION__ >= 202311L
|
||||
{
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Not ended (or ended before a reason could be observed). Also what an unknown future value
|
||||
// decodes to, so an older client reading a newer core degrades to "no opinion".
|
||||
PUNKTFUNK_END_REASON_NONE = 0,
|
||||
#endif
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// **This client** closed the session — the user pressed stop, or the handle was dropped.
|
||||
// Nothing to report: the UI already knows, it initiated it.
|
||||
PUNKTFUNK_END_REASON_LOCAL = 1,
|
||||
#endif
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// The host's launched game exited ([`crate::quic::APP_EXITED_CLOSE_CODE`]). A normal finish,
|
||||
// and the one reason a launcher client can act on: go back to the library the title was
|
||||
// launched from rather than all the way out to host selection.
|
||||
PUNKTFUNK_END_REASON_GAME_EXITED = 2,
|
||||
#endif
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// The host ended the session cleanly and deliberately — an operator "End" in the console, or
|
||||
// the session simply finishing. Normal; say so plainly or say nothing.
|
||||
PUNKTFUNK_END_REASON_HOST_ENDED = 3,
|
||||
#endif
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// The host closed reporting a failure of its own. Worth showing, and the host's log has the
|
||||
// detail.
|
||||
PUNKTFUNK_END_REASON_HOST_ERROR = 4,
|
||||
#endif
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// The connection died rather than being closed: idle timeout, reset, the network going away.
|
||||
// This — and only this — is the "the host may be asleep, wake it" case.
|
||||
PUNKTFUNK_END_REASON_LOST = 5,
|
||||
#endif
|
||||
};
|
||||
#ifndef __cplusplus
|
||||
#if __STDC_VERSION__ >= 202311L
|
||||
typedef enum PunktfunkEndReason PunktfunkEndReason;
|
||||
#else
|
||||
typedef uint8_t PunktfunkEndReason;
|
||||
#endif // __STDC_VERSION__ >= 202311L
|
||||
#endif // __cplusplus
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Per-session colour signalling (CICP / ITU-T H.273 code points) the host resolved for the
|
||||
// encoded video, carried on [`Welcome`]. A client configures its decoder/presenter from these
|
||||
@@ -2505,23 +2563,25 @@ PunktfunkStatus punktfunk_connection_audio_channels(PunktfunkConnection *c, uint
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
// Did this session end because **the game the host launched for it exited**? `*out` is set to 1
|
||||
// when it did and 0 otherwise; the return status reports only whether the handle was usable.
|
||||
// WHY this session ended: `*out` receives a [`PunktfunkEndReason`] byte
|
||||
// (`PUNKTFUNK_END_REASON_*`). The return status reports only whether the handle was usable.
|
||||
//
|
||||
// A refinement of "the session ended", never a substitute — read it only once a plane has
|
||||
// returned [`PunktfunkStatus::Closed`] (or the embedder's own end-of-session signal fired), and
|
||||
// treat 0 as "ended for some other reason" (user stop, host gone, network loss, idle timeout).
|
||||
// It latches, so it is still readable while the connection is being torn down, and a client that
|
||||
// never calls it behaves exactly as it did before this existed.
|
||||
// Read it once a plane has returned [`PunktfunkStatus::Closed`] (or the embedder's own
|
||||
// end-of-session signal fired); before that it reads `NONE`. It latches, so it is still readable
|
||||
// while the connection is torn down, and a client that never calls it behaves exactly as it did
|
||||
// before this existed.
|
||||
//
|
||||
// The point is that a game ending is a normal finish, not a failure: a launcher client can send
|
||||
// the player back to the host's library — one tap from the next title — rather than reporting an
|
||||
// error and dropping to host selection for something the player just did on purpose.
|
||||
// **Most endings are not failures.** Before this, a client had no way to tell a player quitting
|
||||
// their game from a host falling off the network, so every client wrote one message for all of
|
||||
// them and every client chose an error. Use `LOCAL`/`GAME_EXITED`/`HOST_ENDED` to stay quiet (and
|
||||
// `GAME_EXITED` to return to the library the title was launched from), and keep the alarming copy
|
||||
// for `HOST_ERROR` and `LOST`.
|
||||
//
|
||||
// Treat an unrecognized value as `NONE` — this crosses an ABI and the core may be newer than you.
|
||||
//
|
||||
// # Safety
|
||||
// `c` is a valid connection handle; `out` is NULL or writable for one `u8`.
|
||||
PunktfunkStatus punktfunk_connection_game_exited(PunktfunkConnection *c,
|
||||
uint8_t *out);
|
||||
PunktfunkStatus punktfunk_connection_end_reason(PunktfunkConnection *c, uint8_t *out);
|
||||
#endif
|
||||
|
||||
#if defined(PUNKTFUNK_FEATURE_QUIC)
|
||||
|
||||
Reference in New Issue
Block a user