Code Monkey home page Code Monkey logo

ggrs's Introduction

GGRS LOGO

crates.io GitHub Workflow Status

P2P Rollback Networking in Rust

GGRS (good game rollback system) is a reimagination of the GGPO network SDK written in 100% safe Rust ๐Ÿฆ€. The callback-style API from the original library has been replaced with a much saner, simpler control flow. Instead of registering callback functions, GGRS returns a list of requests for the user to fulfill.

If you are interested in integrating rollback networking into your game or just want to chat with other rollback developers (not limited to Rust), check out the GGPO Developers Discord!

Live Demonstrations

GGRS has two demo apps you can try in the browser! One written with macroquad, the other written with bevy. Both use matchbox. Try it out with a friend! Just click the link and match with another player! (You can also open the link in two separate windows to play against yourself)

๐Ÿšง MATCHMAKING CURRENTLY OFFLINE! ๐Ÿšง

Projects using GGRS

Getting Started

To get started with GGRS, check out the following resources:

Development Status

GGRS is in an early stage, but the main functionality for multiple players and spectators should be quite stable. See the Changelog for the latest changes, even those yet unreleased on crates.io! If you want to contribute, check out existing issues, as well as the contribution guide!

Useful Links

Bevy Plugin

The authors of GGRS are also maintaining a bevy plugin. Check it out:

WASM support through WebRTC sockets

If you are interested to run a GGRS application in your browser, check the amazing Matchbox project! The matchbox sockets are compatible with GGRS through a feature flag:

Godot Wrapper

Godot is a popular open-source game engine. marcello505 is developing a wrapper for GGRS. Find the repository here:

Other Rollback Implementations in Rust

Also take a look at the awesome backroll project, a completely async rollback library. Special thanks to james7132 for a lot of inspiration and fruitful discussion.

Licensing

GGRS is dual-licensed under either

at your option.

ggrs's People

Contributors

cscorley avatar gschup avatar heatxd avatar implode-nz avatar jasper310899 avatar johanhelsing avatar maxcwhitehead avatar nelson137 avatar praxtube avatar striezel avatar vrixyz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ggrs's Issues

Provide information about the confirmation state of a frame advance

Is your feature request related to a problem? Please describe.
For a better UX, I may only want to play effects when an event in my game won't be unwound due to being out of sync. For example, in my fighting game, I only want to play the hit sound effect when I have all inputs from all players and my hit has definitely landed.

Describe the solution you'd like
My best idea of the interface would be an additional enum in the GGRSRequest::AdvanceFrame variant. Likely something like the following:

enum FrameConfirmation {
  LocalPrediction,
  FullyConfirmed,
  PartiallyConfirmed(Vec<PlayerHandle>),
}

Where the PartiallyConfirmed case includes the set of players that we have inputs for. The PartiallyConfirmed case is unnecessary, but may support some use cases.

An additional consideration is that we would likely want to run additional loads/updates that we otherwise might not to ensure that every frame advance is called at least (ideally exactly) once with FrameConfirmation::FullyConfirmed. Because of this additional cost, we may want this to be opt-in.

Describe alternatives you've considered
Alternatively, GameInput could include data about being either confirmed from the remote player or predicted based on their previous inputs (I assume ggrs is doing this). Then one could likely know that a game state will never be invalidated by checking that all GameInputs are confirmed.

Desync Detection Mode not behaving as expected

Describe the bug
I would have expected the with_desync_detection_mode to rollback every second or some specific time interval, however in my case it only rollsback at the very start and then it just stops. I am not sure if this is the intended behaviour, but from what I can understand from the docs it should rollback in intervals?

It's a bit hard to see in the picture below, but the console output indicates no syncing errors even though the two peers are clearly desynced.

No sync issues even though we are cleary desynced

