preliminary main menu
This commit is contained in:
+40
-4
@@ -1,12 +1,48 @@
|
||||
use bevy::prelude::*;
|
||||
use net_ui::NetUiPlugin;
|
||||
use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*};
|
||||
use main_menu::MainMenuPlugin;
|
||||
use networking::NetworkingPlugin;
|
||||
|
||||
mod net_ui;
|
||||
mod main_menu;
|
||||
mod networking;
|
||||
|
||||
/// We target this 16:9 resolution to make things easier.
|
||||
pub const WINDOW_TARGET_WIDTH_U32: u32 = 3840;
|
||||
pub const WINDOW_TARGET_HEIGHT_U32: u32 = 2160;
|
||||
pub const WINDOW_TARGET_WIDTH: f32 = WINDOW_TARGET_WIDTH_U32 as f32;
|
||||
pub const WINDOW_TARGET_HEIGHT: f32 = WINDOW_TARGET_HEIGHT_U32 as f32;
|
||||
|
||||
/// The main overarching state of the app.
|
||||
#[derive(States, Debug, Clone, Copy, Default, Eq, PartialEq, Hash)]
|
||||
pub enum AppState {
|
||||
#[default]
|
||||
Menu,
|
||||
Lobby,
|
||||
Game,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((DefaultPlugins, NetUiPlugin, NetworkingPlugin))
|
||||
.add_plugins((DefaultPlugins, MainMenuPlugin, NetworkingPlugin))
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, debug_border)
|
||||
.insert_resource(ClearColor(Color::srgb(0.07, 0.33, 0.45)))
|
||||
.init_state::<AppState>()
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
let mut projection = OrthographicProjection::default_2d();
|
||||
projection.scaling_mode = ScalingMode::AutoMin {
|
||||
min_width: WINDOW_TARGET_WIDTH,
|
||||
min_height: WINDOW_TARGET_HEIGHT,
|
||||
};
|
||||
commands.spawn((Camera2d, Projection::Orthographic(projection)));
|
||||
}
|
||||
|
||||
fn debug_border(mut gizmos: Gizmos) {
|
||||
gizmos.rect_2d(
|
||||
Isometry2d::IDENTITY,
|
||||
Vec2::new(WINDOW_TARGET_WIDTH, WINDOW_TARGET_HEIGHT),
|
||||
GREEN,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
use crate::{AppState, WINDOW_TARGET_HEIGHT, WINDOW_TARGET_WIDTH};
|
||||
use bevy::ui_widgets::Button;
|
||||
use bevy::{prelude::*, ui_widgets::Activate};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
pub struct MainMenuPlugin;
|
||||
|
||||
impl Plugin for MainMenuPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(AppState::Menu), setup_menu)
|
||||
.add_systems(OnExit(AppState::Menu), cleanup_menu)
|
||||
.add_systems(Update, (menu_redraw).run_if(in_state(AppState::Menu)))
|
||||
.add_observer(button_activate_observer)
|
||||
.add_systems(Update, (scale_ui, style_ui_elements));
|
||||
}
|
||||
}
|
||||
|
||||
const N_LAYERS: usize = 5;
|
||||
|
||||
/// A UI object that is selected.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)]
|
||||
struct Selected;
|
||||
|
||||
/// Defines colors that a UI object should turn when interactions occur.
|
||||
#[derive(Component, Clone, PartialEq, Default, Debug)]
|
||||
struct InteractionColors {
|
||||
default: Color,
|
||||
hovered: Color,
|
||||
pressed: Color,
|
||||
selected: Color,
|
||||
selected_hovered: Color,
|
||||
selected_pressed: Color,
|
||||
}
|
||||
impl InteractionColors {
|
||||
pub const DEFAULT: InteractionColors = InteractionColors {
|
||||
default: Color::srgb(0.25, 0.63, 0.84),
|
||||
hovered: Color::srgb(0.31, 0.77, 0.93),
|
||||
pressed: Color::srgb(0.16, 0.55, 0.89),
|
||||
selected: Color::srgb(0.93, 0.66, 0.06),
|
||||
selected_hovered: Color::srgb(1., 0.74, 0.16),
|
||||
selected_pressed: Color::srgb(0.86, 0.56, 0.),
|
||||
};
|
||||
pub const GOOD: InteractionColors = InteractionColors {
|
||||
default: Color::srgb(0.25, 0.75, 0.25),
|
||||
hovered: Color::srgb(0.35, 0.85, 0.35),
|
||||
pressed: Color::srgb(0.15, 0.65, 0.15),
|
||||
selected: Color::srgb(0.35, 0.8, 0.35),
|
||||
selected_hovered: Color::srgb(0.45, 0.9, 0.45),
|
||||
selected_pressed: Color::srgb(0.25, 0.7, 0.25),
|
||||
};
|
||||
pub const DANGER: InteractionColors = InteractionColors {
|
||||
default: Color::srgb(0.75, 0.25, 0.25),
|
||||
hovered: Color::srgb(0.85, 0.35, 0.35),
|
||||
pressed: Color::srgb(0.65, 0.15, 0.15),
|
||||
selected: Color::srgb(0.8, 0.3, 0.3),
|
||||
selected_hovered: Color::srgb(0.9, 0.4, 0.4),
|
||||
selected_pressed: Color::srgb(0.7, 0.2, 0.2),
|
||||
};
|
||||
}
|
||||
|
||||
/// The root of the menu
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug)]
|
||||
struct MenuRoot;
|
||||
|
||||
/// The layer that the page is on.
|
||||
#[derive(
|
||||
Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Default, Debug, Deref, DerefMut,
|
||||
)]
|
||||
struct Layer(usize);
|
||||
|
||||
/// A component that indicates the layer that this button points to.
|
||||
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Default)]
|
||||
struct NextLayer(MenuLayer);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
pub enum MenuLayer {
|
||||
#[default]
|
||||
Root,
|
||||
Play,
|
||||
LoadSave,
|
||||
Browse,
|
||||
DirectConnect,
|
||||
Settings,
|
||||
Controls,
|
||||
Graphics,
|
||||
}
|
||||
|
||||
impl MenuLayer {
|
||||
fn render(&self, layer: usize, menu_root: Entity) -> Box<dyn Scene> {
|
||||
match self {
|
||||
MenuLayer::Root => Box::new(bsn! {
|
||||
menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
menu_button("Play", Some(MenuLayer::Play)),
|
||||
menu_button("Settings", Some(MenuLayer::Settings)),
|
||||
(
|
||||
menu_button("Quit", None)
|
||||
on(|_event: On<Activate>, mut exit: MessageWriter<AppExit>| { exit.write(AppExit::Success); } )
|
||||
InteractionColors::DANGER
|
||||
),
|
||||
]
|
||||
}),
|
||||
MenuLayer::Play => Box::new(bsn! {
|
||||
menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
menu_button("Load Save", Some(MenuLayer::LoadSave)),
|
||||
menu_button("Browse Games", Some(MenuLayer::Browse)),
|
||||
menu_button("Direct Connect", Some(MenuLayer::DirectConnect)),
|
||||
]
|
||||
}),
|
||||
MenuLayer::LoadSave => Box::new(bsn! {
|
||||
menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
menu_button("Save 1", None),
|
||||
menu_button("Save 2", None),
|
||||
(
|
||||
menu_button("New Game", None)
|
||||
InteractionColors::GOOD
|
||||
),
|
||||
]
|
||||
}),
|
||||
MenuLayer::Browse => Box::new(bsn! {
|
||||
full_menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
menu_button("Game 1", None),
|
||||
]
|
||||
}),
|
||||
MenuLayer::DirectConnect => Box::new(bsn! {
|
||||
full_menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
// TODO: have IP addr text box here
|
||||
Text("Enter IP")
|
||||
]
|
||||
}),
|
||||
MenuLayer::Settings => Box::new(bsn! {
|
||||
menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
menu_button("Controls", Some(MenuLayer::Controls)),
|
||||
menu_button("Graphics", Some(MenuLayer::Graphics)),
|
||||
]
|
||||
}),
|
||||
MenuLayer::Controls => Box::new(bsn! {
|
||||
full_menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
Text("Controls..")
|
||||
]
|
||||
}),
|
||||
MenuLayer::Graphics => Box::new(bsn! {
|
||||
full_menu_layer(layer)
|
||||
ChildOf(menu_root)
|
||||
Children [
|
||||
Text("Graphics..")
|
||||
]
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the menu last frame.
|
||||
#[derive(Resource, Debug, Clone, PartialEq, Eq, Hash, Deref, DerefMut)]
|
||||
struct LastMenu(Vec<MenuLayer>);
|
||||
/// The current state of the menu.
|
||||
#[derive(Resource, Debug, Clone, PartialEq, Eq, Hash, Deref, DerefMut)]
|
||||
struct CurrentMenu(Vec<MenuLayer>);
|
||||
|
||||
fn setup_menu(mut commands: Commands) {
|
||||
commands.insert_resource(CurrentMenu(vec![MenuLayer::Root]));
|
||||
commands.insert_resource(LastMenu(vec![]));
|
||||
|
||||
commands.spawn_scene(bsn! {
|
||||
#MenuRoot
|
||||
MenuRoot
|
||||
DespawnOnExit::<AppState>(AppState::Menu)
|
||||
Node {
|
||||
width: percent(90),
|
||||
height: percent(70),
|
||||
margin: UiRect::AUTO,
|
||||
align_items: AlignItems::Start,
|
||||
justify_content: JustifyContent::Start,
|
||||
column_gap: px(10),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn cleanup_menu(mut commands: Commands) {
|
||||
commands.remove_resource::<CurrentMenu>();
|
||||
commands.remove_resource::<LastMenu>();
|
||||
}
|
||||
|
||||
fn menu_redraw(
|
||||
mut commands: Commands,
|
||||
mut last_menu: ResMut<LastMenu>,
|
||||
current_menu: Res<CurrentMenu>,
|
||||
q_layers: Query<(Entity, &Layer), Without<MenuRoot>>,
|
||||
s_menu_root: Single<Entity, (With<MenuRoot>, Without<Layer>)>,
|
||||
) {
|
||||
if current_menu.is_changed() {
|
||||
let last_equal_layer = last_menu
|
||||
.iter()
|
||||
.zip(current_menu.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(idx, (last, current))| (last == current).then_some(idx as i32))
|
||||
.last()
|
||||
.unwrap_or(-1);
|
||||
|
||||
// delete any layers that have been deleted/changed
|
||||
if last_equal_layer < last_menu.len() as i32 {
|
||||
for (e, layer) in q_layers.iter() {
|
||||
if layer.0 as i32 > last_equal_layer {
|
||||
commands.entity(e).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// render new/changed layers
|
||||
for (idx, layer) in current_menu
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip((last_equal_layer + 1) as usize)
|
||||
{
|
||||
commands.spawn_scene(layer.render(idx, *s_menu_root));
|
||||
}
|
||||
|
||||
// update last menu to match current menu
|
||||
last_menu.0 = current_menu.0.clone();
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_layer(layer: usize) -> impl Scene {
|
||||
let name = format!("MenuLayer{}", layer);
|
||||
bsn! {
|
||||
Name(name)
|
||||
Layer(layer)
|
||||
Node {
|
||||
width: percent(100.0 / N_LAYERS as f32),
|
||||
height: percent(100),
|
||||
flex_direction: FlexDirection::Column,
|
||||
// align_items: AlignItems::Start,
|
||||
align_items: AlignItems::Baseline,
|
||||
justify_content: JustifyContent::FlexEnd,
|
||||
row_gap: px(100),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn full_menu_layer(layer: usize) -> impl Scene {
|
||||
bsn! {
|
||||
menu_layer(layer)
|
||||
Node {
|
||||
width: Val::Auto,
|
||||
flex_grow: 1.,
|
||||
}
|
||||
BackgroundColor(Color::srgba(0.8, 0.1, 0.1, 0.7))
|
||||
}
|
||||
}
|
||||
|
||||
fn menu_button(label: &str, next_layer: Option<MenuLayer>) -> impl Scene {
|
||||
let next_layer_component = next_layer
|
||||
.map(|n| Box::new(bsn! { NextLayer(n) }) as Box<dyn Scene>)
|
||||
.unwrap_or(Box::new(bsn! {}));
|
||||
|
||||
bsn! {
|
||||
Button
|
||||
Node {
|
||||
width: px(600),
|
||||
height: px(130),
|
||||
border: px(6),
|
||||
// border_radius: BorderRadius::MAX,
|
||||
justify_content: JustifyContent::Center,
|
||||
align_items: AlignItems::Center,
|
||||
margin: UiRect::all(px(4)),
|
||||
}
|
||||
next_layer_component
|
||||
BorderColor::from(Color::BLACK)
|
||||
InteractionColors::DEFAULT
|
||||
Interaction
|
||||
Children [(
|
||||
Text(label)
|
||||
TextFont {
|
||||
// font: FontSourceTemplate::Handle("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: px(66.0),
|
||||
}
|
||||
TextColor(Color::srgb(0.9, 0.9, 0.9))
|
||||
TextShadow
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
fn button_activate_observer(
|
||||
event: On<Activate>,
|
||||
mut commands: Commands,
|
||||
mut menu: ResMut<CurrentMenu>,
|
||||
q_next_layer: Query<(&NextLayer, &ChildOf)>,
|
||||
q_layer: Query<(&Layer, &Children)>,
|
||||
q_selected: Query<(Entity, Has<Selected>)>,
|
||||
) {
|
||||
if let Ok((next_layer, parent)) = q_next_layer.get(event.entity) {
|
||||
let Ok((layer, layer_entries)) = q_layer.get(parent.parent()) else {
|
||||
warn!(
|
||||
"\"Activate\" triggered on entity with `NextLayer`, but it's parent doesn't have `Layer`!"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
// remove all the layers after
|
||||
if (layer.0 as usize) < menu.len() {
|
||||
menu.0.drain((layer.0 + 1)..);
|
||||
}
|
||||
// push next layer
|
||||
menu.push(next_layer.0);
|
||||
|
||||
// update the buttons so the correct one is selected
|
||||
for (entity, selected) in q_selected.iter_many(layer_entries) {
|
||||
if entity == event.entity {
|
||||
if !selected {
|
||||
commands.entity(entity).insert(Selected);
|
||||
}
|
||||
} else if selected {
|
||||
commands.entity(entity).remove::<Selected>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Styles the UI elements according to their [`InteractionColors`].
|
||||
fn style_ui_elements(
|
||||
mut q_interaction_set: ParamSet<(
|
||||
Query<
|
||||
(
|
||||
&Interaction,
|
||||
&mut BackgroundColor,
|
||||
&InteractionColors,
|
||||
Has<Selected>,
|
||||
),
|
||||
(Or<(Changed<Interaction>, Added<Selected>)>, With<Button>),
|
||||
>,
|
||||
Query<(&Interaction, &mut BackgroundColor, &InteractionColors)>,
|
||||
)>,
|
||||
mut removed_selected: RemovedComponents<Selected>,
|
||||
) {
|
||||
for (interaction, mut bg, colors, selected) in q_interaction_set.p0().iter_mut() {
|
||||
let new_color = match (selected, interaction) {
|
||||
(true, Interaction::Pressed) => colors.selected_pressed,
|
||||
(true, Interaction::Hovered) => colors.selected_hovered,
|
||||
(true, Interaction::None) => colors.selected,
|
||||
(false, Interaction::Pressed) => colors.pressed,
|
||||
(false, Interaction::Hovered) => colors.hovered,
|
||||
(false, Interaction::None) => colors.default,
|
||||
};
|
||||
bg.0 = new_color;
|
||||
}
|
||||
|
||||
// also do entities that have had `Selected` removed
|
||||
for entity in removed_selected.read() {
|
||||
if let Ok((interaction, mut bg, colors)) = q_interaction_set.p1().get_mut(entity) {
|
||||
let new_color = match interaction {
|
||||
Interaction::Pressed => colors.pressed,
|
||||
Interaction::Hovered => colors.hovered,
|
||||
Interaction::None => colors.default,
|
||||
};
|
||||
bg.0 = new_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_ui(mut ui_scale: ResMut<UiScale>, window: Single<&Window>) {
|
||||
let size = window.size();
|
||||
let scale_factor = f32::min(size.x / WINDOW_TARGET_WIDTH, size.y / WINDOW_TARGET_HEIGHT);
|
||||
ui_scale.0 = scale_factor;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
+14
-177
@@ -1,188 +1,25 @@
|
||||
//! 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
|
||||
// ---------------------------------------------------------------------------
|
||||
use bevy::prelude::*;
|
||||
|
||||
/// 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));
|
||||
// TODO: impl
|
||||
let _ = app;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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);
|
||||
/// ```
|
||||
// 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