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.
|
//! Bevy 0.19 demo: a server/client session launcher.
|
||||||
//!
|
//!
|
||||||
//! The launcher UI lives in its own module/plugin: [`net_ui::NetUiPlugin`].
|
//! The launcher UI lives in its own module/plugin: [`net_ui::NetUiPlugin`].
|
||||||
|
//! Networking logic lives in [`networking::NetworkingPlugin`].
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use net_ui::NetUiPlugin;
|
use net_ui::NetUiPlugin;
|
||||||
|
use networking::NetworkingPlugin;
|
||||||
|
|
||||||
mod net_ui;
|
mod net_ui;
|
||||||
|
mod networking;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins((DefaultPlugins, NetUiPlugin))
|
.add_plugins((DefaultPlugins, NetUiPlugin, NetworkingPlugin))
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
+32
-33
@@ -1,8 +1,8 @@
|
|||||||
//! Bevy 0.19 demo: a server/client session launcher UI, as a plugin.
|
//! 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/
|
//! This is *UI only*. Buttons emit [`NetworkAction`](crate::networking::NetworkAction)
|
||||||
//! Stop handlers just log the aeronet calls they *would* make, so the wiring is
|
//! intents to the networking layer; they never touch aeronet themselves. This
|
||||||
//! a drop-in replacement later.
|
//! 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
|
//! UI is built with Bevy 0.19's new scene notation (`bsn!`) composing classic
|
||||||
//! `bevy_ui` components (`Node`, `Button`, `Text`, `EditableText`).
|
//! `bevy_ui` components (`Node`, `Button`, `Text`, `EditableText`).
|
||||||
@@ -17,6 +17,8 @@ use bevy::{
|
|||||||
text::EditableText,
|
text::EditableText,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::networking::NetworkAction;
|
||||||
|
|
||||||
/// Plugin that provides the session-launcher UI.
|
/// Plugin that provides the session-launcher UI.
|
||||||
///
|
///
|
||||||
/// Registers the [`NetUi`] resource, spawns a 2D camera (required for UI), and
|
/// Registers the [`NetUi`] resource, spawns a 2D camera (required for UI), and
|
||||||
@@ -26,7 +28,8 @@ pub struct NetUiPlugin;
|
|||||||
|
|
||||||
impl Plugin for NetUiPlugin {
|
impl Plugin for NetUiPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.init_resource::<InputFocus>()
|
app.configure_sets(Update, NetUiSystems)
|
||||||
|
.init_resource::<InputFocus>()
|
||||||
.init_resource::<NetUi>()
|
.init_resource::<NetUi>()
|
||||||
.add_systems(Startup, spawn_camera)
|
.add_systems(Startup, spawn_camera)
|
||||||
.add_systems(
|
.add_systems(
|
||||||
@@ -36,8 +39,8 @@ impl Plugin for NetUiPlugin {
|
|||||||
// The `bsn!` scene is declarative, so the cheapest "re-render"
|
// The `bsn!` scene is declarative, so the cheapest "re-render"
|
||||||
// is to despawn the previous scene root and spawn the current one.
|
// is to despawn the previous scene root and spawn the current one.
|
||||||
rebuild_net_ui.run_if(resource_changed::<NetUi>),
|
rebuild_net_ui.run_if(resource_changed::<NetUi>),
|
||||||
// React to button presses and form fields.
|
// React to button presses and form fields, emitting intents.
|
||||||
handle_buttons,
|
handle_buttons.in_set(NetUiSystems),
|
||||||
// Repaint the status + log text from the resource.
|
// Repaint the status + log text from the resource.
|
||||||
paint_log.run_if(resource_changed::<NetUi>),
|
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
|
// State
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -165,6 +174,7 @@ fn rebuild_net_ui(
|
|||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn handle_buttons(
|
fn handle_buttons(
|
||||||
mut ui: ResMut<NetUi>,
|
mut ui: ResMut<NetUi>,
|
||||||
|
mut actions: MessageWriter<NetworkAction>,
|
||||||
url_field: Query<&EditableText, With<ConnectUrlField>>,
|
url_field: Query<&EditableText, With<ConnectUrlField>>,
|
||||||
port_field: Query<&EditableText, With<HostPortField>>,
|
port_field: Query<&EditableText, With<HostPortField>>,
|
||||||
q_host: Query<&Interaction, (With<HostButton>, Changed<Interaction>)>,
|
q_host: Query<&Interaction, (With<HostButton>, Changed<Interaction>)>,
|
||||||
@@ -176,7 +186,7 @@ fn handle_buttons(
|
|||||||
) {
|
) {
|
||||||
use Interaction::Pressed;
|
use Interaction::Pressed;
|
||||||
|
|
||||||
// -------- Screen navigation -------------------------------------------
|
// -------- Screen navigation (pure UI, no networking) ------------------
|
||||||
if q_host.iter().any(|i| *i == Pressed) {
|
if q_host.iter().any(|i| *i == Pressed) {
|
||||||
ui.screen = Screen::Host;
|
ui.screen = Screen::Host;
|
||||||
ui.status = "Host setup — pick a listen port.".into();
|
ui.status = "Host setup — pick a listen port.".into();
|
||||||
@@ -191,46 +201,35 @@ fn handle_buttons(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------- Host: start server ------------------------------------------
|
// -------- 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) {
|
if q_start.iter().any(|i| *i == Pressed) {
|
||||||
let mut port = port_field
|
let port: u16 = port_field
|
||||||
.single()
|
.single()
|
||||||
.map(|e| e.value().to_string())
|
.map(|e| e.value().to_string())
|
||||||
.unwrap_or_default();
|
.ok()
|
||||||
if port.is_empty() {
|
.and_then(|p| p.trim().parse().ok())
|
||||||
port = "25570".into();
|
.unwrap_or(25570);
|
||||||
}
|
actions.write(NetworkAction::StartHost { port });
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Client: connect ---------------------------------------------
|
// -------- Client: connect ---------------------------------------------
|
||||||
if q_connect.iter().any(|i| *i == Pressed) {
|
if q_connect.iter().any(|i| *i == Pressed) {
|
||||||
let mut url = url_field
|
let url = url_field
|
||||||
.single()
|
.single()
|
||||||
.map(|e| e.value().to_string())
|
.map(|e| e.value().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if url.is_empty() {
|
let url = if url.trim().is_empty() {
|
||||||
url = "ws://127.0.0.1:25570".into();
|
"ws://127.0.0.1:25570".to_string()
|
||||||
}
|
} else {
|
||||||
ui.push_log(format!(
|
url.trim().to_string()
|
||||||
"// TODO aeronet: client -> commands.spawn_empty().queue(\n \
|
};
|
||||||
WebSocketClient::connect(ClientConfig::builder().with_no_cert_validation(), \"{url}\")\n \
|
actions.write(NetworkAction::Connect { url });
|
||||||
);"
|
|
||||||
));
|
|
||||||
ui.status = format!("Connecting to {url} (UI shell only)");
|
|
||||||
ui.screen = Screen::Session;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Session: stop -----------------------------------------------
|
// -------- Session: stop -----------------------------------------------
|
||||||
if q_stop.iter().any(|i| *i == Pressed) {
|
if q_stop.iter().any(|i| *i == Pressed) {
|
||||||
ui.push_log("// TODO aeronet: stop -> commands.trigger(Disconnect::new(session, \"user\"))".to_string());
|
actions.write(NetworkAction::Disconnect);
|
||||||
ui.status = "Idle — select an action.".into();
|
|
||||||
ui.screen = Screen::Menu;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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