You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor!: SessionBuilder makes systems + world immutable during session build + Add a rollback-safe world reset utility (#489)
# Goals
Allow the world to be reset safely for network rollback:
## Systems + Resources and Rollback Safety
- Systems (stage-based, single success, and startup) are immutable once
session is created.
- Reset-safe initialization of Resources: World is not available during
session build, resources must be set on `SessionBuilder`. (They are
captured so that on initial startup, or after reset, can be set to
initial state).
- Completion status of "startup" and single success systems is stored in
a world resource. (`SessionStarted`, `SingleSuccessSystems`).
- This makes them rollback safe, if rollback before single success
completed, it will run again. If rollback before a reset, startup
systems will run again.
## Triggering World Reset
This may be done with the `ResetWorld` resource. `SessionRunner` is
responsible for calling `world.handle_world_reset`. It is called right
after stage exec in default/ggrs session runner, so these mutations to
world are coupled to a ggrs frame.
## Resetting Resources
During a reset, resources and components are all reset (including
`Entities` resource). Because the initial resources are captured and
saved during session build, after reset on next step during
`SystemStages` startup, the initial resources will be re-inserted.
### "Special" Resources
It turns out we have a lot of special resources, I handled a few of them
to make sure they do the right thing (or what I think makes sense
atm...)
- Shared resources are not removed during reset (just FYI).
- `Sessions`: Behind the scenes this is now inserted as a 'shared
resource', and is not wiped out during reset.
- `Time`: This is preserved. (It is assumed to exist/required by bones,
and I think resetting this may have negative side effects).
- `SessionOptions`: This is consumed by bones core loop and expected to
exist, so preserved.
- `RngGenerator`: GgrsSessionRunner is preserving this on reset, I think
resetting to initial seed after a reset may make things feel less
random, so opted to preserve.
- `SyncingInfo`: This is re-inserted by ggrs runner before each step,
this should not be impacted, no special care needed by reset.
## Session Initialization Changes (`SessionBuilder`)
Changes were made to how sessions are built to enforce system
immutability after creation, and restrict access to `World` while
building session. This has some impact (breaking changes) to API, but I
tried to make it not too painful.
`Sessions` may no longer be constructed directly. Don't want world
resources to be modified in session plugin installs, as that resource
change is then not captured in startup resources for a reset. There are
a couple different ways to make a new session outlined below.
1) `create_with` uses a closure to contain session init, this is nice as
it ensures `SessionBuilder` is finished + added to `Sessions`:
```rust
game.sessions.create_with("menu", |builder| {
builder
.install_plugin(DefaultSessionPlugin)
.add_system_to_stage(Update, menu_system);
});
```
or if just installing one plugin:
```rust
game.sessions.create_with("menu", menu_plugin_function);
```
2) Use `SessionBuilder` directly:
```rust
let mut menu_session_builder = SessionBuilder::new("menu");
menu_session_builder
.install_plugin(DefaultSessionPlugin)
.add_system_to_stage(Update, menu_system);
// Finalize session and register with `Sessions`.
let finished_session_ref = menu_session_builder.finish_and_add(&mut game.sessions);
```
or
```rust
let mut menu_session_builder = SessionBuilder::new("menu");
menu_session_builder
.install_plugin(DefaultSessionPlugin)
.add_system_to_stage(Update, menu_system);
// Finalize session and register with `Sessions`.
// (`create` is the same as `finish_and_add`)
let finished_session_ref = game.sessions.create(menu_session_builder);
```
### Risk of forgetting to finish `SessionBuilder`
I don't love this API - by using `SessionBuilder` to restrict mutability
of resources/systems + disabling ability directly construction a
`Session`, we have a risk of configuring a `SessionBuilder` and
forgetting to "finish" or add to `Sessions` and do anything useful with
it. I added a guard (`FinishGuard`) to builder that if dropped (builder
not consumed/finished), will print a warning.
The other option is changing the `SessionBuilder` functions to move
builder and return it, instead of `&mut SessionBuilder` as it is now.
This could be combined with #[must_use] to lint if it isn't
consumed/finished. I had a hard time deciding which route to go - I
decided against the move-semantics / linear approach as it means if you
are not chaining all calls on builder, you have to rebind it with `let`
again, IMO it is not a pleasant experience for more complicated stuff. I
think the run-time warning on Drop hopefully is enough.
# Syntax Change (how to fix compilation)
Session plugins now take `&mut SessionBuilder` instead of `&mut
Session`:
```rust
// old:
fn install(self, session: &mut: Session);
// new
fn install(self, session: &mut SessionBuilder);
```
World is no longer on session builder, the functions used on world for
resource init now are available on builder directly.
```rust
// old:
session.world.init_resource::<MyResource>();
// new
session.init_resource::<MyResource>();
```
0 commit comments