To Reproduce
I followed the example as best as I could, essentially

  • Add with_desync_detection_mode
  • Add print_events_system to Update
  • Trigger desync on purpose to get error messages (which didn't happen)

It's likely that I forgot something.
I also tried to get desyncs in the example game, but that didn't work.

Expected behavior
I expected for the desync to run continuously. I am also not really sure what the difference between start_synctest_session and with_desync_detection_mode is. Shouldn't they both test checksums for equality and report errors?

Desktop
OS: Zorin OS 16.3 x86_64 (Ubuntu 16 based)

Also this isn't really a bug report, but rather a what the hell is happening, please help me report, but that label doesn't exist unfortunately.

Multiple Local Players not allowed, but should be already supported

Describe the bug
Looking at the SessionBuilder, I just realized that we do not allow multiple local players.
I am however quite sure I implemented this functionality ages ago. The endpoints are set up to handle more than one player per endpoint, which requires more than one local player one at least one of the clients.

Expected behavior
Adding more than one local player should be allowed. In the best case, this check can just be removed.

What to do
Write a test case for this and maybe even extend the example. See if this actually works.

let users set FPS of their application

Is your feature request related to a problem? Please describe.
Currently, ping and frame advantage calculations are hardcoded with 60 FPS in mind.

Describe the solution you'd like
I would like the user be able to set their tickrate with a setter-method.

Input question

I am evaluating ggrs and admittedly, I havent looked through the code entirely yet.

I am looking into using ggrs with Matchbox. How can I record confirmed inputs so that I can later replay an entire deterministic simulation accurately? At what point in the cycle should I be doing this and how?

Thanks.

[Question] Is the SyncTest Mode Suitable for General Purpose Local Play?

I was just wondering whether the sync test mode makes sense in the context of local play, and whether setting the check distance to 0 or something similar can be done to avoid any extra overhead of rolling back.

Edit: Sorry I opened an issue, I just realized you had discussions enabled. This should be converted to a discussion.

Custom input prediction function?

Is your feature request related to a problem? Please describe.

In some cases, it would be convenient to be able to send one-off events as part of the input struct:

struct Input {
    fire_just_pressed: bool,
    jump_held: bool,
}

However, when predicting, we would then get fire_just_presed == true repeatedly when predicting.

Describe the solution you'd like

Instead of just copying the input struct, perhaps it would make sense to be able to inject a prediction function?

session_builder.with_input_prediction(|input| {
    Input { fire_just_pressed: false, ..input }
})

let prediction_to_return = self.prediction; // PlayerInput has copy semantics

Describe alternatives you've considered

Handling just_pressed logic inside the ggrs advance frame step works as a workaround, but sometimes the events you get from game engines are one-offs (bevy ui button press events etc.), so it generates a lot of boilerplate

Additional context

https://discord.com/channels/844211600009199626/844211600009199629/1175361006717898763

unnecessary dropping of Input packets

Currently, when a received input packet has been encoded with a different reference input than the receiver expects, the whole encoded input sequence is simply dropped.

In these cases, the correct encoding reference should only be an earlier input. If we kept more than just one reference inside src/network/udp_protocol.rs, we could find the correct decoding reference (same frame as body.start_frame) and decode the input instead of dropping it.

This should reduce network usage with the added cost of saving and managing the last n inputs instead of only one.

Relevant code section

UDP sockets not working with IPv6

Currently, the library is completely untested with IPv6 addresses. Theoretically, rusts SocketAddr should be able to handle both IPv6 and IPv4, but it is unclear if the current setup, especially here, works properly.

Allow variably sized encoded inputs

Is your feature request related to a problem? Please describe.
The input for the game I'm working on is difficult to serialize into a fixed size Vec<u8>. Each player has different abilities that change over the course of the game. I don't want each client to have to keep track of the input mapping for each player, or worse, the UI for changing input mapping. I'd like to basically serialize the following:

struct Input {
  desired_movement: Vec2,
  actions: Vec<Action>,
}

enum Action {
  Attack,
  UseAbility(AbilityId),
  InteractWithObject(ObjectId),
  // etc.
}

I'd certainly serialize using something like bincode to minimize the bytes, but I can't in general use a fixed size input.

Describe the solution you'd like
I'd like the ability to specify that my inputs are variably sized. Probably having constructors take an explicit enum or Option<usize>. For variably sized inputs, the Vec<u8> can be prefixed with a compact representation of the size, at it should usually be small. We could use the existing SCALE codec's Compact<T>.

Describe alternatives you've considered
I could have a queue of actions and send whatever I can fit each frame. That could potentially add unwanted delay to actions and would interfere with the ability to "hold down" an action. I'd additionally likely need to write a custom serialize implementation for my actions as a naive one would likely be too large.

Alternatively, remove MAX_INPUT_BYTES and allow the user to determine the fixed size large enough to fit their inputs.

Additional context
Add any other context or screenshots about the feature request here.

rare desyncing issue

While playing around with clumsy and these settings:
clumsy_settings

I was (after several minutes of running the game) able to desync the BoxGame clients, but could never replicate it. I don't know how that happened.

Important notice: when affecting loopback packets (localhost -> localhost), all clumsy settings affect these packets twice, meaning that a 50ms delay is actually a 100ms delay, leading to a 200ms round-trip time.

Build Error: the trait `Sized` is not implemented

Describe the bug

I am following the extreme bevy tutorial. When I add bevy_ggrs as a dependency, ggrs fails to compile with the following error:

error[E0277]: the size for values of type `dyn StdError + Send + Sync` cannot be known at compilation time
  --> /home/axelmagn/.cargo/registry/src/index.crates.io-6f17d22bba15001f/ggrs-0.10.1/src/network/compression.rs:37:41
   |
37 |     let buf = bitfield_rle::decode(data)?;
   |                                         ^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn StdError + Send + Sync`, which is required by `Result<Vec<Vec<u8>>, Box<dyn StdError>>: FromResidual<Result<Infallible, Box<dyn StdError + Send + Sync>>>`
   = help: the following other types implement trait `FromResidual<R>`:
             <Result<T, F> as FromResidual<Yeet<E>>>
             <Result<T, F> as FromResidual<Result<Infallible, E>>>
   = note: required for `Box<dyn StdError + Send + Sync>` to implement `StdError`
   = note: required for `Box<dyn StdError>` to implement `From<Box<dyn StdError + Send + Sync>>`
   = note: required for `Result<Vec<Vec<u8>>, Box<dyn StdError>>` to implement `FromResidual<Result<Infallible, Box<dyn StdError + Send + Sync>>>`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `ggrs` (lib) due to 1 previous error

To Reproduce

  • follow extreme bevy tutorial up to the line bevy_ggrs = { version = "0.15.0", features = ["wasm-bindgen"] }
  • toolchain: stable-x86_64-unknown-linux-gnu
  • rustc version: rustc 1.77.2 (25ef9e3d8 2024-04-09)
  • target: wasm32-unknown-unknown

Expected behavior
ggrs builds successfully.

Screenshots
N/A

Desktop (please complete the following information):

  • OS: WSL2 inside of Windows 11
  • Version: Windows 11, version 23H2

Additional context

I'm not super familiar with ggrs as a library, but I would be happy to contribute towards the fix if someone could diagnose the problem and mentor me in the right direction.

Rollback requests ignored when `max_predictions` number of local inputs is saved

Describe the bug

Take a sample situation that can occur in a P2P session in laggy conditions:

  1. A peer sends you input different than you predicted.
  2. In adjust_gamestate you schedule a rollback/LoadGameState request and modify the session's internal state in preparation for it.
  3. However, because of the laggy conditions, you already have max_predictions number of local inputs stored so you drop all created request and instead return a PredictionThreshold error from the advance_frame/add_local_input methods.
  4. On the next advance_frame call, since the session state was modified in preparation for a rollback as if the rollback was guaranteed to happen, you now think there is no need to rollback anymore.

Following these steps leads to a desync as you received a peer's input that didn't match your prediction but didn't rollback.

Expected behavior

In the described situation I would expect the next advance_frame call to remake the requests which were cancelled in the last frame, including the rollback one.

Default predicted input on first frame (zeroed Input) assumes zeroed is equivalent to no movement

Describe the bug
When GGRS provides a predicted input for a client with no previous inputs on session startup, it uses a zeroed value of PlayerInput.

ggrs/src/frame_info.rs

Lines 56 to 61 in 0af1a04

pub(crate) fn blank_input(frame: Frame) -> Self {
Self {
frame,
input: I::zeroed(),
}
}

If the user provided input type does not represent no input when zeroed, this seemed to sometimes lead to desync on session start. Not 100% sure if it actually makes sense that this should've caused desync though, perhaps should have been rolled back on remote once actual input was replicated.

We were representing movement input between [-1.0, 1.0] quantized to integer such that 0 bits represented -1.0 movement and ran into this issue. Changing representation such that zeored represents 0.0 or no movement on axis resolved the issue.

Expected behavior
It might be beneficial to document this assumption on ggrs::Config::Input. Another option is to require Default trait and use this for blank input.

local_frame_advantage bigger than i8::MAX

Describe the bug
Calculations for quality reports use i8 and can be too small during stress testing.

To Reproduce
Steps to reproduce the behavior:

  • Using https://github.com/cscorley/bevy_ggrs_rapier_example
  • Compile using release build
  • Setup a game and introduce aggressive network lag such that 127+ "Skipping a frame: PredictionThreshold" will be produced on the clients
  • Observe panic thread 'main' panicked at 'local_frame_advantage bigger than i8::MAX: TryFromIntError(())', C:\Users\cscorley\scoop\persist\rustup\.cargo\git\checkouts\ggrs-151f8bd9eb75dc5d\9e4a20a\src\network\protocol.rs:512:18

Expected behavior
Non-panic, just keep skipping frames.

Screenshots
I was testing a game with clumsy on Windows, using this configuration.
image

Desktop (please complete the following information):

  • OS: Windows 10
  • Version: main branch, 9e4a20a

Additional context

Allow Optimizing `NonBlockingSocket` for Message Broadcasts

Is your feature request related to a problem? Please describe.
For my game I'm using a proxy server to route all of the GGRS messages to each peer, instead of using real p2p connections. It occurred to me that for things like sending user input, each client is probably going to broadcast it's input to all the other clients, which could be done in a more bandwidth efficient way with a proxy duplicating the message at the proxy side, instead of multiple copies of the user input to the proxy server over and over.

Describe the solution you'd like
I'm not sure about the internals of GGRS, but I was wondering if you thought it makes sense to have a special case for sending messages that will be broadcast to all peers, in the NonBlockingSocket trait. That would allow me to reduce bandwidth for all broadcast messages by sending a different kind of message to my proxy server for broadcasts, than for point-to-point messages.

Describe alternatives you've considered
Maybe the internal logic for GGRS doesn't fit well to distinguishing between a broadcast and sending to specific peers. In that case this wouldn't really be needed.

Additional context
This is pretty much just a nice-to-have, so it's up for debate on whether or not it makes sense to implement.

As an thought on the API, we could add an extra broadcast method, that would have a default implementation that just loops over all the clients and calls send_to for each message. That way the default functionality makes sense, while still allowing the implementer to specialize if there is a benefit for their transport.

Update Wiki

A lot has changed since the last time I updated the Wiki. This issue is just a reminder.

WebRTC Support

Since the UDP bindings are just a single file, it shouldn't be too bad to add webrtc support. The problem with that is only that webrtc itself is a fickle beast, but that just comes with exporting to web.

Reconnect disconnected player

Is your feature request related to a problem? Please describe.
To make a robust networked game, we need to allow players that lose internet connection to reconnect and continue playing the game.

From playing the examples, I cannot reconnect after disconnecting.

Describe the solution you'd like
Ideally, the code in the examples would just work with clients that are catching up from far behind.

Likely the best option is for remote players to share the state snapshot and any inputs for the reconnecting player so that player can replay from that state.

Describe alternatives you've considered
Alternatively, all inputs can be recorded and a reconnecting player can replay the entire game. This does not require state to be shared, but requires more and more work as the game runs longer. Additionally, all user inputs would have to be stored from the beginning of the game.

decoding/encoding inputs leads to diverging gamestates

When running the P2P BoxGame example, the clients eventually desync, with the spectator desyncing much easier than the two players. Since the BoxGame SyncTest is running absolutely rock-solid, I assume it has to to with the transmission of the inputs.

spectator disconnects players on the wrong frame for more than two players

Describe the bug
If BoxGame is launched with more than two players and a spectator and a player disconnects, the remaining players stay synchronized, but the spectator disconnects the player on the wrong frame.

To Reproduce
Steps to reproduce the behavior:

  • launch BoxGame with 3 players and a spectator
  • disconnect a player that is not hosting the spectator

Expected behavior
all clients should remain in sync

Desktop (please complete the following information):

  • OS: Windows 10
  • Version: 64bit

Demo crash

Sorry to burst the issue template; wanted to get this out to you anyway.

Ran the demo in two chrome on the same macos, one of which was incognito.

Here are the browser logs.

One

Constructing socket...
mq_js_bundle.js:1 Starting new game...
mq_js_bundle.js:1 Event: Synchronizing { addr: "8ee20709-4dfe-4a99-8e38-4507b3008b2f", total: 5, count: 1 }
mq_js_bundle.js:1 Event: Synchronizing { addr: "8ee20709-4dfe-4a99-8e38-4507b3008b2f", total: 5, count: 2 }
mq_js_bundle.js:1 Event: Synchronizing { addr: "8ee20709-4dfe-4a99-8e38-4507b3008b2f", total: 5, count: 3 }
mq_js_bundle.js:1 Event: Synchronizing { addr: "8ee20709-4dfe-4a99-8e38-4507b3008b2f", total: 5, count: 4 }
mq_js_bundle.js:1 Event: Synchronized { addr: "8ee20709-4dfe-4a99-8e38-4507b3008b2f" }
mq_js_bundle.js:1 PanicInfo { payload: Any { .. }, message: Some(send_to failed: TrySendError { kind: Disconnected }), location: Location { file: "C:\\Users\\Georg Schuppe\\.cargo\\git\\checkouts\\matchbox-9d3f96917a0a12d8\\94986ca\\matchbox_socket\\src\\webrtc_socket\\mod.rs", line: 122, col: 14 } }
ggrs_demo_bg.wasm:0xc2284 Uncaught RuntimeError: unreachable
    at __rust_start_panic (ggrs_demo_bg.wasm:0xc2284)
    at rust_panic (ggrs_demo_bg.wasm:0xbfd06)
    at std::panicking::rust_panic_with_hook::h19d99b2b2597d628 (ggrs_demo_bg.wasm:0xaf824)
    at std::panicking::begin_panic_handler::{{closure}}::hdf90814cf3dcdd18 (ggrs_demo_bg.wasm:0xb7cda)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h1a5f05611837cc03 (ggrs_demo_bg.wasm:0xc11b5)
    at rust_begin_unwind (ggrs_demo_bg.wasm:0xbe138)
    at core::panicking::panic_fmt::hbc500cb2b2c8690c (ggrs_demo_bg.wasm:0xbf43b)
    at core::result::unwrap_failed::he5c411e7d753a069 (ggrs_demo_bg.wasm:0xb8a65)
    at <matchbox_socket::ggrs_socket::WebRtcNonBlockingSocket as ggrs::NonBlockingSocket<alloc::string::String>>::send_to::h10606d0c2a54140a (ggrs_demo_bg.wasm:0xa3f30)
    at ggrs::network::protocol::UdpProtocol<T>::send_all_messages::hb4e724e6ae75d556 (ggrs_demo_bg.wasm:0x7b0d7)

Other

Starting new game...
mq_js_bundle.js:1 Event: Synchronizing { addr: "757d8ade-bcc0-46f2-a631-076ad7683413", total: 5, count: 1 }
mq_js_bundle.js:1 Event: Synchronizing { addr: "757d8ade-bcc0-46f2-a631-076ad7683413", total: 5, count: 2 }
mq_js_bundle.js:1 Event: Synchronizing { addr: "757d8ade-bcc0-46f2-a631-076ad7683413", total: 5, count: 3 }
mq_js_bundle.js:1 Event: Synchronizing { addr: "757d8ade-bcc0-46f2-a631-076ad7683413", total: 5, count: 4 }
mq_js_bundle.js:1 Event: Synchronized { addr: "757d8ade-bcc0-46f2-a631-076ad7683413" }
mq_js_bundle.js:1 PanicInfo { payload: Any { .. }, message: Some(send_to failed: TrySendError { kind: Disconnected }), location: Location { file: "C:\\Users\\Georg Schuppe\\.cargo\\git\\checkouts\\matchbox-9d3f96917a0a12d8\\94986ca\\matchbox_socket\\src\\webrtc_socket\\mod.rs", line: 122, col: 14 } }
ggrs_demo_bg.wasm:0xc2284 Uncaught RuntimeError: unreachable
    at __rust_start_panic (ggrs_demo_bg.wasm:0xc2284)
    at rust_panic (ggrs_demo_bg.wasm:0xbfd06)
    at std::panicking::rust_panic_with_hook::h19d99b2b2597d628 (ggrs_demo_bg.wasm:0xaf824)
    at std::panicking::begin_panic_handler::{{closure}}::hdf90814cf3dcdd18 (ggrs_demo_bg.wasm:0xb7cda)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h1a5f05611837cc03 (ggrs_demo_bg.wasm:0xc11b5)
    at rust_begin_unwind (ggrs_demo_bg.wasm:0xbe138)
    at core::panicking::panic_fmt::hbc500cb2b2c8690c (ggrs_demo_bg.wasm:0xbf43b)
    at core::result::unwrap_failed::he5c411e7d753a069 (ggrs_demo_bg.wasm:0xb8a65)
    at <matchbox_socket::ggrs_socket::WebRtcNonBlockingSocket as ggrs::NonBlockingSocket<alloc::string::String>>::send_to::h10606d0c2a54140a (ggrs_demo_bg.wasm:0xa3f30)
    at ggrs::network::protocol::UdpProtocol<T>::send_all_messages::hb4e724e6ae75d556 (ggrs_demo_bg.wasm:0x7b0d7)
mq_js_bundle.js:1 Unsupported keyboard key:  MetaLeft
mq_js_bundle.js:1 PanicInfo { payload: Any { .. }, message: Some(failed to notify about open connection: TrySendError { kind: Disconnected }), location: Location { file: "C:\\Users\\Georg Schuppe\\.cargo\\git\\checkouts\\matchbox-9d3f96917a0a12d8\\94986ca\\matchbox_socket\\src\\webrtc_socket\\wasm\\message_loop.rs", line: 328, col: 14 } }
ggrs_demo_bg.wasm:0xaf81e Uncaught RuntimeError: unreachable
    at std::panicking::rust_panic_with_hook::h19d99b2b2597d628 (ggrs_demo_bg.wasm:0xaf81e)
    at std::panicking::begin_panic_handler::{{closure}}::hdf90814cf3dcdd18 (ggrs_demo_bg.wasm:0xb7cda)
    at std::sys_common::backtrace::__rust_end_short_backtrace::h1a5f05611837cc03 (ggrs_demo_bg.wasm:0xc11b5)
    at rust_begin_unwind (ggrs_demo_bg.wasm:0xbe138)
    at core::panicking::panic_fmt::hbc500cb2b2c8690c (ggrs_demo_bg.wasm:0xbf43b)
    at core::result::unwrap_failed::he5c411e7d753a069 (ggrs_demo_bg.wasm:0xb8a65)
    at matchbox_socket::webrtc_socket::wasm::message_loop::create_data_channel::{{closure}}::h50fcb51dc975bac4 (ggrs_demo_bg.wasm:0xb5f85)
    at <dyn core::ops::function::FnMut<(A,)>+Output = R as wasm_bindgen::closure::WasmClosure>::describe::invoke::h6246336136d39a8a (ggrs_demo_bg.wasm:0xc06d5)
    at __wbg_adapter_37 (ggrs_demo.js:219:10)
    at RTCDataChannel.real (ggrs_demo.js:192:20)

Support enum in `Config::Input`

I would like to use enums in Config::Input to differentiate different possible inputs.

Config::Input is requiring on Pod, which has a lot of constraints, main one is that any bit patterns should be correct, making most enum potentially unfit.

The trait Pod has been split in different traits in this PR: Lokathor/bytemuck#91 ; If I'm not mistaken, ideally CheckedBitPattern should be enough.

This should enable us to have enums within Config::Input.

Somewhat related issue: Lokathor/bytemuck#84

As noted #41 (comment), I think we're dependent on bytemuck to support more than fieldless enums.

Crate doesn't build after `cargo update`: `E0277`

To Reproduce

git clone https://github.com/gschup/ggrs
cargo check
ggrs main cargo check
    Checking ggrs v0.10.1 (J:\dev\ggrs)
error[E0277]: the size for values of type `dyn StdError + Send + Sync` cannot be known at compilation time
  --> src\network\compression.rs:37:41
   |
37 |     let buf = bitfield_rle::decode(data)?;
   |                                         ^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `dyn StdError + Send + Sync`, which is required by `Result<Vec<Vec<u8>>, Box<dyn StdError>>: FromResidual<Result<Infallible, Box<dyn StdError + Send + Sync>>>`
   = help: the following other types implement trait `FromResidual<R>`:
             <Result<T, F> as FromResidual<Yeet<E>>>
             <Result<T, F> as FromResidual<Result<Infallible, E>>>
   = note: required for `Box<dyn StdError + Send + Sync>` to implement `StdError`
   = note: required for `Box<dyn StdError>` to implement `From<Box<dyn StdError + Send + Sync>>`
   = note: required for `Result<Vec<Vec<u8>>, Box<dyn StdError>>` to implement `FromResidual<Result<Infallible, Box<dyn StdError + Send + Sync>>>`

For more information about this error, try `rustc --explain E0277`.

Expected behavior
No errors

Desktop (please complete the following information):

Remove MAX_PLAYERS

Is your feature request related to a problem? Please describe.
I'm using ggrs for a 8/9 player PvE game (8 humans, 1 server). I currently, could not construct a P2PSession with more than 4 players due to the MAX_PLAYERS limit.

Describe the solution you'd like
There should be no imposed limit on the number of players in a game. The documentation can warn about considerations and potential problem at high player counts and optionally log a warning. But ultimately, it should be up to the user of the crate to decide how many players to use ggrs with.

Additional context
It may also be fruitful to consider a hub and spoke networking topology to not have network communication scale quadratically in the number of players, letting much larger player counts be feasible.

Ability to simulate poor network conditions

Is your feature request related to a problem? Please describe.
To make a high quality, robust game, I need to be able to experiment with how it works when the local or remote player(s) have poor network conditions.

Describe the solution you'd like
I would like some mechanism to hook into the ggrs protocol and interfere with packet arrival or contents. The most flexible interface would probably be something like:

trait Interference {
  fn interfere(&mut self, session: &P2PSession, packet: Packet) -> PacketBehavior;
}

enum PacketBehavior {
  Drop,
  Process(Packet),
  Delay(Packet, Duration),
}

P2PSession having an Option<Box<dyn Interference>> field shouldn't affect performance when not using interference (as the compiler should be able to see it's always None and remove related checks.

Having it as a trait will let users override with their own impl to test the cases they're interested in. We can include some basic impls for common cases as well.

Describe alternatives you've considered
There are existing tools that interact at the ethernet or OS layer, but they are general purpose (possibly not allowing for specific problems) and difficult to configure (requiring hardware or OS-level interfacing, making it difficult for a team to have a consistent setup). Additionally, much testing is done by running multiple instances on one machine, making it unclear if OS or eth level interference would even work.

Additional context
I'm happy to implement this, I'd just like some guidance around the interface and then some guidance around where I should be changing things.

Rollback when remote player disconnects

When we receive information that a player has disconnected on a certain frame, we should rollback to that frame and resimulate the game with correct input information. This is currently simply not happening and we wrongly have a few frames of input prediction in the game simulation.

This can lead to desyncs in sessions with more than two players.

Relevant code section

PredictionThreshold error drops requests missing correction

Describe the bug

When client receives input and flags a first_incorrect frame, adjust_gamestate pushes load + advance requests to perform rollback. it then calls self.sync_layer.reset_prediction() which resets tracked first_incorrect frame.

In advance_frame when applying local input, if PredictionThreshold error is returned, function exits with error and drops the requests for the correction. Due to first incorrect frame being reset, this correction is missed and causes desync.

I'm still reasoning about exactly where the issue is / what the solution is. I believe the example games are vulnerable to this bug too.

Additional Context

Here is example of logs on a client that missed the correction (3 client game. One client missed correction for player 1's input, the 2nd applied it correctly, now players 0 and 2 are desynced). (the frame listed in input log is predicted frame, in case that is confusing). Might be more confusing than helpful, but demonstrates that both clients were notified of player 1's input, ggrs pushed requests for corrections, but the one that hit prediction threshold did not rollback, causing desync.

Player 0's logs (Missed correction on player 1's input due to prediction threshold)

  • We confirm frame 500 without rollback applied. (No log here for bones performing rollback like in second block, only prediction error)
2024-04-10T02:42:31.164609Z DEBUG bones_framework::networking: Net player(1) local: false, status: Predicted, frame: 504 input: DensePlayerControl { .0: 992, jump_pressed: false, shoot_pressed: false, grab_pressed: false, slide_pressed: false, ragdoll_pressed: false, move_direction: DenseMoveDirection(Vec2(1.0, 0.0)) }
2024-04-10T02:42:31.180776Z  WARN ggrs::input_queue: Setting first incorrect frame: 500
2024-04-10T02:42:31.180807Z  WARN ggrs::sessions::p2p_session: Requesting rollback to frame: 500
2024-04-10T02:42:31.180818Z  WARN ggrs::sessions::p2p_session: Set last confirmed frame: 498
2024-04-10T02:42:31.180823Z  WARN ggrs::sync_layer: Prediction threshold error
2024-04-10T02:42:31.180829Z  WARN bones_framework::networking: Freezing game while waiting for network to catch-up.
2024-04-10T02:42:31.214065Z  WARN ggrs::sessions::p2p_session: Set last confirmed frame: 500
2024-04-10T02:42:31.216185Z DEBUG bones_framework::networking: Net player(1) local: false, status: Predicted, frame: 505 input: DensePlayerControl { .0: 0, jump_pressed: false, shoot_pressed: false, grab_pressed: false, slide_pressed: false, ragdoll_pressed: false, move_direction: DenseMoveDirection(Vec2(0.0, 0.0)) }

Player 2's logs (correction applied correctly on player 1's input):

  • Rollback in game is logged, and player 1's next input is confirmed.
2024-04-10T02:42:31.182435Z DEBUG bones_framework::networking: Net player(1) local: false, status: Predicted, frame: 503 input: DensePlayerControl { .0: 992, jump_pressed: false, shoot_pressed: false, grab_pressed: false, slide_pressed: false, ragdoll_pressed: false, move_direction: DenseMoveDirection(Vec2(1.0, 0.0)) }
2024-04-10T02:42:31.189945Z  WARN ggrs::input_queue: Setting first incorrect frame: 500
2024-04-10T02:42:31.189965Z  WARN ggrs::sessions::p2p_session: Requesting rollback to frame: 500
2024-04-10T02:42:31.189974Z  WARN ggrs::sessions::p2p_session: Set last confirmed frame: 499
2024-04-10T02:42:31.190980Z DEBUG bones_framework::networking: Loading (rollback) frame: 500
2024-04-10T02:42:31.190999Z DEBUG bones_framework::networking: Net player(1) local: false, status: Confirmed, frame: 504 input: DensePlayerControl { .0: 0, jump_pressed: false, shoot_pressed: false, grab_pressed: false, slide_pressed: false, ragdoll_pressed: false, move_direction: DenseMoveDirection(Vec2(0.0, 0.0)) }

To Reproduce

I can repro in jumpy pretty easily / can provide steps - but I will try to write a test to repro this for testing + preventing regression once figure out what to do here.

One thing that helps repro is having 4 clients open at once (with a relay server so not p2p locally), and having 200+ ping, lots of prediction threshold errors :) (Silly way to say that poor conditions definitely bring this to light, possibly high ping + lower prediction window might do it).

should different sessions have a unified API?

Currently, P2PSession, P2PSpectatorSession and SyncTestSession have slightly different API's due to their differing functionality.

It is currently unclear to me if users would prefer a unified API instead, so they don't have to change their code drastically when launching different sessions.

Use with no predictions

Is your feature request related to a problem? Please describe.
I want to set prediction to 0.
Currently it crashs with max_prediction 0 or 1.

Describe the solution you'd like
I wish ggrs supports no predictions.

Describe alternatives you've considered
Currently I implement on udp without ggrs, but it needs to newly implement delays, synchronize, resend, and others.

Additional context
This would be useful when it is difficult to provide savestate, ex; unofficial netbattle project.

test more than two players

Currently, BoxGame only tests 2 players with n spectators. Either extending BoxGame or writing a different example alltogether to test functionality for playery > 2 would be very insightful.

Add option for comparing checksums in `P2PSession`s

Is your feature request related to a problem? Please describe.

  1. Sometimes, de-syncs will only trigger due to platform-specific differences. These issues will not be detected with a regular synctest session.
  2. In the unfortunate case that de-syncs are extremely very rare, and we can't find the cause of the bug, it would be good with some mechanism to alert the user, stop the ggrs session, ideally saving the game and syncing state from one of the peers and starting a new session.

Describe the solution you'd like

As a starting point, it would be good if there was an option on the p2p session similar to that on synctest session, that took checksums of the state and sent it to the other peers so they could compare.

Describe alternatives you've considered

Workaround: Implementing a checksum in game code and include it as part of the input struct.

Or: Just hope desyncs won't happen.

Additional context

Suggested by @Vrixyz :)

spectator catch-up when too far behind

Currently, the spectator has a queue of received host inputs it works through. When that queue overflows, the SpectatorSession returns an Error, as we cannot hold all received inputs anymore.

It would be useful to let the spectator run faster if that queue is close to being full.

The simplest solution is to just request two GGRSRequest::AdvanceFrame if the spectator is too far behind.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.