Skip to content

Commit 9697e6b

Browse files
committed
More ergonomics
1 parent ba91851 commit 9697e6b

5 files changed

Lines changed: 377 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
### Added
1818

19+
- `GameStateCell::load_or_err()` method for strict state loading with proper error handling
20+
- `InvalidFrameReason::MissingState` variant for clearer error messages when loading missing states
21+
- `SessionBuilder::with_lan_defaults()` preset for low-latency LAN play
22+
- `SessionBuilder::with_internet_defaults()` preset for typical online play
23+
- `SessionBuilder::with_high_latency_defaults()` preset for mobile/unstable connections
1924
- `Frame` ergonomic methods for safe arithmetic and conversion:
2025
- `as_usize()`, `try_as_usize()` — convert to usize with Option/Result
2126
- `buffer_index(size)`, `try_buffer_index(size)` — ring buffer index calculation

examples/configuration.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,28 @@ fn basic_configuration() {
7777
}
7878

7979
/// Using built-in presets for common network conditions
80+
///
81+
/// NOTE: For even simpler configuration, use the new convenience presets:
82+
/// - `with_lan_defaults()` - LAN/local play with minimal latency
83+
/// - `with_internet_defaults()` - Typical online play (2-frame input delay)
84+
/// - `with_high_latency_defaults()` - Mobile/unstable connections (4-frame input delay)
85+
///
86+
/// Example:
87+
/// ```ignore
88+
/// SessionBuilder::<GameConfig>::new()
89+
/// .with_num_players(2).unwrap()
90+
/// .with_lan_defaults()
91+
/// .add_local_player(0).unwrap()
92+
/// .add_remote_player(1, addr).unwrap()
93+
/// .start_p2p_session(socket)
94+
/// ```
8095
fn network_presets() {
8196
println!("--- Network Presets ---");
97+
println!("TIP: Use with_lan_defaults(), with_internet_defaults(), or");
98+
println!(" with_high_latency_defaults() for quick configuration!\n");
8299

83100
// LAN/Local play - fast connections, minimal latency
101+
// Equivalent to: .with_lan_defaults()
84102
let lan_builder = SessionBuilder::<GameConfig>::new()
85103
.with_num_players(2)
86104
.unwrap()
@@ -99,6 +117,7 @@ fn network_presets() {
99117
println!(" Builder: {:?}\n", lan_builder);
100118

101119
// Regional internet (20-80ms RTT)
120+
// Equivalent to: .with_internet_defaults().with_max_prediction_window(8)
102121
let regional_builder = SessionBuilder::<GameConfig>::new()
103122
.with_num_players(2)
104123
.unwrap()
@@ -116,6 +135,7 @@ fn network_presets() {
116135
println!(" Builder: {:?}\n", regional_builder);
117136

118137
// High-latency networks (80-200ms RTT)
138+
// Similar to: .with_high_latency_defaults().with_max_prediction_window(12)
119139
let high_latency_builder = SessionBuilder::<GameConfig>::new()
120140
.with_num_players(2)
121141
.unwrap()

src/error.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ pub enum InvalidFrameReason {
154154
},
155155
/// Frame is NULL or negative (general validation).
156156
NullOrNegative,
157+
/// No saved state exists for this frame.
158+
///
159+
/// Returned when attempting to load a game state that was never saved.
160+
/// This typically indicates a programming error — [`LoadGameState`] requests
161+
/// should only be issued for frames that were previously saved via
162+
/// [`SaveGameState`].
163+
///
164+
/// [`LoadGameState`]: crate::FortressRequest::LoadGameState
165+
/// [`SaveGameState`]: crate::FortressRequest::SaveGameState
166+
MissingState,
157167
/// Custom reason (fallback for API compatibility).
158168
Custom(&'static str),
159169
}
@@ -192,6 +202,7 @@ impl Display for InvalidFrameReason {
192202
)
193203
},
194204
Self::NullOrNegative => write!(f, "frame is NULL or negative"),
205+
Self::MissingState => write!(f, "no saved state exists for this frame"),
195206
Self::Custom(s) => write!(f, "{}", s),
196207
}
197208
}
@@ -1336,6 +1347,16 @@ mod tests {
13361347
assert!(display.contains("50"));
13371348
}
13381349

