create scaffolding for networking logic
This commit is contained in:
+4
-1
@@ -1,15 +1,18 @@
|
||||
//! Bevy 0.19 demo: a server/client session launcher.
|
||||
//!
|
||||
//! The launcher UI lives in its own module/plugin: [`net_ui::NetUiPlugin`].
|
||||
//! Networking logic lives in [`networking::NetworkingPlugin`].
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use net_ui::NetUiPlugin;
|
||||
use networking::NetworkingPlugin;
|
||||
|
||||
mod net_ui;
|
||||
mod networking;
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((DefaultPlugins, NetUiPlugin))
|
||||
.add_plugins((DefaultPlugins, NetUiPlugin, NetworkingPlugin))
|
||||
.run();
|
||||
}
|
||||
+32
-33
@@ -1,8 +1,8 @@
|
||||
//! Bevy 0.19 demo: a server/client session launcher UI, as a plugin.
|
||||
//!
|
||||
//! This is *UI only* — no aeronet networking is wired up yet. The Start/Connect/
|
||||
//! Stop handlers just log the aeronet calls they *would* make, so the wiring is
|
||||
//! a drop-in replacement later.
|
||||
//! This is *UI only*. Buttons emit [`NetworkAction`](crate::networking::NetworkAction)
|
||||
//! intents to the networking layer; they never touch aeronet themselves. This
|
||||
//! module renders whatever [`NetUi`] says, and reacts to that resource changing.
|
||||
//!
|
||||
//! UI is built with Bevy 0.19's new scene notation (`bsn!`) composing classic
|
||||
//! `bevy_ui` components (`Node`, `Button`, `Text`, `EditableText`).
|
||||
@@ -17,6 +17,8 @@ use bevy::{
|
||||
text::EditableText,
|
||||
};
|
||||
|
||||
use crate::networking::NetworkAction;
|
||||
|
||||
/// Plugin that provides the session-launcher UI.
|
||||
///
|
||||
/// Registers the [`NetUi`] resource, spawns a 2D camera (required for UI), and
|
||||
@@ -26,7 +28,8 @@ pub struct NetUiPlugin;
|
||||
|
||||
impl Plugin for NetUiPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<InputFocus>()
|
||||
app.configure_sets(Update, NetUiSystems)
|
||||
.init_resource::<InputFocus>()
|
||||
.init_resource::<NetUi>()
|
||||
.add_systems(Startup, spawn_camera)
|
||||
.add_systems(
|
||||
@@ -36,8 +39,8 @@ impl Plugin for NetUiPlugin {
|
||||
// The `bsn!` scene is declarative, so the cheapest "re-render"
|
||||
// is to despawn the previous scene root and spawn the current one.
|
||||
rebuild_net_ui.run_if(resource_changed::<NetUi>),
|
||||
// React to button presses and form fields.
|
||||
handle_buttons,
|
||||
// React to button presses and form fields, emitting intents.
|
||||
handle_buttons.in_set(NetUiSystems),
|
||||
// Repaint the status + log text from the resource.
|
||||
paint_log.run_if(resource_changed::<NetUi>),
|
||||
),
|
||||
@@ -45,6 +48,12 @@ impl Plugin for NetUiPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Systems that read the UI widgets and emit [`NetworkAction`] intents.
|
||||
/// The networking layer schedules after this set, so it sees intents the
|
||||
/// same frame the button was pressed.
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct NetUiSystems;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -165,6 +174,7 @@ fn rebuild_net_ui(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_buttons(
|
||||
mut ui: ResMut<NetUi>,
|
||||
mut actions: MessageWriter<NetworkAction>,
|
||||
url_field: Query<&EditableText, With<ConnectUrlField>>,
|
||||
port_field: Query<&EditableText, With<HostPortField>>,
|
||||
q_host: Query<&Interaction, (With<HostButton>, Changed<Interaction>)>,
|
||||
@@ -176,7 +186,7 @@ fn handle_buttons(
|
||||
) {
|
||||
use Interaction::Pressed;
|
||||
|
||||
// -------- Screen navigation -------------------------------------------
|
||||
// -------- Screen navigation (pure UI, no networking) ------------------
|
||||
if q_host.iter().any(|i| *i == Pressed) {
|
||||
ui.screen = Screen::Host;
|
||||
ui.status = "Host setup — pick a listen port.".into();
|
||||
@@ -191,46 +201,35 @@ fn handle_buttons(
|
||||
}
|
||||
|
||||
// -------- Host: start server ------------------------------------------
|
||||
// The next three only *request* the action; the networking layer owns the
|
||||
// result and updates `NetUi`. None of this spawns sockets or reacts to them.
|
||||
if q_start.iter().any(|i| *i == Pressed) {
|
||||
let mut port = port_field
|
||||
let port: u16 = port_field
|
||||
.single()
|
||||
.map(|e| e.value().to_string())
|
||||
.unwrap_or_default();
|
||||
if port.is_empty() {
|
||||
port = "25570".into();
|
||||
}
|
||||
ui.push_log(format!(
|
||||
"// TODO aeronet: server -> commands.spawn_empty().queue(\n \
|
||||
WebSocketServer::open(ServerConfig::builder().with_bind_raw(None, {port}))\n \
|
||||
);"
|
||||
));
|
||||
ui.status = format!("Hosting on 0.0.0.0:{port} (UI shell only)");
|
||||
ui.screen = Screen::Session;
|
||||
.ok()
|
||||
.and_then(|p| p.trim().parse().ok())
|
||||
.unwrap_or(25570);
|
||||
actions.write(NetworkAction::StartHost { port });
|
||||
}
|
||||
|
||||
// -------- Client: connect ---------------------------------------------
|
||||
if q_connect.iter().any(|i| *i == Pressed) {
|
||||
let mut url = url_field
|
||||
let url = url_field
|
||||
.single()
|
||||
.map(|e| e.value().to_string())
|
||||
.unwrap_or_default();
|
||||
if url.is_empty() {
|
||||
url = "ws://127.0.0.1:25570".into();
|
||||
}
|
||||
ui.push_log(format!(
|
||||
"// TODO aeronet: client -> commands.spawn_empty().queue(\n \
|
||||
WebSocketClient::connect(ClientConfig::builder().with_no_cert_validation(), \"{url}\")\n \
|
||||
);"
|
||||
));
|
||||
ui.status = format!("Connecting to {url} (UI shell only)");
|
||||
ui.screen = Screen::Session;
|
||||
let url = if url.trim().is_empty() {
|
||||
"ws://127.0.0.1:25570".to_string()
|
||||
} else {
|
||||
url.trim().to_string()
|
||||
};
|
||||
actions.write(NetworkAction::Connect { url });
|
||||
}
|
||||
|
||||
// -------- Session: stop -----------------------------------------------
|
||||
if q_stop.iter().any(|i| *i == Pressed) {
|
||||
ui.push_log("// TODO aeronet: stop -> commands.trigger(Disconnect::new(session, \"user\"))".to_string());
|
||||
ui.status = "Idle — select an action.".into();
|
||||
ui.screen = Screen::Menu;
|
||||
actions.write(NetworkAction::Disconnect);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
//! Scaffold for the aeronet networking logic.
|
||||
//!
|
||||
//! **This file is where the actual server/client code goes.** Right now it
|
||||
//! depends only on `bevy::prelude`, so it compiles and runs standalone. The real
|
||||
//! sockets/transport sit behind `// TODO(aeronet)` markers — enabling them means
|
||||
//! adding `aeronet` + an IO layer + `aeronet_transport` to `Cargo.toml` and
|
||||
//! swapping the marked bodies for the real calls (exact API noted below).
|
||||
//!
|
||||
//! ## Division of responsibility
|
||||
//!
|
||||
//! - **UI** (`net_ui`): presentation only. It emits [`NetworkAction`] intents
|
||||
//! and renders whatever [`NetUi`](crate::net_ui::NetUi) says.
|
||||
//! - **Networking** (this module): owns the connection lifecycle. It consumes
|
||||
//! actions, talks to aeronet, and writes status/log/screen back into `NetUi`.
|
||||
//!
|
||||
//! So the UI never touches aeronet, and the networking layer never spawns UI.
|
||||
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
use crate::net_ui::{NetUi, Screen};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Intent channel: UI -> networking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// An action the user asked for through the UI. These are the *only* messages
|
||||
/// the UI sends about networking; everything else it just displays.
|
||||
// In bevy 0.19 events are "messages": `#[derive(Message)]` + `MessageReader`/
|
||||
// `MessageWriter` system params (the old `Event`/`EventReader`/`EventWriter`).
|
||||
#[derive(Message, Debug, Clone)]
|
||||
pub enum NetworkAction {
|
||||
/// User pressed "Start server".
|
||||
StartHost { port: u16 },
|
||||
/// User pressed "Connect".
|
||||
Connect { url: String },
|
||||
/// User pressed "Disconnect / Stop".
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Owns all networking state and lifecycle.
|
||||
pub struct NetworkingPlugin;
|
||||
|
||||
impl Plugin for NetworkingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// Register the UI->networking intent channel.
|
||||
app.add_message::<NetworkAction>();
|
||||
|
||||
// TODO(aeronet): register the IO + transport plugins once the crates are
|
||||
// added. The exact set depends on which IO layer(s) the app supports:
|
||||
// host -> app.add_plugins((aeronet_websocket::server::WebSocketServerPlugin, AeronetTransportPlugin));
|
||||
// client-> app.add_plugins((aeronet_websocket::client::WebSocketClientPlugin, AeronetTransportPlugin));
|
||||
//
|
||||
// Each *_Plugin pulls in `aeronet_io` automatically but NOT transport.
|
||||
app.init_resource::<Network>()
|
||||
.add_systems(Update, handle_actions.after(crate::net_ui::NetUiSystems));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Networking state + the aeronet connection entity, once wired.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct Network {
|
||||
/// Which process role we're running as (server vs client).
|
||||
role: Role,
|
||||
/// Entity representing the current aeronet session/server, if any.
|
||||
///
|
||||
/// TODO(aeronet): filled by the connection/disconnection observers below.
|
||||
/// The entity *is* the peer identifier — there is no `ClientId` in aeronet.
|
||||
session: Option<Entity>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Role {
|
||||
#[default]
|
||||
None,
|
||||
Host,
|
||||
Client,
|
||||
}
|
||||
|
||||
/// TODO(aeronet): transport lanes must be declared up front. Add these (and a
|
||||
/// `LaneKind` import) when the crate lands. Boilerplate an order-of-magnitude
|
||||
/// easier done with the crate than comments:
|
||||
///
|
||||
/// ```ignore
|
||||
/// const LANES: [LaneKind; 2] = [
|
||||
/// LaneKind::ReliableOrdered, // game state / actions
|
||||
/// LaneKind::UnreliableOrdered, // e.g. position snapshots
|
||||
/// ];
|
||||
/// const SEND_LANE: LaneIndex = LaneIndex::new(0);
|
||||
/// ```
|
||||
// #[allow(dead_code)]
|
||||
// const _: () = ();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Systems
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Consume user actions and drive the (future) aeronet connection.
|
||||
fn handle_actions(
|
||||
mut actions: MessageReader<NetworkAction>,
|
||||
mut net: ResMut<Network>,
|
||||
mut ui: ResMut<NetUi>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for action in actions.read() {
|
||||
match action {
|
||||
NetworkAction::StartHost { port } => {
|
||||
start_host(&mut net, &mut ui, &mut commands, *port);
|
||||
}
|
||||
NetworkAction::Connect { url } => {
|
||||
connect(&mut net, &mut ui, &mut commands, url);
|
||||
}
|
||||
NetworkAction::Disconnect => {
|
||||
disconnect(&mut net, &mut ui, &mut commands);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// User wants to act as a server listening on `port`.
|
||||
fn start_host(net: &mut Network, ui: &mut NetUi, commands: &mut Commands, port: u16) {
|
||||
net.role = Role::Host;
|
||||
ui.push_log(format!("starting server on 0.0.0.0:{port}"));
|
||||
|
||||
// Only used once the TODO(aeronet) bodies are filled in.
|
||||
let _ = commands;
|
||||
|
||||
// TODO(aeronet): open a server entity.
|
||||
// let identity = aeronet_websocket::server::Identity::self_signed(["localhost", "127.0.0.1"])?;
|
||||
// commands.spawn_empty().queue(WebSocketServer::open(
|
||||
// ServerConfig::builder().with_bind(([0,0,0,0], port).into()).with_identity(identity),
|
||||
// ));
|
||||
// Then register observers (in the plugin's `build`):
|
||||
// app.add_observer(on_opened) // On<Add, Server> -> record server entity
|
||||
// .add_observer(on_client_conn) // On<Add, Session> -> insert a Transport on the session
|
||||
// .add_observer(on_close) // On<Disconnected> -> clean up + push to NetUi.log
|
||||
|
||||
ui.status = format!("hosting on 0.0.0.0:{port} (scaffold — no listener yet)");
|
||||
ui.screen = Screen::Session;
|
||||
}
|
||||
|
||||
/// User wants to connect to a server at `url`.
|
||||
fn connect(net: &mut Network, ui: &mut NetUi, commands: &mut Commands, url: &str) {
|
||||
net.role = Role::Client;
|
||||
ui.push_log(format!("connecting to {url}"));
|
||||
|
||||
// Only used once the TODO(aeronet) bodies are filled in.
|
||||
let _ = commands;
|
||||
|
||||
// TODO(aeronet): spawn a session entity and queue a connect.
|
||||
// let config = ClientConfig::builder().with_no_cert_validation(); // demo only
|
||||
// commands.spawn((TransportConfig::default(),))
|
||||
// .queue(WebSocketClient::connect(config, url));
|
||||
// Observers:
|
||||
// on_connecting // On<Add, SessionEndpoint> -> "connecting…"
|
||||
// on_connected // On<Add, Session> -> insert Transport w/ LANES, record session entity
|
||||
// on_disconnected// On<Disconnected> -> log reason, clear net.session, back to menu
|
||||
|
||||
ui.status = format!("connecting to {url} (scaffold — no socket yet)");
|
||||
ui.screen = Screen::Session;
|
||||
}
|
||||
|
||||
/// User wants to tear down the current connection.
|
||||
fn disconnect(net: &mut Network, ui: &mut NetUi, commands: &mut Commands) {
|
||||
// `commands` is only used by the TODO(aeronet) block below.
|
||||
let _ = commands;
|
||||
ui.push_log("disconnecting");
|
||||
|
||||
// TODO(aeronet): never despawn/remove manually — trigger the event so
|
||||
// cleanup observers run:
|
||||
// if let Some(session) = net.session {
|
||||
// commands.trigger(Disconnect::new(session, "user pressed stop"));
|
||||
// }
|
||||
|
||||
net.session = None;
|
||||
net.role = Role::None;
|
||||
ui.status = "idle — select an action.".into();
|
||||
ui.screen = Screen::Menu;
|
||||
}
|
||||
Reference in New Issue
Block a user