Compare commits

..

No commits in common. 'f3552382b49375005c9aa5d9d4f59aa3059ecb41' and '54615c265dd7085947b10e99db7ef313b0723378' have entirely different histories.

  1. 33
      client/src/args.rs
  2. 31
      client/src/block.rs
  3. 39
      client/src/block_assets.rs
  4. 9
      client/src/camera.rs
  5. 20
      client/src/loading.rs
  6. 113
      client/src/main.rs
  7. 4
      client/src/placement.rs
  8. 72
      common/src/asset_loading.rs
  9. 9
      common/src/lib.rs
  10. 9
      common/src/network/client.rs
  11. 8
      common/src/network/server.rs
  12. 14
      common/src/network/server_webtransport.rs

@ -1,8 +1,7 @@
use bevy::ecs::resource::Resource;
use clap::{Parser, Subcommand};
use common::network::{DEFAULT_ADDRESS, DEFAULT_PORT};
#[derive(Resource, Parser, Default, Debug)]
#[derive(Parser, Default, Debug)]
#[command(version, about)]
pub struct Args {
#[command(subcommand)]
@ -46,22 +45,24 @@ impl Args {
use bevy::log::error;
use web_sys::{UrlSearchParams, window};
let params = window()
.and_then(|window| window.location().search().ok())
.and_then(|search| UrlSearchParams::new_with_str(&search).ok());
if let Some(params) = params {
if let Some(address) = params.get("connect") {
if let Some(digest) = params.get("digest") {
return Self {
mode: Some(Mode::Connect { address, digest }),
let Some(window) = window() else {
return Self::default();
};
let Ok(search) = window.location().search() else {
return Self::default();
};
let Ok(params) = UrlSearchParams::new_with_str(&search) else {
return Self::default();
};
let Some(address) = params.get("connect") else {
return Self::default();
};
} else {
let Some(digest) = params.get("digest") else {
error!("Missing 'digest' parameter.");
return Self::default();
};
Self {
mode: Some(Mode::Connect { address, digest }),
}
}
}
Self::default()
}
}

@ -0,0 +1,31 @@
use bevy::prelude::*;
pub use common::block::*;
#[derive(Resource)]
pub struct BlockResources {
mesh: Handle<Mesh>,
material: Handle<StandardMaterial>,
}
pub fn setup_blocks(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.insert_resource(BlockResources {
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
material: materials.add(Color::srgb_u8(124, 144, 255)),
});
}
pub fn add_block_visuals(
add: On<Add, Block>,
mut commands: Commands,
resources: Res<BlockResources>,
) {
commands.entity(add.entity).insert((
Mesh3d(resources.mesh.clone()),
MeshMaterial3d(resources.material.clone()),
));
}

@ -1,39 +0,0 @@
use bevy::prelude::*;
use common::prelude::*;
pub fn plugin(app: &mut App) {
app.add_observer(insert_block_visuals);
app.load_resource::<BlockAssets>();
}
#[derive(Resource, Asset, Reflect, Clone)]
#[reflect(Resource)]
pub struct BlockAssets {
#[dependency]
mesh: Handle<Mesh>,
#[dependency]
material: Handle<StandardMaterial>,
}
impl FromWorld for BlockAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
Self {
mesh: assets.add(Cuboid::new(1.0, 1.0, 1.0).into()),
material: assets.add(Color::srgb_u8(124, 144, 255).into()),
}
}
}
/// Observer which automatically inserts block visuals (mesh and
/// material) when an entity has the `Block` component added to it.
fn insert_block_visuals(
add: On<Add, Block>,
mut commands: Commands,
block_assets: Res<BlockAssets>,
) {
commands.entity(add.entity).insert((
Mesh3d(block_assets.mesh.clone()),
MeshMaterial3d(block_assets.material.clone()),
));
}