1350+
#[test]
1351+
fn invalid_frame_reason_missing_state_display() {
1352+
let reason = InvalidFrameReason::MissingState;
1353+
let display = format!("{reason}");
1354+
assert!(
1355+
display.contains("saved state"),
1356+
"Expected 'saved state' in: {display}"
1357+
);
1358+
}
1359+
13391360
#[test]
13401361
fn test_internal_error_kind_index_out_of_bounds() {
13411362
let kind = InternalErrorKind::IndexOutOfBounds(IndexOutOfBounds {

src/sessions/builder.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,141 @@ impl<T: Config> SessionBuilder<T> {
765765
self
766766
}
767767

768+
// =========================================================================
769+
// Session Presets
770+
// =========================================================================
771+
772+
/// Applies LAN-optimized defaults for low-latency local network play.
773+
///
774+
/// This preset configures the session for minimal latency scenarios typical
775+
/// of local area networks, where RTT is typically <10ms and packet loss is rare.
776+
///
777+
/// # Configuration Applied
778+
///
779+
/// - **Sync**: Fast handshake with fewer required roundtrips ([`SyncConfig::lan()`])
780+
/// - **Protocol**: Competitive settings with quick detection ([`ProtocolConfig::competitive()`])
781+
/// - **Time Sync**: Small window for responsive sync ([`TimeSyncConfig::lan()`])
782+
/// - **Input Delay**: 0 frames (can be adjusted with [`with_input_delay()`])
783+
///
784+
/// # Example
785+
///
786+
/// ```
787+
/// use fortress_rollback::{SessionBuilder, Config};
788+
///
789+
/// # struct MyConfig;
790+
/// # impl Config for MyConfig {
791+
/// # type Input = u8;
792+
/// # type State = ();
793+
/// # type Address = std::net::SocketAddr;
794+
/// # }
795+
/// let builder = SessionBuilder::<MyConfig>::new()
796+
/// .with_lan_defaults();
797+
/// ```
798+
///
799+
/// [`SyncConfig::lan()`]: crate::SyncConfig::lan
800+
/// [`ProtocolConfig::competitive()`]: crate::ProtocolConfig::competitive
801+
/// [`TimeSyncConfig::lan()`]: crate::TimeSyncConfig::lan
802+
/// [`with_input_delay()`]: Self::with_input_delay
803+
pub fn with_lan_defaults(self) -> Self {
804+
self.with_sync_config(SyncConfig::lan())
805+
.with_protocol_config(ProtocolConfig::competitive())
806+
.with_time_sync_config(TimeSyncConfig::lan())
807+
}
808+
809+
/// Applies Internet-optimized defaults for typical online play.
810+
///
811+
/// This preset configures the session for typical internet connections with
812+
/// moderate latency (30-100ms RTT) and occasional packet loss (<5%).
813+
///
814+
/// # Configuration Applied
815+
///
816+
/// - **Sync**: Default handshake settings ([`SyncConfig::default()`])
817+
/// - **Protocol**: Default network timing ([`ProtocolConfig::default()`])
818+
/// - **Time Sync**: Default averaging window ([`TimeSyncConfig::default()`])
819+
/// - **Input Delay**: 2 frames (recommended for hiding network jitter)
820+
///
821+
/// # Errors
822+
///
823+
/// Returns [`FortressError::InvalidRequest`] if the input delay cannot be set
824+
/// (e.g., if the input queue is too small).
825+
///
826+
/// # Example
827+
///
828+
/// ```
829+
/// use fortress_rollback::{SessionBuilder, Config, FortressError};
830+
///
831+
/// # struct MyConfig;
832+
/// # impl Config for MyConfig {
833+
/// # type Input = u8;
834+
/// # type State = ();
835+
/// # type Address = std::net::SocketAddr;
836+
/// # }
837+
/// let builder = SessionBuilder::<MyConfig>::new()
838+
/// .with_internet_defaults()?;
839+
/// # Ok::<(), FortressError>(())
840+
/// ```
841+
///
842+
/// [`SyncConfig::default()`]: crate::SyncConfig::default
843+
/// [`ProtocolConfig::default()`]: crate::ProtocolConfig::default
844+
/// [`TimeSyncConfig::default()`]: crate::TimeSyncConfig::default
845+
/// [`FortressError::InvalidRequest`]: crate::FortressError::InvalidRequest
846+
pub fn with_internet_defaults(self) -> Result<Self, FortressError> {
847+
self.with_sync_config(SyncConfig::default())
848+
.with_protocol_config(ProtocolConfig::default())
849+
.with_time_sync_config(TimeSyncConfig::default())
850+
.with_input_delay(2)
851+
}
852+
853+
/// Applies mobile/high-latency defaults for unstable connections.
854+
///
855+
/// This preset configures the session for challenging network conditions
856+
/// typical of mobile/cellular networks or high-latency connections:
857+
/// - High RTT (100-300ms)
858+
/// - Variable latency (high jitter)
859+
/// - Intermittent packet loss (5-20%)
860+
/// - Connection handoffs (WiFi/cellular switches)
861+
///
862+
/// # Configuration Applied
863+
///
864+
/// - **Sync**: Robust handshake with more retries ([`SyncConfig::mobile()`])
865+
/// - **Protocol**: Tolerant settings with larger buffers ([`ProtocolConfig::mobile()`])
866+
/// - **Time Sync**: Large window for stable sync ([`TimeSyncConfig::mobile()`])
867+
/// - **Input Queue**: Larger queue for longer history ([`InputQueueConfig::high_latency()`])
868+
/// - **Input Delay**: 4 frames (helps absorb jitter spikes)
869+
///
870+
/// # Errors
871+
///
872+
/// Returns [`FortressError::InvalidRequest`] if the input delay cannot be set.
873+
///
874+
/// # Example
875+
///
876+
/// ```
877+
/// use fortress_rollback::{SessionBuilder, Config, FortressError};
878+
///
879+
/// # struct MyConfig;
880+
/// # impl Config for MyConfig {
881+
/// # type Input = u8;
882+
/// # type State = ();
883+
/// # type Address = std::net::SocketAddr;
884+
/// # }
885+
/// let builder = SessionBuilder::<MyConfig>::new()
886+
/// .with_high_latency_defaults()?;
887+
/// # Ok::<(), FortressError>(())
888+
/// ```
889+
///
890+
/// [`SyncConfig::mobile()`]: crate::SyncConfig::mobile
891+
/// [`ProtocolConfig::mobile()`]: crate::ProtocolConfig::mobile
892+
/// [`TimeSyncConfig::mobile()`]: crate::TimeSyncConfig::mobile
893+
/// [`InputQueueConfig::high_latency()`]: crate::InputQueueConfig::high_latency
894+
/// [`FortressError::InvalidRequest`]: crate::FortressError::InvalidRequest
895+
pub fn with_high_latency_defaults(self) -> Result<Self, FortressError> {
896+
self.with_sync_config(SyncConfig::mobile())
897+
.with_protocol_config(ProtocolConfig::mobile())
898+
.with_time_sync_config(TimeSyncConfig::mobile())
899+
.with_input_queue_config(InputQueueConfig::high_latency())
900+
.with_input_delay(4)
901+
}
902+
768903
/// Consumes the builder to construct a [`P2PSession`] and starts synchronization of endpoints.
769904
/// # Errors
770905
/// - Returns [`InvalidRequest`] if insufficient players have been registered.
@@ -1290,4 +1425,105 @@ mod tests {
12901425
.handles
12911426
.contains_key(&PlayerHandle::new(1)));
12921427
}
1428+
1429+
// ========================================================================
1430+
// Session Preset Tests
1431+
// ========================================================================
1432+
1433+
#[test]
1434+
fn with_lan_defaults_returns_valid_builder() {
1435+
// Arrange & Act: Create builder with LAN preset
1436+
let builder = SessionBuilder::<TestConfig>::new()
1437+
.with_num_players(2)
1438+
.unwrap()
1439+
.with_lan_defaults();
1440+
1441+
// Assert: Builder is in a valid state and can accept players
1442+
let result = builder.add_local_player(0);
1443+
assert!(result.is_ok());
1444+
}
1445+
1446+
#[test]
1447+
fn with_internet_defaults_returns_valid_builder() {
1448+
// Arrange & Act: Create builder with internet preset
1449+
let builder = SessionBuilder::<TestConfig>::new()
1450+
.with_num_players(2)
1451+
.unwrap()
1452+
.with_internet_defaults()
1453+
.expect("with_internet_defaults should succeed");
1454+
1455+
// Assert: Builder is in a valid state and can accept players
1456+
let result = builder.add_local_player(0);
1457+
assert!(result.is_ok());
1458+
}
1459+
1460+
#[test]
1461+
fn with_high_latency_defaults_returns_valid_builder() {
1462+
// Arrange & Act: Create builder with high-latency preset
1463+
let builder = SessionBuilder::<TestConfig>::new()
1464+
.with_num_players(2)
1465+
.unwrap()
1466+
.with_high_latency_defaults()
1467+
.expect("with_high_latency_defaults should succeed");
1468+
1469+
// Assert: Builder is in a valid state and can accept players
1470+
let result = builder.add_local_player(0);
1471+
assert!(result.is_ok());
1472+
}
1473+
1474+
#[test]
1475+
fn with_lan_defaults_applies_expected_configs() {
1476+
// Arrange & Act
1477+
let builder = SessionBuilder::<TestConfig>::new().with_lan_defaults();
1478+
1479+
// Assert: Verify the preset applied the expected configuration
1480+
assert_eq!(builder.sync_config, SyncConfig::lan());
1481+
assert_eq!(builder.protocol_config, ProtocolConfig::competitive());
1482+
assert_eq!(builder.time_sync_config, TimeSyncConfig::lan());
1483+
}
1484+
1485+
#[test]
1486+
fn with_internet_defaults_applies_expected_configs() {
1487+
// Arrange & Act
1488+
let builder = SessionBuilder::<TestConfig>::new()
1489+
.with_internet_defaults()
1490+
.expect("with_internet_defaults should succeed");
1491+
1492+
// Assert: Verify the preset applied the expected configuration
1493+
assert_eq!(builder.sync_config, SyncConfig::default());
1494+
assert_eq!(builder.protocol_config, ProtocolConfig::default());
1495+
assert_eq!(builder.time_sync_config, TimeSyncConfig::default());
1496+
assert_eq!(builder.input_delay, 2);
1497+
}
1498+
1499+
#[test]
1500+
fn with_high_latency_defaults_applies_expected_configs() {
1501+
// Arrange & Act
1502+
let builder = SessionBuilder::<TestConfig>::new()
1503+
.with_high_latency_defaults()
1504+
.expect("with_high_latency_defaults should succeed");
1505+
1506+
// Assert: Verify the preset applied the expected configuration
1507+
assert_eq!(builder.sync_config, SyncConfig::mobile());
1508+
assert_eq!(builder.protocol_config, ProtocolConfig::mobile());
1509+
assert_eq!(builder.time_sync_config, TimeSyncConfig::mobile());
1510+
assert_eq!(builder.input_queue_config, InputQueueConfig::high_latency());
1511+
assert_eq!(builder.input_delay, 4);
1512+
}
1513+
1514+
#[test]
1515+
fn presets_are_chainable_with_other_methods() {
1516+
// Arrange & Act: Chain preset with additional configuration
1517+
let builder = SessionBuilder::<TestConfig>::new()
1518+
.with_num_players(4)
1519+
.unwrap()
1520+
.with_lan_defaults()
1521+
.with_max_prediction_window(6)
1522+
.with_desync_detection_mode(DesyncDetection::Off);
1523+
1524+
// Assert: Both preset and subsequent configs applied
1525+
assert_eq!(builder.sync_config, SyncConfig::lan());
1526+
assert_eq!(builder.max_prediction, 6);
1527+
assert_eq!(builder.desync_detection, DesyncDetection::Off);
1528+
}
12931529
}

0 commit comments

Comments
 (0)