1#![warn(missing_docs)]
4#![cfg_attr(doc, allow(unknown_lints))]
6#![deny(rustdoc::all)]
7
8pub use bevy;
9
10pub mod prelude {
12 pub use crate::*;
13}
14
15pub mod debug;
16pub mod storage;
18
19pub mod convert;
21use convert::*;
22
23pub mod input;
25use input::*;
26
27pub mod render;
29use render::*;
30
31pub mod ui;
33use ui::*;
34
35pub mod rumble;
37use bevy::{log::LogPlugin, prelude::*};
38use bones::GamepadsRumble;
39use bones_framework::prelude as bones;
40use rumble::*;
41
42use bevy::{
43 input::InputSystem,
44 render::RenderApp,
45 sprite::{extract_sprites, SpriteSystem},
46 tasks::IoTaskPool,
47 utils::Instant,
48};
49use std::path::{Path, PathBuf};
50
51pub struct BonesBevyRenderer {
53 pub preload: bool,
56 pub custom_load_progress: Option<LoadingFunction>,
58 pub pixel_art: bool,
60 pub game: bones::Game,
62 pub game_version: bones::Version,
64 pub app_namespace: (String, String, String),
69 pub asset_dir: PathBuf,
71 pub packs_dir: PathBuf,
73}
74
75#[derive(Resource, Deref, DerefMut)]
77pub struct BonesGame(pub bones::Game);
78impl BonesGame {
79 pub fn asset_server(&self) -> Option<bones::Ref<'_, bones::AssetServer>> {
81 self.0.get_shared_resource()
82 }
83}
84
85#[derive(Resource, Deref, DerefMut)]
86struct LoadingContext(pub Option<LoadingFunction>);
87type LoadingFunction =
88 Box<dyn FnMut(&bones::AssetServer, &bevy_egui::egui::Context) + Sync + Send + 'static>;
89
90impl BonesBevyRenderer {
91 pub fn new(game: bones::Game) -> Self {
96 BonesBevyRenderer {
97 preload: true,
98 pixel_art: true,
99 custom_load_progress: None,
100 game,
101 game_version: bones::Version::new(0, 1, 0),
102 app_namespace: ("local".into(), "developer".into(), "bones_demo_game".into()),
103 asset_dir: PathBuf::from("assets"),
104 packs_dir: PathBuf::from("packs"),
105 }
106 }
107 pub fn preload(self, preload: bool) -> Self {
110 Self { preload, ..self }
111 }
112 pub fn loading_screen(mut self, function: LoadingFunction) -> Self {
114 self.custom_load_progress = Some(function);
115 self
116 }
117 pub fn pixel_art(self, pixel_art: bool) -> Self {
119 Self { pixel_art, ..self }
120 }
121 pub fn namespace(mut self, (qualifier, organization, application): (&str, &str, &str)) -> Self {
126 self.app_namespace = (qualifier.into(), organization.into(), application.into());
127 self
128 }
129 pub fn asset_dir(self, asset_dir: PathBuf) -> Self {
131 Self { asset_dir, ..self }
132 }
133 pub fn packs_dir(self, packs_dir: PathBuf) -> Self {
135 Self { packs_dir, ..self }
136 }
137 pub fn version(self, game_version: bones::Version) -> Self {
139 Self {
140 game_version,
141 ..self
142 }
143 }
144
145 pub fn app(mut self) -> App {
147 let mut app = App::new();
148
149 let mut plugins = DefaultPlugins
151 .set(WindowPlugin {
152 primary_window: Some(Window {
153 fit_canvas_to_parent: true,
154 ..default()
155 }),
156 ..default()
157 })
158 .disable::<LogPlugin>()
159 .build();
160 if self.pixel_art {
161 plugins = plugins.set(ImagePlugin::default_nearest());
162 }
164
165 app.add_plugins(plugins).add_plugins((
166 bevy_egui::EguiPlugin,
167 bevy_prototype_lyon::plugin::ShapePlugin,
168 debug::BevyDebugPlugin,
169 ));
170 if self.pixel_art {
171 app.insert_resource({
172 let mut egui_settings = bevy_egui::EguiSettings::default();
173 egui_settings.use_nearest_descriptor();
174 egui_settings
175 });
176 }
177 app.init_resource::<BonesImageIds>();
178
179 if let Some(mut asset_server) = self.game.get_shared_resource_mut::<bones::AssetServer>() {
180 asset_server.set_game_version(self.game_version);
181 asset_server.set_io(asset_io(&self.asset_dir, &self.packs_dir));
182
183 if self.preload {
184 let s = asset_server.clone();
186 IoTaskPool::get()
187 .spawn(async move {
188 s.load_assets().await.unwrap();
189 })
190 .detach();
191 }
192
193 asset_server.watch_for_changes();
195 }
196
197 let mut storage = bones::Storage::with_backend(Box::new(storage::StorageBackend::new(
199 &self.app_namespace.0,
200 &self.app_namespace.1,
201 &self.app_namespace.2,
202 )));
203 storage.load();
204 self.game.insert_shared_resource(storage);
205 self.game
206 .insert_shared_resource(bones::EguiTextures::default());
207
208 self.game.init_shared_resource::<GamepadsRumble>();
210 app.add_systems(
211 Update,
212 handle_bones_rumble.run_if(assets_are_loaded.or_else(move || !self.preload)),
213 );
214
215 self.game.init_shared_resource::<bones::KeyboardInputs>();
217 self.game.init_shared_resource::<bones::MouseInputs>();
218 self.game.init_shared_resource::<bones::GamepadInputs>();
219
220 #[cfg(not(target_arch = "wasm32"))]
221 {
222 self.game.init_shared_resource::<bones::ExitBones>();
223 app.add_systems(Update, handle_exits);
224 }
225
226 app.insert_resource(BonesGame(self.game))
228 .insert_resource(LoadingContext(self.custom_load_progress))
229 .init_resource::<BonesGameEntity>();
230
231 app.add_systems(
233 PreUpdate,
234 (
235 setup_egui,
236 get_bones_input.pipe(insert_bones_input).after(InputSystem),
237 get_mouse_position
238 .pipe(insert_mouse_position)
239 .after(InputSystem),
240 egui_input_hook,
241 )
242 .chain()
243 .run_if(assets_are_loaded.or_else(move || !self.preload))
244 .after(bevy_egui::EguiSet::ProcessInput)
245 .before(bevy_egui::EguiSet::BeginFrame),
246 );
247
248 if self.preload {
249 app.add_systems(Update, asset_load_status.run_if(assets_not_loaded));
250 }
251 app.add_systems(
252 Update,
253 (
254 load_egui_textures,
255 sync_bones_window,
256 handle_asset_changes,
257 step_bones_game,
259 (
261 sync_egui_settings,
262 sync_clear_color,
263 sync_cameras,
264 sync_bones_path2ds,
265 ),
266 )
267 .chain()
268 .run_if(assets_are_loaded.or_else(move || !self.preload))
269 .run_if(egui_ctx_initialized),
270 );
271
272 if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
273 render_app.add_systems(
274 ExtractSchedule,
275 (extract_bones_sprites, extract_bones_tilemaps)
276 .in_set(SpriteSystem::ExtractSprites)
277 .after(extract_sprites),
278 );
279 }
280
281 app
282 }
283}
284
285fn egui_ctx_initialized(game: Res<BonesGame>) -> bool {
286 game.get_shared_resource::<bones::EguiCtx>().is_some()
287}
288
289fn assets_are_loaded(game: Res<BonesGame>) -> bool {
290 game.asset_server()
292 .as_ref()
293 .map(|x| x.load_progress.is_finished())
294 .unwrap_or(true)
295}
296
297fn assets_not_loaded(game: Res<BonesGame>) -> bool {
298 game.asset_server()
299 .as_ref()
300 .map(|x| !x.load_progress.is_finished())
301 .unwrap_or(true)
302}
303
304pub fn asset_io(asset_dir: &Path, packs_dir: &Path) -> impl bones::AssetIo + 'static {
306 #[cfg(not(target_arch = "wasm32"))]
307 {
308 bones::FileAssetIo::new(asset_dir, packs_dir)
309 }
310 #[cfg(target_arch = "wasm32")]
311 {
312 let _ = asset_dir;
313 let _ = packs_dir;
314 let window = web_sys::window().unwrap();
315 let path = window.location().pathname().unwrap();
316 let base = path.rsplit_once('/').map(|x| x.0).unwrap_or(&path);
317 bones::WebAssetIo::new(&format!("{base}/assets"))
318 }
319}
320
321fn asset_load_status(
322 game: Res<BonesGame>,
323 mut custom_load_context: ResMut<LoadingContext>,
324 mut egui_query: Query<&mut bevy_egui::EguiContext, With<Window>>,
325) {
326 let Some(asset_server) = &game.asset_server() else {
327 return;
328 };
329
330 let mut ctx = egui_query.single_mut();
331 if let Some(function) = &mut **custom_load_context {
332 (function)(asset_server, ctx.get_mut());
333 } else {
334 default_load_progress(asset_server, ctx.get_mut());
335 }
336}
337
338fn load_egui_textures(
339 mut has_initialized: Local<bool>,
340 game: ResMut<BonesGame>,
341 mut bones_image_ids: ResMut<BonesImageIds>,
342 mut bevy_images: ResMut<Assets<Image>>,
343 mut bevy_egui_textures: ResMut<bevy_egui::EguiUserTextures>,
344) {
345 if !*has_initialized {
346 *has_initialized = true;
347 } else {
348 return;
349 }
350 if let Some(asset_server) = &game.asset_server() {
351 let bones_egui_textures_cell = game.shared_resource_cell::<bones::EguiTextures>().unwrap();
352 let mut bones_egui_textures = bones_egui_textures_cell.borrow_mut().unwrap();
356 bones_image_ids.load_bones_images(
358 asset_server,
359 &mut bones_egui_textures,
360 &mut bevy_images,
361 &mut bevy_egui_textures,
362 );
363 }
364}
365
366fn step_bones_game(world: &mut World) {
368 world.resource_scope(|world: &mut World, mut game: Mut<BonesGame>| {
369 let time = world.get_resource::<Time>().unwrap();
370 game.step(time.last_update().unwrap_or_else(Instant::now));
371 });
372}
373
374pub fn handle_asset_changes(
376 game: ResMut<BonesGame>,
377 mut bevy_images: ResMut<Assets<Image>>,
378 mut bevy_egui_textures: ResMut<bevy_egui::EguiUserTextures>,
379 mut bones_image_ids: ResMut<BonesImageIds>,
380) {
381 if let Some(mut asset_server) = game.get_shared_resource_mut::<bones::AssetServer>() {
382 asset_server.handle_asset_changes(|asset_server, handle| {
383 let mut bones_egui_textures = game.shared_resource_mut::<bones::EguiTextures>();
384 let Some(mut asset) = asset_server.get_asset_untyped_mut(handle) else {
385 return;
387 };
388
389 if let Ok(image) = asset.data.try_cast_mut::<bones::Image>() {
392 bones_image_ids.load_bones_image(
393 handle.typed(),
394 image,
395 &mut bones_egui_textures,
396 &mut bevy_images,
397 &mut bevy_egui_textures,
398 );
399 }
400 })
401 }
402}
403
404#[cfg(not(target_arch = "wasm32"))]
405fn handle_exits(game: Res<BonesGame>, mut exits: EventWriter<bevy::app::AppExit>) {
406 if **game.shared_resource::<bones::ExitBones>() {
407 exits.send(bevy::app::AppExit);
408 }
409}