#![doc = include_str!("../README.md")]
#[cfg(feature = "asset_loading")]
mod asset_reader;
mod typed;
#[cfg(feature = "asset_loading")]
use asset_reader::WebAssetReader;
use bevy_app::{App, Plugin, Update};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EntityEvent,
prelude::Query,
query::Without,
resource::Resource,
system::{Commands, ResMut},
};
use bevy_tasks::IoTaskPool;
use crossbeam_channel::{Receiver, bounded};
use ehttp::{Request, Response};
pub mod prelude {
#[cfg(feature = "response_as_component")]
pub use super::RequestResponse;
pub use super::typed::{RegisterRequestTypeTrait, RequestBundle, ResponseTyped};
pub use super::{
HttpClientSetting, HttpPlugin, HttpRequest, RequestResponseExt, RequestTask, ResponseString,
};
pub use ehttp::{Error, Request, Response};
}
#[derive(Default)]
pub struct HttpPlugin;
impl Plugin for HttpPlugin {
fn build(&self, app: &mut App) {
if !app.world().contains_resource::<HttpClientSetting>() {
app.init_resource::<HttpClientSetting>();
}
app.add_systems(Update, (handle_request, handle_response));
#[cfg(feature = "asset_loading")]
{
use bevy_asset::{AssetApp, io::AssetSourceBuilder};
app.register_asset_source(
"http",
AssetSourceBuilder::new(|| Box::<WebAssetReader<false>>::default()),
)
.register_asset_source(
"https",
AssetSourceBuilder::new(|| Box::<WebAssetReader<true>>::default()),
);
}
}
}
#[derive(Resource)]
pub struct HttpClientSetting {
pub max_concurrent: usize,
current_clients: usize,
}
impl Default for HttpClientSetting {
fn default() -> Self {
Self {
max_concurrent: 5,
current_clients: 0,
}
}
}
impl HttpClientSetting {
pub fn new(max_concurrent: usize) -> Self {
Self {
max_concurrent,
current_clients: 0,
}
}
#[inline]
pub fn is_available(&self) -> bool {
self.current_clients < self.max_concurrent
}
}
pub trait RequestResponseExt {
fn response(&self) -> &Result<Response, String>;
fn success(&self) -> bool {
self.response().as_ref().is_ok_and(|f| f.ok)
}
fn url(&self) -> Result<&String, &String> {
self.response().as_ref().map(|e| &e.url)
}
}
#[derive(Component, Debug, Clone, Deref, DerefMut)]
pub struct HttpRequest(pub Request);
impl HttpRequest {
pub fn new(request: Request) -> Self {
Self(request)
}
pub fn get(url: impl ToString) -> Self {
Self(Request::get(url))
}
pub fn post(url: &str, body: Vec<u8>) -> Self {
Self(Request::post(url, body))
}
}
#[cfg(feature = "response_as_component")]
#[derive(Component, Debug, Clone, Deref, DerefMut)]
pub struct RequestResponse(pub Result<Response, String>);
#[derive(EntityEvent, DerefMut, Deref)]
pub struct ResponseString {
#[deref]
pub response: Result<Response, String>,
pub entity: Entity,
}
impl RequestResponseExt for ResponseString {
fn response(&self) -> &Result<Response, String> {
&self.response
}
}
#[derive(Component)]
pub struct RequestTask {
pub receiver: Receiver<Result<Response, ehttp::Error>>,
}
impl RequestTask {
fn poll(&mut self) -> Option<Result<Response, ehttp::Error>> {
self.receiver.try_recv().ok()
}
}
fn handle_request(
mut commands: Commands,
mut req_res: ResMut<HttpClientSetting>,
requests: Query<(Entity, &HttpRequest), Without<RequestTask>>,
) {
for (entity, request) in requests.iter() {
if req_res.is_available() {
let req = request.clone();
{
let (sender, receiver) = bounded(1);
IoTaskPool::get()
.spawn(async move {
let result = ehttp::fetch_async(req.0).await;
sender.send(result).ok();
})
.detach();
commands
.entity(entity)
.remove::<HttpRequest>()
.insert(RequestTask { receiver });
}
req_res.current_clients += 1;
}
}
}
fn handle_response(
mut commands: Commands,
mut req_res: ResMut<HttpClientSetting>,
mut request_tasks: Query<(Entity, &mut RequestTask)>,
) {
for (e, mut task) in request_tasks.iter_mut() {
if let Some(result) = task.poll() {
let mut cmd = commands.entity(e);
#[cfg(feature = "response_as_component")]
cmd.insert(RequestResponse(result.clone()));
cmd.remove::<RequestTask>();
cmd.trigger(|entity| ResponseString {
response: result,
entity,
});
req_res.current_clients -= 1;
}
}
}