use std::sync::Mutex;
#[cfg(feature = "diagnostics")]
use bevy::diagnostic::FrameTimeDiagnosticsPlugin;
use bevy::prelude::*;
#[cfg(not(target_arch = "wasm32"))]
use bevy::window::PrimaryWindow;
use bevy_remote::RemoteMethodSystemId;
use bevy_remote::RemoteMethods;
use bevy_remote::RemotePlugin;
#[cfg(not(target_arch = "wasm32"))]
use bevy_remote::http::RemoteHttpPlugin;
#[cfg(not(target_arch = "wasm32"))]
use crate::DEFAULT_REMOTE_PORT;
#[cfg(feature = "diagnostics")]
use crate::diagnostics;
use crate::keyboard;
use crate::mouse;
use crate::screenshot;
use crate::shutdown;
use crate::window_title;
const EXTRAS_COMMAND_PREFIX: &str = "brp_extras/";
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug)]
pub enum PortDisplay {
Always,
NonDefault,
}
pub struct Unconfigured;
#[cfg(not(target_arch = "wasm32"))]
pub struct PortConfigured(u16);
#[cfg(not(target_arch = "wasm32"))]
pub struct HttpPluginConfigured(Mutex<Option<RemoteHttpPlugin>>);
#[cfg(not(target_arch = "wasm32"))]
pub trait HasEffectivePort {
fn fallback_port(&self) -> u16;
fn is_explicit(&self) -> bool;
}
#[cfg(not(target_arch = "wasm32"))]
impl HasEffectivePort for Unconfigured {
fn fallback_port(&self) -> u16 { DEFAULT_REMOTE_PORT }
fn is_explicit(&self) -> bool { false }
}
#[cfg(not(target_arch = "wasm32"))]
impl HasEffectivePort for PortConfigured {
fn fallback_port(&self) -> u16 { self.0 }
fn is_explicit(&self) -> bool { true }
}
#[allow(non_upper_case_globals)]
pub const BrpExtrasPlugin: BrpExtrasPlugin = BrpExtrasPlugin::new();
pub struct BrpExtrasPlugin<HttpConfig = Unconfigured> {
http_config: HttpConfig,
#[cfg(not(target_arch = "wasm32"))]
port_display: Option<PortDisplay>,
}
impl Default for BrpExtrasPlugin<Unconfigured> {
fn default() -> Self { Self::new() }
}
impl BrpExtrasPlugin<Unconfigured> {
#[must_use]
pub const fn new() -> Self {
Self {
http_config: Unconfigured,
#[cfg(not(target_arch = "wasm32"))]
port_display: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub const fn with_port(port: u16) -> BrpExtrasPlugin<PortConfigured> {
BrpExtrasPlugin {
http_config: PortConfigured(port),
port_display: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub const fn with_http_plugin(
plugin: RemoteHttpPlugin,
) -> BrpExtrasPlugin<HttpPluginConfigured> {
BrpExtrasPlugin {
http_config: HttpPluginConfigured(Mutex::new(Some(plugin))),
port_display: None,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl<H: HasEffectivePort> BrpExtrasPlugin<H> {
#[must_use]
pub fn get_effective_port(&self) -> (u16, String) {
let fallback = self.http_config.fallback_port();
let env_port = std::env::var("BRP_EXTRAS_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok());
let effective_port = env_port.unwrap_or(fallback);
let explicit = self.http_config.is_explicit();
let source_description = match (env_port, explicit) {
(Some(_), false) => {
format!("environment override from default {DEFAULT_REMOTE_PORT}")
},
(Some(_), true) => {
format!("environment override from with_port {fallback}")
},
(None, false) => "default".to_string(),
(None, true) => "with_port".to_string(),
};
(effective_port, source_description)
}
#[must_use]
pub const fn port_in_title(mut self, display: PortDisplay) -> Self {
self.port_display = Some(display);
self
}
}
impl Plugin for BrpExtrasPlugin<Unconfigured> {
fn build(&self, app: &mut App) {
#[cfg(not(target_arch = "wasm32"))]
{
add_managed_http_transport(app, None);
maybe_add_port_title_system(app, &self.http_config, self.port_display);
}
build_shared(app);
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Plugin for BrpExtrasPlugin<PortConfigured> {
fn build(&self, app: &mut App) {
add_managed_http_transport(app, Some(self.http_config.0));
maybe_add_port_title_system(app, &self.http_config, self.port_display);
build_shared(app);
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Plugin for BrpExtrasPlugin<HttpPluginConfigured> {
fn build(&self, app: &mut App) {
let Some(plugin) = self
.http_config
.0
.lock()
.ok()
.and_then(|mut guard| guard.take())
else {
error!("failed to retrieve `RemoteHttpPlugin` configuration");
build_shared(app);
return;
};
if app.is_plugin_added::<RemoteHttpPlugin>() {
warn!(
"`RemoteHttpPlugin` is already added — the `RemoteHttpPlugin` provided to \
`BrpExtrasPlugin::with_http_plugin()` will be ignored. The existing HTTP \
transport will be used as-is."
);
} else {
app.add_plugins(plugin);
}
build_shared(app);
}
}
fn build_shared(app: &mut App) {
if !app.is_plugin_added::<RemotePlugin>() {
app.add_plugins(RemotePlugin::default());
}
register_extras_methods(app.world_mut());
#[cfg(feature = "diagnostics")]
if !app.is_plugin_added::<FrameTimeDiagnosticsPlugin>() {
app.add_plugins(FrameTimeDiagnosticsPlugin::default());
}
app.init_resource::<mouse::SimulatedCursorPosition>();
app.add_systems(Update, keyboard::process_timed_key_releases);
app.add_systems(Update, keyboard::process_text_typing);
app.add_systems(Update, mouse::sync_cursor_position);
app.add_systems(Update, mouse::process_timed_button_releases);
app.add_systems(Update, mouse::process_scheduled_clicks);
app.add_systems(Update, mouse::process_drag_operations);
app.add_systems(Update, shutdown::deferred_shutdown_system);
}
#[cfg(not(target_arch = "wasm32"))]
fn add_managed_http_transport(app: &mut App, configured_port: Option<u16>) {
if app.is_plugin_added::<RemoteHttpPlugin>() {
warn!(
"`RemoteHttpPlugin` is already added — `BrpExtrasPlugin` port configuration \
(with_port / BRP_EXTRAS_PORT) will be ignored. The existing HTTP transport \
will be used as-is."
);
return;
}
let env_port = std::env::var("BRP_EXTRAS_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok());
let effective_port = env_port.unwrap_or_else(|| configured_port.unwrap_or(DEFAULT_REMOTE_PORT));
let source_description = match (env_port, configured_port) {
(Some(_), Some(with_port_value)) => {
format!("environment override from with_port {with_port_value}")
},
(Some(_), None) => {
format!("environment override from default {DEFAULT_REMOTE_PORT}")
},
(None, Some(_)) => "with_port".to_string(),
(None, None) => "default".to_string(),
};
let http_plugin = RemoteHttpPlugin::default().with_port(effective_port);
app.add_plugins(http_plugin);
app.add_systems(Startup, move |_world: &mut World| {
log_initialization(effective_port, &source_description);
});
}
fn register_extras_methods(world: &mut World) {
let mut methods = vec![
(
format!("{EXTRAS_COMMAND_PREFIX}click_mouse"),
RemoteMethodSystemId::Instant(world.register_system(mouse::click_mouse_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}double_click_mouse"),
RemoteMethodSystemId::Instant(world.register_system(mouse::double_click_mouse_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}double_tap_gesture"),
RemoteMethodSystemId::Instant(world.register_system(mouse::double_tap_gesture_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}drag_mouse"),
RemoteMethodSystemId::Instant(world.register_system(mouse::drag_mouse_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}move_mouse"),
RemoteMethodSystemId::Instant(world.register_system(mouse::move_mouse_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}pinch_gesture"),
RemoteMethodSystemId::Instant(world.register_system(mouse::pinch_gesture_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}rotation_gesture"),
RemoteMethodSystemId::Instant(world.register_system(mouse::rotation_gesture_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}screenshot"),
RemoteMethodSystemId::Instant(world.register_system(screenshot::handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}scroll_mouse"),
RemoteMethodSystemId::Instant(world.register_system(mouse::scroll_mouse_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}send_keys"),
RemoteMethodSystemId::Instant(world.register_system(keyboard::send_keys_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}send_mouse_button"),
RemoteMethodSystemId::Instant(world.register_system(mouse::send_mouse_button_handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}set_window_title"),
RemoteMethodSystemId::Instant(world.register_system(window_title::handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}shutdown"),
RemoteMethodSystemId::Instant(world.register_system(shutdown::handler)),
),
(
format!("{EXTRAS_COMMAND_PREFIX}type_text"),
RemoteMethodSystemId::Instant(world.register_system(keyboard::type_text_handler)),
),
];
#[cfg(feature = "diagnostics")]
methods.push((
format!("{EXTRAS_COMMAND_PREFIX}get_diagnostics"),
RemoteMethodSystemId::Instant(world.register_system(diagnostics::handler)),
));
let mut remote_methods = world.resource_mut::<RemoteMethods>();
for (name, system_id) in methods {
remote_methods.insert(name, system_id);
}
}
#[cfg(not(target_arch = "wasm32"))]
fn log_initialization(port: u16, source_description: &str) {
info!("BRP extras enabled on http://localhost:{port} ({source_description})");
}
#[cfg(not(target_arch = "wasm32"))]
fn maybe_add_port_title_system(
app: &mut App,
http_config: &impl HasEffectivePort,
port_display: Option<PortDisplay>,
) {
let Some(display) = port_display else {
return;
};
let fallback = http_config.fallback_port();
let env_port = std::env::var("BRP_EXTRAS_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok());
let effective_port = env_port.unwrap_or(fallback);
let should_display = match display {
PortDisplay::Always => true,
PortDisplay::NonDefault => effective_port != DEFAULT_REMOTE_PORT,
};
if should_display {
app.add_systems(
Startup,
move |mut query: Query<&mut Window, With<PrimaryWindow>>| {
if let Ok(mut window) = query.single_mut() {
window.title = format!("{} (port: {effective_port})", window.title);
}
},
);
}
}