#![warn(missing_docs)]
#![cfg_attr(doc, allow(unknown_lints))]
#![deny(rustdoc::all)]
pub use bevy;
pub mod prelude {
pub use crate::*;
}
mod debug;
mod storage;
mod convert;
use convert::*;
mod input;
use input::*;
mod render;
use render::*;
mod ui;
use ui::*;
mod rumble;
use bevy::{log::LogPlugin, prelude::*};
use bones::GamepadsRumble;
use bones_framework::prelude as bones;
use rumble::*;
use bevy::{
input::InputSystem,
render::RenderApp,
sprite::{extract_sprites, SpriteSystem},
tasks::IoTaskPool,
utils::Instant,
};
use std::path::{Path, PathBuf};
pub struct BonesBevyRenderer {
pub preload: bool,
pub custom_load_progress: Option<LoadingFunction>,
pub pixel_art: bool,
pub game: bones::Game,
pub game_version: bones::Version,
pub app_namespace: (String, String, String),
pub asset_dir: PathBuf,
pub packs_dir: PathBuf,
}
#[derive(Resource, Deref, DerefMut)]
pub struct BonesGame(pub bones::Game);
impl BonesGame {
pub fn asset_server(&self) -> Option<bones::Ref<bones::AssetServer>> {
self.0.shared_resource()
}
}
#[derive(Resource, Deref, DerefMut)]
struct LoadingContext(pub Option<LoadingFunction>);
type LoadingFunction =
Box<dyn FnMut(&bones::AssetServer, &bevy_egui::egui::Context) + Sync + Send + 'static>;
impl BonesBevyRenderer {
pub fn new(game: bones::Game) -> Self {
BonesBevyRenderer {
preload: true,
pixel_art: true,
custom_load_progress: None,
game,
game_version: bones::Version::new(0, 1, 0),
app_namespace: ("local".into(), "developer".into(), "bones_demo_game".into()),
asset_dir: PathBuf::from("assets"),
packs_dir: PathBuf::from("packs"),
}
}
pub fn preload(self, preload: bool) -> Self {
Self { preload, ..self }
}
pub fn loading_screen(mut self, function: LoadingFunction) -> Self {
self.custom_load_progress = Some(function);
self
}
pub fn pixel_art(self, pixel_art: bool) -> Self {
Self { pixel_art, ..self }
}
pub fn namespace(mut self, (qualifier, organization, application): (&str, &str, &str)) -> Self {
self.app_namespace = (qualifier.into(), organization.into(), application.into());
self
}
pub fn asset_dir(self, asset_dir: PathBuf) -> Self {
Self { asset_dir, ..self }
}
pub fn packs_dir(self, packs_dir: PathBuf) -> Self {
Self { packs_dir, ..self }
}
pub fn version(self, game_version: bones::Version) -> Self {
Self {
game_version,
..self
}
}
pub fn app(mut self) -> App {
let mut app = App::new();
let mut plugins = DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
fit_canvas_to_parent: true,
..default()
}),
..default()
})
.disable::<LogPlugin>()
.build();
if self.pixel_art {
plugins = plugins.set(ImagePlugin::default_nearest());
}
app.add_plugins(plugins).add_plugins((
bevy_egui::EguiPlugin,
bevy_prototype_lyon::plugin::ShapePlugin,
debug::BevyDebugPlugin,
));
if self.pixel_art {
app.insert_resource({
let mut egui_settings = bevy_egui::EguiSettings::default();
egui_settings.use_nearest_descriptor();
egui_settings
});
}
app.init_resource::<BonesImageIds>();
if let Some(mut asset_server) = self.game.shared_resource_mut::<bones::AssetServer>() {
asset_server.set_game_version(self.game_version);
asset_server.set_io(asset_io(&self.asset_dir, &self.packs_dir));
if self.preload {
let s = asset_server.clone();
IoTaskPool::get()
.spawn(async move {
s.load_assets().await.unwrap();
})
.detach();
}
asset_server.watch_for_changes();
}
let mut storage = bones::Storage::with_backend(Box::new(storage::StorageBackend::new(
&self.app_namespace.0,
&self.app_namespace.1,
&self.app_namespace.2,
)));
storage.load();
self.game.insert_shared_resource(storage);
self.game
.insert_shared_resource(bones::EguiTextures::default());
self.game.init_shared_resource::<GamepadsRumble>();
app.add_systems(
Update,
handle_bones_rumble.run_if(assets_are_loaded.or_else(move || !self.preload)),
);
self.game.init_shared_resource::<bones::KeyboardInputs>();
self.game.init_shared_resource::<bones::MouseInputs>();
self.game.init_shared_resource::<bones::GamepadInputs>();
#[cfg(not(target_arch = "wasm32"))]
{
self.game.init_shared_resource::<bones::ExitBones>();
app.add_systems(Update, handle_exits);
}
app.insert_resource(BonesGame(self.game))
.insert_resource(LoadingContext(self.custom_load_progress))
.init_resource::<BonesGameEntity>();
app.add_systems(
PreUpdate,
(
setup_egui,
get_bones_input.pipe(insert_bones_input).after(InputSystem),
get_mouse_position
.pipe(insert_mouse_position)
.after(InputSystem),
egui_input_hook,
)
.chain()
.run_if(assets_are_loaded.or_else(move || !self.preload))
.after(bevy_egui::EguiSet::ProcessInput)
.before(bevy_egui::EguiSet::BeginFrame),
);
if self.preload {
app.add_systems(Update, asset_load_status.run_if(assets_not_loaded));
}
app.add_systems(
Update,
(
load_egui_textures,
sync_bones_window,
handle_asset_changes,
step_bones_game,
(
sync_egui_settings,
sync_clear_color,
sync_cameras,
sync_bones_path2ds,
),
)
.chain()
.run_if(assets_are_loaded.or_else(move || !self.preload))
.run_if(egui_ctx_initialized),
);
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(
ExtractSchedule,
(extract_bones_sprites, extract_bones_tilemaps)
.in_set(SpriteSystem::ExtractSprites)
.after(extract_sprites),
);
}
app
}
}
fn egui_ctx_initialized(game: Res<BonesGame>) -> bool {
game.shared_resource::<bones::EguiCtx>().is_some()
}
fn assets_are_loaded(game: Res<BonesGame>) -> bool {
game.asset_server()
.as_ref()
.map(|x| x.load_progress.is_finished())
.unwrap_or(true)
}
fn assets_not_loaded(game: Res<BonesGame>) -> bool {
game.asset_server()
.as_ref()
.map(|x| !x.load_progress.is_finished())
.unwrap_or(true)
}
pub fn asset_io(asset_dir: &Path, packs_dir: &Path) -> impl bones::AssetIo + 'static {
#[cfg(not(target_arch = "wasm32"))]
{
bones::FileAssetIo::new(asset_dir, packs_dir)
}
#[cfg(target_arch = "wasm32")]
{
let _ = asset_dir;
let _ = packs_dir;
let window = web_sys::window().unwrap();
let path = window.location().pathname().unwrap();
let base = path.rsplit_once('/').map(|x| x.0).unwrap_or(&path);
bones::WebAssetIo::new(&format!("{base}/assets"))
}
}
fn asset_load_status(
game: Res<BonesGame>,
mut custom_load_context: ResMut<LoadingContext>,
mut egui_query: Query<&mut bevy_egui::EguiContext, With<Window>>,
) {
let Some(asset_server) = &game.asset_server() else {
return;
};
let mut ctx = egui_query.single_mut();
if let Some(function) = &mut **custom_load_context {
(function)(asset_server, ctx.get_mut());
} else {
default_load_progress(asset_server, ctx.get_mut());
}
}
fn load_egui_textures(
mut has_initialized: Local<bool>,
game: ResMut<BonesGame>,
mut bones_image_ids: ResMut<BonesImageIds>,
mut bevy_images: ResMut<Assets<Image>>,
mut bevy_egui_textures: ResMut<bevy_egui::EguiUserTextures>,
) {
if !*has_initialized {
*has_initialized = true;
} else {
return;
}
if let Some(asset_server) = &game.asset_server() {
let bones_egui_textures_cell = game.shared_resource_cell::<bones::EguiTextures>().unwrap();
let mut bones_egui_textures = bones_egui_textures_cell.borrow_mut().unwrap();
bones_image_ids.load_bones_images(
asset_server,
&mut bones_egui_textures,
&mut bevy_images,
&mut bevy_egui_textures,
);
}
}
fn step_bones_game(world: &mut World) {
world.resource_scope(|world: &mut World, mut game: Mut<BonesGame>| {
let time = world.get_resource::<Time>().unwrap();
game.step(time.last_update().unwrap_or_else(Instant::now));
});
}
pub fn handle_asset_changes(
game: ResMut<BonesGame>,
mut bevy_images: ResMut<Assets<Image>>,
mut bevy_egui_textures: ResMut<bevy_egui::EguiUserTextures>,
mut bones_image_ids: ResMut<BonesImageIds>,
) {
if let Some(mut asset_server) = game.shared_resource_mut::<bones::AssetServer>() {
asset_server.handle_asset_changes(|asset_server, handle| {
let mut bones_egui_textures =
game.shared_resource_mut::<bones::EguiTextures>().unwrap();
let Some(mut asset) = asset_server.get_asset_untyped_mut(handle) else {
return;
};
if let Ok(image) = asset.data.try_cast_mut::<bones::Image>() {
bones_image_ids.load_bones_image(
handle.typed(),
image,
&mut bones_egui_textures,
&mut bevy_images,
&mut bevy_egui_textures,
);
}
})
}
}
#[cfg(not(target_arch = "wasm32"))]
fn handle_exits(game: Res<BonesGame>, mut exits: EventWriter<bevy::app::AppExit>) {
if **game.shared_resource::<bones::ExitBones>().unwrap() {
exits.send(bevy::app::AppExit);
}
}