@ -1,8 +1,7 @@
use std::f32::consts::TAU;
use bevy::prelude::*;
use bevy::input::mouse::AccumulatedMouseMotion;
use bevy::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions};
pub fn cursor_grab(
@ -158,11 +157,9 @@ pub fn noclip_controller(
if key_input.pressed(noclip.key_back ) { movement.z -= 1.0; }
if key_input.pressed(noclip.key_right ) { movement.x += 1.0; }
if key_input.pressed(noclip.key_left ) { movement.x -= 1.0; }
movement = movement.clamp_length_max(1.0);
// Movement along the Y (up/down) axis shouldn't be clamped.
if key_input.pressed(noclip.key_up ) { movement.y += 1.0; }
if key_input.pressed(noclip.key_down) { movement.y -= 1.0; }
movement * noclip.speed
if key_input.pressed(noclip.key_down ) { movement.y -= 1.0; }
movement.clamp_length_max(1.0) * noclip.speed
};
if movement != Vec3::ZERO {

@ -1,20 +0,0 @@
use bevy::prelude::*;
use common::asset_loading::ResourceHandles;
use crate::Screen;
pub fn plugin(app: &mut App) {
app.add_systems(
Update,
enter_gameplay_screen.run_if(in_state(Screen::Loading).and(all_assets_loaded)),
);
}
fn enter_gameplay_screen(mut next_screen: ResMut<NextState<Screen>>) {
next_screen.set(Screen::Gameplay);
}
fn all_assets_loaded(resource_handles: Res<ResourceHandles>) -> bool {
resource_handles.is_all_done()
}

@ -1,33 +1,25 @@
use bevy::prelude::*;
use common::prelude::network::*;
use common::prelude::*;
use common::network::*;
use bevy::asset::AssetMetaCheck;
use bevy::window::WindowResolution;
use lightyear::prelude::server::Started;
mod args;
mod block_assets;
mod block;
mod camera;
mod loading;
mod placement;
use args::*;
use block::*;
use camera::*;
use placement::*;
#[derive(States, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Screen {
Loading,
Gameplay,
}
#[rustfmt::skip]
fn main() -> Result {
let cli = Args::parse();
let mut app = App::new();
app.add_plugins(
DefaultPlugins
.set(WindowPlugin {
app.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "bevy-bloxel-classic".into(),
// Steam Deck: DPI appears pretty high, causing everything to be scaled up.
@ -40,56 +32,39 @@ fn main() -> Result {
..default()
}),
..default()
})
.set(AssetPlugin {
// WEB: Don't check for `.meta` files since we don't use them.
meta_check: AssetMetaCheck::Never,
..default()
}),
);
app.add_plugins((
bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin,
ServerPlugin,
ClientPlugin,
common::asset_loading::plugin,
block_assets::plugin,
loading::plugin,
));
}));
// Fixes issue on web where the cursor isn't ungrabbed properly.
app.add_plugins(bevy_fix_cursor_unlock_web::FixPointerUnlockPlugin);
app.insert_resource(Args::parse());
app.insert_state(Screen::Loading);
app.add_systems(
OnEnter(Screen::Gameplay),
(setup_crosshair, setup_scene, start_server_or_connect),
);
// TODO: Create and configure `SystemSet`s for gameplay, input, etc.
app.add_systems(
Update,
(
cursor_grab,
update_crosshair_visibility.after(cursor_grab),
camera_look.after(cursor_grab).run_if(is_cursor_grabbed),
noclip_controller
.after(camera_look)
.run_if(is_cursor_grabbed),
)
.run_if(in_state(Screen::Gameplay)),
);
app.add_plugins(common::network::ServerPlugin);
app.add_plugins(common::network::ClientPlugin);
app.add_systems(Startup, setup_crosshair);
app.add_systems(Startup, setup_blocks);
app.add_systems(Startup, setup_scene.after(setup_blocks));
app.add_systems(Update, cursor_grab);
app.add_systems(Update, update_crosshair_visibility.after(cursor_grab));
app.add_systems(Update, camera_look.after(cursor_grab).run_if(is_cursor_grabbed));
app.add_systems(Update, noclip_controller.after(camera_look).run_if(is_cursor_grabbed));
// `place_break_blocks` requires the camera's `GlobalTransform`.
// For a most up-to-date value, run it after that's been updated.
app.add_systems(
PostUpdate,
place_break_blocks
.after(TransformSystems::Propagate)
.run_if(in_state(Screen::Gameplay).and(is_cursor_grabbed)),
);
// TODO: Move this to a more general world generation module.
app.add_systems(PostUpdate, place_break_blocks.after(TransformSystems::Propagate));
app.add_observer(spawn_initial_blocks);
app.add_observer(add_block_visuals);
let mut commands = app.world_mut().commands();
match cli.mode.unwrap_or_default() {
Mode::Local => commands.queue(StartLocalServerCommand),
#[cfg(not(target_family = "wasm"))]
Mode::Host { port } => commands.queue(StartWebTransportServerCommand::new(port)),
Mode::Connect { address, digest } => commands.queue(ConnectWebTransportCommand::new(&address, digest)?),
}
// NOTE: When setting up a local server, a host-client is automatically
// connected to it thanks to the `autoconnect_host_client` observer.
app.run();
Ok(())
@ -119,23 +94,3 @@ fn spawn_initial_blocks(_event: On<Add, Started>, mut blocks: Blocks) {
}
}
}
fn start_server_or_connect(args: Res<Args>, mut commands: Commands) -> Result {
let mode = args.mode.as_ref();
let default = Mode::default();
match mode.unwrap_or(&default) {
Mode::Local => {
commands.queue(StartLocalServerCommand);
}
#[cfg(not(target_family = "wasm"))]
Mode::Host { port } => {
commands.queue(StartWebTransportServerCommand::new(*port));
}
Mode::Connect { address, digest } => {
commands.queue(ConnectWebTransportCommand::new(&address, digest.clone())?);
}
}
// NOTE: When setting up a local server, a host-client is automatically
// connected to it thanks to the `autoconnect_host_client` observer.
Ok(())
}

