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