Skip to content

Commit fc00c0c

Browse files
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>(); ```
1 parent 3eb56b8 commit fc00c0c

File tree

32 files changed

+1157
-254
lines changed

32 files changed

+1157
-254
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ parking_lot = "0.12"
3030
smallvec = "1.11"
3131
ustr = "0.10"
3232
iroh = "0.29"
33+
tracing = "0.1"
3334

3435
[profile.release]
3536
lto = true

demos/asset_packs/src/main.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ fn main() {
4646

4747
// Create a new session for the game menu. Each session is it's own bones world with it's own
4848
// plugins, systems, and entities.
49-
let menu_session = game.sessions.create("menu");
50-
menu_session
49+
game.sessions.create_with("menu", |builder| {
5150
// Install the default bones_framework plugin for this session
52-
.install_plugin(DefaultSessionPlugin)
53-
// Add our menu system to the update stage
54-
.add_system_to_stage(Update, menu_system);
51+
builder
52+
.install_plugin(DefaultSessionPlugin)
53+
// Add our menu system to the update stage
54+
.add_system_to_stage(Update, menu_system);
55+
});
5556

5657
BonesBevyRenderer::new(game).app().run();
5758
}

demos/assets_minimal/src/main.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ fn main() {
3636

3737
// Create a new session for the game menu. Each session is it's own bones world with it's own
3838
// plugins, systems, and entities.
39-
let menu_session = game.sessions.create("menu");
40-
menu_session
41-
// Install the default bones_framework plugin for this session
39+
let mut menu_session_builder = SessionBuilder::new("menu");
40+
41+
// Install the default bones_framework plugin for this session
42+
menu_session_builder
4243
.install_plugin(DefaultSessionPlugin)
4344
// Add our menu system to the update stage
4445
.add_system_to_stage(Update, menu_system);
4546

47+
// Finalize session and register with game sessions.
48+
menu_session_builder.finish_and_add(&mut game.sessions);
49+
4650
BonesBevyRenderer::new(game).app().run();
4751
}
4852

demos/features/src/main.rs

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ pub fn create_game() -> Game {
125125
TilemapDemoMeta::register_schema();
126126

127127
// Create our menu session
128-
game.sessions.create("menu").install_plugin(menu_plugin);
128+
game.sessions.create_with("menu", menu_plugin);
129129

130130
game
131131
}
@@ -139,12 +139,11 @@ struct MenuData {
139139
}
140140

141141
/// Menu plugin
142-
pub fn menu_plugin(session: &mut Session) {
142+
pub fn menu_plugin(session: &mut SessionBuilder) {
143143
// Register our menu system
144144
session
145145
// Install the bones_framework default plugins for this session
146146
.install_plugin(DefaultSessionPlugin)
147-
.world
148147
// Initialize our menu data resource
149148
.init_resource::<MenuData>();
150149

@@ -198,9 +197,7 @@ fn menu_system(
198197
session_options.delete = true;
199198

200199
// Create a session for the match
201-
sessions
202-
.create("sprite_demo")
203-
.install_plugin(sprite_demo_plugin);
200+
sessions.create_with("sprite_demo", sprite_demo_plugin);
204201
}
205202

206203
if BorderedButton::themed(&meta.button_style, localization.get("atlas-demo"))
@@ -211,9 +208,7 @@ fn menu_system(
211208
session_options.delete = true;
212209

213210
// Create a session for the match
214-
sessions
215-
.create("atlas_demo")
216-
.install_plugin(atlas_demo_plugin);
211+
sessions.create_with("atlas_demo", atlas_demo_plugin);
217212
}
218213

219214
if BorderedButton::themed(&meta.button_style, localization.get("tilemap-demo"))
@@ -224,9 +219,7 @@ fn menu_system(
224219
session_options.delete = true;
225220

226221
// Create a session for the match
227-
sessions
228-
.create("tilemap_demo")
229-
.install_plugin(tilemap_demo_plugin);
222+
sessions.create_with("tilemap_demo", tilemap_demo_plugin);
230223
}
231224

232225
if BorderedButton::themed(&meta.button_style, localization.get("audio-demo"))
@@ -237,9 +230,7 @@ fn menu_system(
237230
session_options.delete = true;
238231

239232
// Create a session for the match
240-
sessions
241-
.create("audio_demo")
242-
.install_plugin(audio_demo_plugin);
233+
sessions.create_with("audio_demo", audio_demo_plugin);
243234
}
244235

245236
if BorderedButton::themed(&meta.button_style, localization.get("storage-demo"))
@@ -250,9 +241,7 @@ fn menu_system(
250241
session_options.delete = true;
251242

252243
// Create a session for the match
253-
sessions
254-
.create("storage_demo")
255-
.install_plugin(storage_demo_plugin);
244+
sessions.create_with("storage_demo", storage_demo_plugin);
256245
}
257246

258247
if BorderedButton::themed(&meta.button_style, localization.get("path2d-demo"))
@@ -263,9 +252,7 @@ fn menu_system(
263252
session_options.delete = true;
264253

265254
// Create a session for the match
266-
sessions
267-
.create("path2d_demo")
268-
.install_plugin(path2d_demo_plugin);
255+
sessions.create_with("path2d_demo", path2d_demo_plugin);
269256
}
270257

271258
if let Some(exit_bones) = &mut exit_bones {
@@ -293,7 +280,7 @@ fn menu_system(
293280
}
294281

295282
/// Plugin for running the sprite demo.
296-
fn sprite_demo_plugin(session: &mut Session) {
283+
fn sprite_demo_plugin(session: &mut SessionBuilder) {
297284
session
298285
.install_plugin(DefaultSessionPlugin)
299286
.add_startup_system(sprite_demo_startup)
@@ -357,7 +344,7 @@ fn move_sprite(
357344
}
358345

359346
/// Plugin for running the tilemap demo.
360-
fn tilemap_demo_plugin(session: &mut Session) {
347+
fn tilemap_demo_plugin(session: &mut SessionBuilder) {
361348
session
362349
.install_plugin(DefaultSessionPlugin)
363350
.add_startup_system(tilemap_startup_system)
@@ -403,7 +390,7 @@ fn tilemap_startup_system(
403390
}
404391

405392
/// Plugin for running the atlas demo.
406-
fn atlas_demo_plugin(session: &mut Session) {
393+
fn atlas_demo_plugin(session: &mut SessionBuilder) {
407394
session
408395
.install_plugin(DefaultSessionPlugin)
409396
.add_startup_system(atlas_demo_startup)
@@ -451,7 +438,7 @@ fn atlas_demo_startup(
451438
);
452439
}
453440

454-
fn audio_demo_plugin(session: &mut Session) {
441+
fn audio_demo_plugin(session: &mut SessionBuilder) {
455442
session
456443
.install_plugin(DefaultSessionPlugin)
457444
.add_system_to_stage(Update, back_to_menu_ui)
@@ -477,7 +464,7 @@ fn audio_demo_ui(
477464
});
478465
}
479466

480-
fn storage_demo_plugin(session: &mut Session) {
467+
fn storage_demo_plugin(session: &mut SessionBuilder) {
481468
session
482469
.install_plugin(DefaultSessionPlugin)
483470
.add_system_to_stage(Update, storage_demo_ui)
@@ -507,7 +494,7 @@ fn storage_demo_ui(
507494
});
508495
}
509496

510-
fn path2d_demo_plugin(session: &mut Session) {
497+
fn path2d_demo_plugin(session: &mut SessionBuilder) {
511498
session
512499
.install_plugin(DefaultSessionPlugin)
513500
.add_startup_system(path2d_demo_startup)
@@ -558,7 +545,7 @@ fn back_to_menu_ui(
558545
ui.add_space(20.0);
559546
if ui.button(localization.get("back-to-menu")).clicked() {
560547
session_options.delete = true;
561-
sessions.create("menu").install_plugin(menu_plugin);
548+
sessions.create_with("menu", menu_plugin);
562549
}
563550
});
564551
});

demos/hello_world/src/main.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ fn main() {
1010

1111
// Create a new session for the game menu. Each session is it's own bones world with it's own
1212
// plugins, systems, and entities.
13-
let menu_session = game.sessions.create("menu");
14-
menu_session
15-
// Install the default bones_framework plugin for this session
16-
.install_plugin(DefaultSessionPlugin)
17-
// Add our menu system to the update stage
18-
.add_system_to_stage(Update, menu_system);
13+
game.sessions.create_with("menu", |session| {
14+
session
15+
// Install the default bones_framework plugin for this session
16+
.install_plugin(DefaultSessionPlugin)
17+
// Add our menu system to the update stage
18+
.add_system_to_stage(Update, menu_system);
19+
});
1920

2021
BonesBevyRenderer::new(game).app().run();
2122
}

demos/scripting/src/main.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ fn main() {
2222
.register_default_assets();
2323
GameMeta::register_schema();
2424

25-
game.sessions
26-
.create("launch")
27-
.add_startup_system(launch_game_session);
25+
game.sessions.create_with("launch", |builder| {
26+
builder.add_startup_system(launch_game_session);
27+
});
2828

2929
let mut renderer = BonesBevyRenderer::new(game);
3030
renderer.app_namespace = (
@@ -41,15 +41,17 @@ fn launch_game_session(
4141
mut session_ops: ResMut<SessionOptions>,
4242
) {
4343
session_ops.delete = true;
44-
let game_session = sessions.create("game");
45-
game_session
46-
.install_plugin(DefaultSessionPlugin)
47-
// Install the plugin that will load our lua plugins and run them in the game session
48-
.install_plugin(LuaPluginLoaderSessionPlugin(
49-
// Tell it to install the lua plugins specified in our game meta
50-
Arc::new(meta.plugins.iter().copied().collect()),
51-
))
52-
.add_startup_system(game_startup);
44+
// Build game session and add to `Sessions`
45+
sessions.create_with("game", |builder| {
46+
builder
47+
.install_plugin(DefaultSessionPlugin)
48+
// Install the plugin that will load our lua plugins and run them in the game session
49+
.install_plugin(LuaPluginLoaderSessionPlugin(
50+
// Tell it to install the lua plugins specified in our game meta
51+
Arc::new(meta.plugins.iter().copied().collect()),
52+
))
53+
.add_startup_system(game_startup);
54+
});
5355
}
5456

5557
fn game_startup(

framework_crates/bones_asset/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ serde = { version = "1.0", features = ["derive"] }
4040
serde_json = "1.0"
4141
serde_yaml = "0.9"
4242
sha2 = "0.10"
43-
tracing = "0.1"
43+
tracing = { workspace = true }
4444
ulid = "1.0"
4545
ustr = { workspace = true }
4646

framework_crates/bones_ecs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ atomicell = "0.2"
4040
bitset-core = "0.1"
4141
once_map = "0.4.12"
4242
thiserror = "1.0"
43+
tracing = { workspace = true }
4344

4445
[dev-dependencies]
4546
glam = "0.24"

framework_crates/bones_ecs/examples/pos_vel.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@ fn main() {
2222
// Initialize an empty world
2323
let mut world = World::new();
2424

25-
// Create a SystemStages to store the systems that we will run more than once.
26-
let mut stages = SystemStages::with_core_stages();
25+
// Build SystemStages to store the systems that we will run more than once.
26+
let mut stages_builder = SystemStagesBuilder::with_core_stages();
2727

2828
// Add our systems to the system stages
29-
stages
29+
stages_builder
3030
.add_startup_system(startup_system)
3131
.add_system_to_stage(CoreStage::Update, pos_vel_system)
3232
.add_system_to_stage(CoreStage::PostUpdate, print_system);
3333

34+
// Finish build to get `SystemStages`.
35+
let mut stages = stages_builder.finish();
36+
3437
// Run our game loop for 10 frames
3538
for _ in 0..10 {
3639
stages.run(&mut world);

0 commit comments

Comments
 (0)