repame_shell/portal/mod.rs
1//! Portal integration: ads, cloud saves, host pause, lifecycle mute and the
2//! host-facing gameplay notices, in one place.
3//!
4//! A portal is a host a game runs inside (Playgama, Yandex Games, CrazyGames,
5//! and the rest). Six games in this family ship a byte-identical
6//! `sim/platform.rs` and an `app/platform.rs` that differs between two of them
7//! only in three log strings: ads with timeouts, cloud-save push and pull, the
8//! host's pause, the lifecycle mute, and a `time_scale` gate copy-pasted into
9//! every frame loop that touches the clock. This module is that code, once.
10//!
11//! # The three pieces
12//!
13//! - [`Portal`] is one backend: the adapter that speaks to a host. The Playgama
14//! one lives in `portal::playgama` behind the `playgama` feature and is off by
15//! default. [`NoPortal`] is the desktop backend, and it refuses every request
16//! with a typed reason instead of pretending.
17//! - [`PortalHost`] is the state machine the game owns. It asks the backend,
18//! absorbs what the backend reports, owns the ad deadline and the rate limit
19//! the host enforces, and answers one question per frame: does gameplay run,
20//! does the world tick, does sound play.
21//! - [`Suspend`] is that answer, and [`Suspend::apply`] is the only place
22//! `Sim::time_scale` is written.
23//!
24//! # The no-fabrication rule
25//!
26//! A host that cannot serve an ad or store a cloud save says so **now**, with a
27//! typed [`Refusal`]: nothing waits on a callback that will not come, and
28//! nothing reports a success it did not get. Every request is answered on the
29//! frame it was made, and everything that was taken ends in an [`AdOutcome`] or
30//! a cloud answer:
31//!
32//! - an ad that is not supported, or that another break is already in the way
33//! of, is refused before it starts ([`AdRefused`]);
34//! - an ad the host accepted and never ended is released by its deadline, and
35//! reported as [`AdOutcome::TimedOut`]: a break the host forgot cannot hold
36//! the player;
37//! - a rewarded ad pays only when the host reported the reward point, never on
38//! the strength of having asked ([`AdOutcome::pays`]);
39//! - an empty or non-text cloud payload is refused rather than stored, because
40//! the platform reads an empty value as a delete;
41//! - with no host at all, [`NoPortal`] still answers every request, with
42//! [`Refusal::NoPortal`], rather than going quiet.
43//!
44//! # What a game writes
45//!
46//! ```ignore
47//! // Once, at boot: the web after the SDK loaded, the desktop plain.
48//! let mut portal = PortalHost::pull_first(Box::new(backend));
49//! portal.loaded();
50//!
51//! // Once per frame, in this order.
52//! portal.update(dt); // one clock for ad deadlines
53//! let gate = portal.gate(GameState {
54//! playing: run.playing(),
55//! transitional: run.transitional(),
56//! modal: false,
57//! muted: save.data.muted,
58//! });
59//! gate.apply(&mut sim); // the one writer of time_scale
60//! if gate.live() { schedule.run(&mut sim.world); }
61//! portal.ready(); // first playable frame: game_ready
62//! for report in portal.take_reports() { /* ad outcomes, cloud answers */ }
63//! ```
64//!
65//! The gate replaces three hand-written lines that the games keep at every call
66//! site that touches the clock (`live`, `suspended`, `time_scale`), and
67//! [`Suspend::live`] replaces the fourth that decides whether the gameplay
68//! schedule runs at all.
69//!
70//! `repame-fx`'s `SimDriver::frame(step_fx, blocked, tick_sim, clear_edges)`
71//! encodes the sequencing half of the same problem (step ungated fx first, then
72//! tick gameplay only when open, and roll the input edges when it is not).
73//! [`Suspend::suspended`] is exactly its `blocked` argument, so the two
74//! compose: the gate decides, the driver sequences. The driver has no opinion
75//! about run state, ads, modals, mute or lifecycle, and it does not own the
76//! clock, which is why the gate is here and not there.
77
78mod gate;
79mod host;
80pub mod lifecycle;
81mod none;
82#[cfg(feature = "playgama")]
83pub mod playgama;
84mod protocol;
85
86pub use gate::{Cause, GameState, PortalView, Suspend};
87pub use host::{AdOutcome, AdRefused, CloudRefused, PortalHost, PortalReport};
88pub use none::NoPortal;
89pub use protocol::{
90 AdEnding, AdKind, AdSchedule, Availability, HostNotice, INTERSTITIAL_TIMEOUT, Portal,
91 PortalEvent, PortalRequest, PortalStatus, REWARDED_TIMEOUT, Refusal,
92};