@ -1,9 +1,7 @@
use bevy::prelude::*;
use common::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions};
// TODO: Use picking system instead of manually raycasting.
use crate::block::*;
pub fn place_break_blocks(
mut commands: Commands,

@ -1,72 +0,0 @@
//! A high-level way to load collections of asset handles as resources.
// Taken from: https://github.com/TheBevyFlock/bevy_new_2d/blob/main/src/asset_tracking.rs
use std::collections::VecDeque;
use bevy::prelude::*;
pub fn plugin(app: &mut App) {
app.init_resource::<ResourceHandles>();
app.add_systems(PreUpdate, load_resource_assets);
}
pub trait LoadResource {
/// This will load the [`Resource`] as an [`Asset`]. When all of its asset dependencies
/// have been loaded, it will be inserted as a resource. This ensures that the resource only
/// exists when the assets are ready.
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self;
}
impl LoadResource for App {
fn load_resource<T: Resource + Asset + Clone + FromWorld>(&mut self) -> &mut Self {
self.init_asset::<T>();
let world = self.world_mut();
let value = T::from_world(world);
let assets = world.resource::<AssetServer>();
let handle = assets.add(value);
let mut handles = world.resource_mut::<ResourceHandles>();
handles
.waiting
.push_back((handle.untyped(), |world, handle| {
let assets = world.resource::<Assets<T>>();
if let Some(value) = assets.get(handle.id().typed::<T>()) {
world.insert_resource(value.clone());
}
}));
self
}
}
/// A function that inserts a loaded resource.
type InsertLoadedResource = fn(&mut World, &UntypedHandle);
#[derive(Resource, Default)]
pub struct ResourceHandles {
// Use a queue for waiting assets so they can be cycled through and moved to
// `finished` one at a time.
waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>,
finished: Vec<UntypedHandle>,
}
impl ResourceHandles {
/// Returns true if all requested [`Asset`]s have finished loading and are available as [`Resource`]s.
pub fn is_all_done(&self) -> bool {
self.waiting.is_empty()
}
}
fn load_resource_assets(world: &mut World) {
world.resource_scope(|world, mut resource_handles: Mut<ResourceHandles>| {
world.resource_scope(|world, assets: Mut<AssetServer>| {
for _ in 0..resource_handles.waiting.len() {
let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap();
if assets.is_loaded_with_dependencies(&handle) {
insert_fn(world, &handle);
resource_handles.finished.push(handle);
} else {
resource_handles.waiting.push_back((handle, insert_fn));
}
}
});
});
}

@ -1,11 +1,2 @@
pub mod asset_loading;
pub mod block;
pub mod network;
// This is mostly just re-exportings things for now, but in the future
// we might want to limit what gets exposed by the `prelude` module.
pub mod prelude {
pub use super::asset_loading::LoadResource;
pub use super::block::*;
pub use super::network;
}

@ -14,6 +14,7 @@ impl Plugin for ClientPlugin {
app.add_plugins(super::ProtocolPlugin);
}
app.add_observer(on_connecting);
app.add_observer(on_connected);
app.add_observer(on_disconnected);
@ -34,6 +35,14 @@ fn autoconnect_host_client(event: On<Add, server::Started>, mut commands: Comman
.trigger(|entity| Connect { entity });
}
fn on_connecting(event: On<Add, Connecting>, clients: Query<(), With<Client>>) {
let client = event.entity;
if !clients.contains(client) {
return; // Not a client we started. (server-side?)
};
info!("Client '{client}' connecting ...");
}
fn on_connected(event: On<Add, Connected>, clients: Query<(), With<Client>>) {
let client = event.entity;
if !clients.contains(client) {

@ -32,11 +32,9 @@ pub struct StartLocalServerCommand;
impl Command for StartLocalServerCommand {
fn apply(self, world: &mut World) {
world.spawn((
Server::default(),
Name::from("LocalServer"),
Started, // yeah it's started, alright
));
world
.spawn((Server::default(), Name::from("Server"), RawServer))
.trigger(|entity| Start { entity });
}
}

@ -43,18 +43,18 @@ impl Command for StartWebTransportServerCommand {
}
}
pub(super) fn print_certificate_digest(
event: On<Add, WebTransportServerIo>,
pub(crate) fn print_certificate_digest(
event: On<Add, Started>,
servers: Query<&WebTransportServerIo>,
) {
) -> Result {
let server = event.entity;
// SAFETY: Event guarantees the component exists on the entity.
let certificate = &servers.get(server).unwrap().certificate;
let certificate_hash = &certificate.certificate_chain().as_slice()[0].hash();
let certificate = &servers.get(server)?.certificate;
let certificate_hash = certificate.certificate_chain().as_slice()[0].hash();
let certificate_digest = certificate_hash.to_string().replace(':', "");
info!("== Certificate Digest ==");
info!(" {certificate_digest}");
info!(" (Clients use this to securely connect to the server.)");
Ok(())
}

Loading…
Cancel
Save