~starkingdoms/starkingdoms

386a970b53fcd24c4701300b2a8a02a5b5844ce6 — core 27 days ago 71ade52
recursive disconnect
M .cargo/config.toml => .cargo/config.toml +2 -2
@@ 5,8 5,8 @@ xtask = "run --release --package xtask --"
#rustc-wrapper = "/usr/bin/sccache"

[target.x86_64-unknown-linux-gnu]
#linker = "mold"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold", "-Zshare-generics=y"]

[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']

M crates/unified/Cargo.toml => crates/unified/Cargo.toml +2 -2
@@ 4,8 4,8 @@ description = "A game about floating through space"
edition = "2024"
version = "0.1.0"

[lib]
crate-type = ["cdylib", "rlib"]
#[lib]
#crate-type = ["cdylib", "rlib"]

[dependencies]
bevy = { version = "0.17", default-features = false, features = [

M crates/unified/src/attachment.rs => crates/unified/src/attachment.rs +1 -1
@@ 33,7 33,7 @@ pub struct Peer {
#[derive(Component, Serialize, Deserialize, MapEntities)]
#[relationship(relationship_target = Joints)]
pub struct JointOf(#[entities] pub Entity);
#[derive(Component, Serialize, Deserialize, MapEntities, Debug)]
#[derive(Component, Serialize, Deserialize, MapEntities, Debug, Clone)]
#[relationship_target(relationship = JointOf)]
pub struct Joints(#[entities] Vec<Entity>);
impl Deref for Joints {

M crates/unified/src/client/mod.rs => crates/unified/src/client/mod.rs +7 -4
@@ 26,7 26,7 @@ use bevy::window::PrimaryWindow;
use planet::incoming_planets::incoming_planets_plugin;

pub struct ClientPlugin {
    pub server: String,
    pub server: Option<String>,
}
impl Plugin for ClientPlugin {
    fn build(&self, app: &mut App) {


@@ 34,15 34,13 @@ impl Plugin for ClientPlugin {
        app
            .insert_resource(CursorWorldCoordinates(None))
            .add_systems(Startup, move |mut commands: Commands| {
                let Some(server) = server.as_ref() else { return };
                let config = net::websocket_config();

                commands
                    .spawn(Name::new("default-session"))
                    .queue(WebSocketClient::connect(config, server.clone()));
            })
            .add_observer(net::on_connecting)
            .add_observer(net::on_connected)
            .add_observer(net::on_disconnected)
            .add_systems(Startup, setup_graphics)
            .add_systems(Update, update_cursor_position)
            .add_systems(Update, follow_camera)


@@ 55,6 53,11 @@ impl Plugin for ClientPlugin {
            .add_plugins(ui_plugin)
            .add_plugins(zoom_plugin)
            .insert_resource(DebugPickingMode::Disabled);
        if self.server.is_some() {
            app.add_observer(net::on_connecting)
                .add_observer(net::on_connected)
                .add_observer(net::on_disconnected);
        }
    }
}


M crates/unified/src/client_plugins.rs => crates/unified/src/client_plugins.rs +2 -5
@@ 11,22 11,19 @@ use bevy::ui::UiPlugin;
use bevy_replicon::RepliconPlugins;

pub struct ClientPluginGroup {
    pub server: String,
    pub server: Option<String>,
}
impl PluginGroup for ClientPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add_group(DefaultPlugins.build().disable::<LogPlugin>())
            .add_group(RepliconPlugins)
            .add(WebSocketClientPlugin)
            .add(AeronetRepliconClientPlugin)
            .add(MeshPickingPlugin)
            .add(DebugPickingPlugin)
            .add(UiPlugin)
            .add(ClientPlugin {
                server: self.server,
            })

            .add(UiPlugin)
            //.add(PhysicsDebugPlugin) -- debug rendering
            //.add(FpsOverlayPlugin::default())
            //.add(EguiPlugin::default())

D crates/unified/src/lib.rs => crates/unified/src/lib.rs +0 -43
@@ 1,43 0,0 @@
#![warn(clippy::pedantic)] // Be annoying, and disable specific irritating lints if needed
#![deny(
    clippy::allow_attributes_without_reason,
    clippy::assertions_on_result_states
)]
#![warn(clippy::if_then_some_else_none)]
#![allow(clippy::type_complexity, reason = "Bevy makes this a nightmare")]
#![allow(clippy::needless_pass_by_value, reason = "Bevy makes this a nightmare")]
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "We cast ints to floats a lot"
)]
#![allow(clippy::missing_panics_doc, reason = "Gamedev! We panic a lot")]
#![allow(clippy::too_many_arguments, reason = "Le Bevy:tm:")]
#![allow(
    clippy::too_many_lines,
    reason = "With the three of us, this is impossible"
)]

//! Primary entrypoint for the lib... mostly useful for wasm
#[cfg(target_arch = "wasm32")]
pub mod wasm_entrypoint;
#[cfg(target_arch = "wasm32")]
pub use wasm_entrypoint::*;

pub mod attachment;
pub mod client;
pub mod client_plugins;
pub mod config;
pub mod ecs;
#[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
pub mod particle_editor;
pub mod particles;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server_plugins;
pub mod shared_plugins;

pub mod physics;
pub mod prelude;
\ No newline at end of file

M crates/unified/src/main.rs => crates/unified/src/main.rs +97 -13
@@ 1,16 1,65 @@
use bevy::log::tracing_subscriber;
use starkingdoms::prelude::*;
#![warn(clippy::pedantic)] // Be annoying, and disable specific irritating lints if needed
#![deny(
    clippy::allow_attributes_without_reason,
    clippy::assertions_on_result_states
)]
#![warn(clippy::if_then_some_else_none)]
#![allow(clippy::type_complexity, reason = "Bevy makes this a nightmare")]
#![allow(clippy::needless_pass_by_value, reason = "Bevy makes this a nightmare")]
#![allow(
    clippy::cast_precision_loss,
    clippy::cast_possible_truncation,
    clippy::cast_sign_loss,
    reason = "We cast ints to floats a lot"
)]
#![allow(clippy::missing_panics_doc, reason = "Gamedev! We panic a lot")]
#![allow(clippy::too_many_arguments, reason = "Le Bevy:tm:")]
#![allow(
    clippy::too_many_lines,
    reason = "With the three of us, this is impossible"
)]

pub mod attachment;
pub mod client;
pub mod client_plugins;
pub mod config;
pub mod ecs;
#[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
pub mod particle_editor;
pub mod particles;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server;
#[cfg(all(not(target_arch = "wasm32"), feature = "native"))]
pub mod server_plugins;
pub mod shared_plugins;

pub mod physics;
pub mod prelude;

#[cfg(target_arch = "wasm32")]
pub mod wasm_entrypoint;
#[cfg(target_arch = "wasm32")]
pub use wasm_entrypoint::*;

use bevy::log::{tracing_subscriber, LogPlugin};
use crate::prelude::*;
use clap::Parser;
use starkingdoms::client_plugins::ClientPluginGroup;
use crate::client_plugins::ClientPluginGroup;
#[cfg(not(target_arch = "wasm32"))]
use starkingdoms::server_plugins::ServerPluginGroup;
use starkingdoms::shared_plugins::SharedPluginGroup;
use crate::server_plugins::ServerPluginGroup;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::process::exit;
use std::str::FromStr;
use std::time::Duration;
use bevy::app::ScheduleRunnerPlugin;
use bevy::diagnostic::FrameCountPlugin;
use bevy::state::app::StatesPlugin;
use bevy::time::TimePlugin;
use bevy::ui::UiPlugin;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::Directive;
use tracing_subscriber::util::SubscriberInitExt;
use crate::shared_plugins::SharedPluginGroup;

#[derive(Parser, Debug, Clone)]
#[command(version, about)]


@@ 48,7 97,12 @@ fn run(cli: Cli) -> AppExit {
                warn!("This can result in segfaults and inconsistent behavior! If there is weirdness, try disabling it.");
                warn!("-+-+-+-+-+-+- Starting with hotpatching enabled -+-+-+-+-+-+-");
            }
            app.add_plugins(ClientPluginGroup { server });
            app.add_plugins(
                DefaultPlugins.build()
                    .disable::<LogPlugin>()
                    .disable::<UiPlugin>()
            );
            app.add_plugins(ClientPluginGroup { server: Some(server) });
            app.add_plugins(SharedPluginGroup);
        }
        #[cfg(not(target_arch = "wasm32"))]


@@ 57,7 111,7 @@ fn run(cli: Cli) -> AppExit {
            tick_rate,
            max_clients,
            hotpatching_enabled,
            ..
            with_client
        } => {
            if hotpatching_enabled {
                warn!("-+-+-+-+-+-+- Starting with hotpatching enabled -+-+-+-+-+-+-");


@@ 69,16 123,45 @@ fn run(cli: Cli) -> AppExit {
                exit(1);
            }

            app.add_plugins(ServerPluginGroup {

            if with_client {
                app.add_plugins(
                    DefaultPlugins.build()
                        .disable::<LogPlugin>()
                        .disable::<UiPlugin>()
                );
                app.add_plugins(|app: &mut App| {
                    app.add_systems(Startup, crate::server::player::join::ls_magically_invent_player);
                });
            } else {
               app
                   .add_plugins(AssetPlugin::default())
                   .add_plugins(StatesPlugin)
                    .add_plugins(TaskPoolPlugin::default())
                    .add_plugins(FrameCountPlugin)
                    .add_plugins(TimePlugin)
                    .add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32(
                        1.0 / tick_rate,
                    )));
            }

            app.add_plugins(SharedPluginGroup);

            let mut pg = ServerPluginGroup {
                bind,
                tick_rate,
                max_clients,
            });
            app.add_plugins(SharedPluginGroup);
            }.build();
            if with_client {
                pg = pg.add_group(ClientPluginGroup {
                    server: None
                });
            }
            app.add_plugins(pg);
        }
        #[cfg(all(not(target_arch = "wasm32"), feature = "particle_editor"))]
        Cli::ParticleEditor {} => {
            app.add_plugins(starkingdoms::particle_editor::particle_editor_plugin);
            app.add_plugins(crate::particle_editor::particle_editor_plugin);
        }
    }



@@ 104,6 187,8 @@ fn main() -> AppExit {
        Cli::Client { .. } => { run(cli) },
        Cli::ParticleEditor { .. } => { run(cli) },
        Cli::Server { with_client, bind, hotpatching_enabled, .. } => {
            run(cli)
            /*
            if !with_client {
                run(cli)
            } else {


@@ 162,8 247,7 @@ fn main() -> AppExit {
                        AppExit::Error(c) => AppExit::Error(c),
                        _ => AppExit::Success
                    }
                }
            }
                }*/
        }
    }
}

M crates/unified/src/server/player.rs => crates/unified/src/server/player.rs +177 -64
@@ 1,3 1,5 @@
pub mod join;

use crate::attachment::{Joint, JointOf, Joints, PartInShip, Peer, SnapOf, SnapOfJoint};
use crate::config::planet::Planet;
use crate::ecs::{DragRequestEvent, Part, Player, PlayerStorage, PlayerThrust, ThrustEvent};


@@ 8,20 10,168 @@ use crate::server::{ConnectedGameEntity, ConnectedNetworkEntity};
use crate::prelude::*;
use bevy_replicon::prelude::{ClientId, FromClient};
use std::f32::consts::PI;
use crate::config::world::GlobalWorldConfig;

pub fn player_management_plugin(app: &mut App) {
    app.add_systems(
        Update,
        (
            handle_new_players,
            join::handle_pending_players,
            join::handle_new_players,
            player_thrust,
            magic_fuel_regen,
            dragging,
        )
            .in_set(PlayerInputSet),
    );
    app.add_systems(Update, complete_partial_disconnects);
}

#[derive(Component)]
struct PartiallyDisconnected;

/// Partial Disconnects are created when a part is disconnected to indicate that a disconnect has started.
/// Disconnects cannot be completed until the next tick, after relevant Peer relationships have been removed.
/// This system does this by performing a flood search to determine if the 'partial-disconnected' parts still have
/// a valid path to hearty; if they do not, they are removed from the ship.
fn complete_partial_disconnects(
    partial_disconnected_parts: Query<Entity, With<PartiallyDisconnected>>,
    mut q_joints: Query<&Joints>,
    mut q_maybe_peer: Query<Option<&Peer>>,
    mut q_joint_of_part: Query<&JointOf>,
    mut q_is_hearty: Query<Option<&Player>>,
    mut dc_q_joints: Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>,
    mut dc_q_only_joints: Query<&Joints>,

    mut commands: Commands,
) {
    for partially_disconnected_part in &partial_disconnected_parts {

        trace!(?partially_disconnected_part, "completing partial disconnect from previous tick");
        commands.entity(partially_disconnected_part).remove::<PartiallyDisconnected>();

        // is it still connected to hearty?
        let mut search_visited_joints = vec![];
        let can_reach_hearty = can_reach_hearty(
            partially_disconnected_part,
            q_joints.reborrow(),
            q_maybe_peer.reborrow(),
            q_joint_of_part.reborrow(),
            q_is_hearty.reborrow(),
            &mut search_visited_joints,
            true
        );
        if can_reach_hearty {
            // great, leave them alone
            continue;
        }
        // this part cannot reach hearty
        // trigger a disconnect on them to propagate the disconnection

        let mut disconnect_queue = vec![];
        disconnect_part(
            (partially_disconnected_part, match q_joints.get(partially_disconnected_part) {
                Ok(j) => j,
                Err(e) => {
                    warn!(?partially_disconnected_part, "part does not have a Joints? this should be impossible...");
                    continue;
                }
            }),
            &dc_q_joints,
            &dc_q_only_joints,
            &mut disconnect_queue,
            commands.reborrow()
        );
        commands.entity(partially_disconnected_part).remove::<PartiallyDisconnected>();
    }
}

/// Determine if a part has a path to hearty by performing a depth-first search
/// TODO: This can be very slow on large ships- the path propagation model will be significantly faster
/// TODO: Ask core for an explanation of what the path propagation model is if you want to implement this
fn can_reach_hearty(
    part: Entity,

    mut q_joints: Query<&Joints>,
    mut q_maybe_peer: Query<Option<&Peer>>,
    mut q_joint_of_part: Query<&JointOf>,
    mut q_is_hearty: Query<Option<&Player>>,

    mut visited_joints: &mut Vec<Entity>,

    is_top_of_recursion: bool
) -> bool {
    // Get the joints of this entity
    let Ok(our_joints) = q_joints.get(part).cloned() else {
        warn!("part does not have a Joints? this should be impossible...");
        return false;
    };

    // Iterate over each joint:
    // if it's Hearty: great! we're connected
    // if it's another part: recurse to it
    // if it's not connected: lame, move on
    'to_next_joint: for joint in &**our_joints {
        // mark that we've visited this joint, so we don't come back to it later
        visited_joints.push(*joint);

        // does this joint have a peer?
        let maybe_peer = q_maybe_peer.get(*joint).expect("cannot fail");

        if let Some(peer_info) = maybe_peer {
            // we have a peer! figure out what it's connected to...
            let other_parts_joint = peer_info.peer_joint_entity_id;
            // have we visited this joint already?
            if visited_joints.contains(&other_parts_joint) {
                // if so, move on
                continue 'to_next_joint;
            }
            // we have not, find it's parent part
            let other_part = q_joint_of_part.get(other_parts_joint).expect("joint is missing JointOf").0;
            // is this part Hearty?
            let maybe_is_hearty = q_is_hearty.get(other_part).expect("cannot fail");
            if maybe_is_hearty.is_some() {
                // yay! found hearty
                debug!("partial detach DFS: visited {} joints => found hearty @ {:?}", visited_joints.len(), other_part);
                debug!("-> via {:?}", part);
                return true;
            } else {
                // not hearty. but can the other part reach hearty?
                let can_other_part_reach = can_reach_hearty(
                    other_part,
                    q_joints.reborrow(),
                    q_maybe_peer.reborrow(),
                    q_joint_of_part.reborrow(),
                    q_is_hearty.reborrow(),
                    visited_joints,
                    false
                );
                if can_other_part_reach {
                    // great, they are connected
                    // log that we're in the path, then bubble up
                    debug!("-> via {:?}", part);
                    return true;
                } else {
                    // lame. continue to next part
                    continue 'to_next_joint;
                }
            }
        } else {
            // we do not have a peer. move on
            continue 'to_next_joint;
        }
    }

    // Exhausted all options; we are not connected to hearty bubble up
    if is_top_of_recursion {
        // print a debug message
        debug!("partial detach DFS: visited {} joints => not connected", visited_joints.len());
    }
    false
}



fn disconnect_part(
    (entity, joints): (Entity, &Joints),
    q_joints: &Query<(&Joint, &JointOf, &Transform, Option<&Peer>, Entity)>,


@@ 54,18 204,21 @@ fn disconnect_part(
        };
        let Some(other_peer) = other_peer else { continue; };
        commands.entity(peer.peer_joint_entity_id).remove::<Peer>();
        processed_peers.push(peer.peer_joint_entity_id);

        let Ok(other_joints) = q_only_joints.get(other_joint_of.0) else {
            continue
        };
        commands.entity(other_joint_of.0).remove::<PartInShip>();

        if other_joint_of.0 != entity {
            disconnect_part((peer.peer_joint_entity_id, joints), q_joints,
        if !processed_peers.contains(&peer.peer_joint_entity_id) {
            disconnect_part((other_joint_of.0, joints), q_joints,
                q_only_joints, processed_peers, commands.reborrow());
        }

        processed_peers.push(peer.peer_joint_entity_id);
    }
    // recursive disconnect part
    commands.entity(entity).insert(PartiallyDisconnected);
    commands.entity(entity).remove::<PartInShip>();
}



@@ 89,6 242,7 @@ fn dragging(
    q_joints: Query<&Joints>,
    q_joint: Query<&FixedJoint>,
    clients: Query<&ConnectedNetworkEntity>,
    q_ls_me: Query<Entity, With<crate::client::Me>>,
    mut commands: Commands,
) {
    for FromClient {


@@ 96,13 250,15 @@ fn dragging(
        message: event,
    } in events.read()
    {
        let client_entity = match client_id {
            ClientId::Client(e) => e,
            _ => continue,
        let player_hearty_entity = match client_id {
            ClientId::Client(client_entity) => {
                let ConnectedNetworkEntity {
                    game_entity: player_hearty_entity,
                } = clients.get(*client_entity).unwrap();
                player_hearty_entity
            },
            ClientId::Server => &q_ls_me.iter().next().unwrap()
        };
        let ConnectedNetworkEntity {
            game_entity: player_hearty_entity,
        } = clients.get(*client_entity).unwrap();

        debug!(?event, "got drag request event");



@@ 272,55 428,7 @@ fn dragging(
    }
}

fn handle_new_players(
    mut commands: Commands,
    q_new_clients: Query<Entity, Added<ConnectedGameEntity>>,
    world_config: Res<WorldConfigResource>,
    planets: Query<(&Transform, &Planet)>,
    asset_server: Res<AssetServer>,
) {
    let Some(wc) = &world_config.config else {
        return;
    };
    for joined_player in &q_new_clients {
        trace!(?joined_player, "detected joined player!");
        // find earth
        let (spawn_planet_pos, spawn_planet) = planets
            .iter()
            .find(|p| p.1.name == wc.hearty.spawn_at)
            .unwrap_or_else(|| {
                panic!(
                    "spawn planet {} is missing? (check that the planet is named exactly '{}')",
                    wc.hearty.spawn_at, wc.hearty.spawn_at
                )
            });
        let angle = rand::random::<f32>() * std::f32::consts::TAU;
        let offset = spawn_planet.radius + 150.0;
        let mut new_transform =
            Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0);
        new_transform.rotate_z(angle);
        new_transform.translation += spawn_planet_pos.translation;

        info!(?new_transform, ?joined_player, "set player's position!");

        commands
            .entity(joined_player)
            .insert(new_transform)
            .insert(SpawnPartRequest(
                asset_server.load("config/parts/hearty.part.toml"),
            ))
            .insert(PlayerThrust::default())
            .insert(PlayerStorage {
                fuel_capacity: 25.0,
                fuel: 25.0,
                power_capacity: 25.0,
                power: 25.0,
            })
            .insert(Player {
                client: joined_player,
            });
    }
}


fn magic_fuel_regen(players: Query<&mut PlayerStorage, With<Player>>, time: Res<Time>) {
    for mut storage in players {


@@ 338,6 446,7 @@ fn player_thrust(
    )>,
    clients: Query<&ConnectedNetworkEntity>,
    mut thrust_event: MessageReader<FromClient<ThrustEvent>>,
    q_ls_me: Query<Entity, With<crate::client::Me>>,
    world_config: Res<WorldConfigResource>,
) {
    for FromClient {


@@ 345,13 454,17 @@ fn player_thrust(
        message: event,
    } in thrust_event.read()
    {
        let client_entity = match client_id {
            ClientId::Client(e) => e,
            _ => continue,
        let player_hearty_entity = match client_id {
            ClientId::Client(client_entity) => {
                let ConnectedNetworkEntity {
                    game_entity: player_hearty_entity,
                } = clients.get(*client_entity).unwrap();
                player_hearty_entity
            },
            ClientId::Server => &q_ls_me.iter().next().unwrap()
        };
        let ConnectedNetworkEntity { game_entity } = clients.get(*client_entity).unwrap();

        let Ok((_, _, _, mut thrust, _)) = players.get_mut(*game_entity) else {
        let Ok((_, _, _, mut thrust, _)) = players.get_mut(*player_hearty_entity) else {
            continue;
        };


A crates/unified/src/server/player/join.rs => crates/unified/src/server/player/join.rs +110 -0
@@ 0,0 1,110 @@
use crate::config::planet::Planet;
use crate::config::world::GlobalWorldConfig;
use crate::ecs::{Player, PlayerStorage, PlayerThrust};
use crate::prelude::*;
use crate::server::ConnectedGameEntity;
use crate::server::part::SpawnPartRequest;
use crate::server::world_config::WorldConfigResource;

fn join_player(joined_player: Entity, mut commands: Commands, wc: &GlobalWorldConfig, planets: Query<(&Transform, &Planet)>, asset_server: &AssetServer) {
    trace!(?joined_player, "detected joined player!");
    // find earth
    if planets.is_empty() {
        warn!("planets have not loaded yet, setting this player to pending until they do!");
        commands.entity(joined_player).insert(PendingPlayer);
        return;
    }
    let (spawn_planet_pos, spawn_planet) = planets
        .iter()
        .find(|p| p.1.name == wc.hearty.spawn_at)
        .unwrap_or_else(|| {
            panic!(
                "spawn planet {} is missing? (check that the planet is named exactly '{}')",
                wc.hearty.spawn_at, wc.hearty.spawn_at
            )
        });
    let angle = rand::random::<f32>() * std::f32::consts::TAU;
    let offset = spawn_planet.radius + 150.0;
    let mut new_transform =
        Transform::from_xyz(angle.cos() * offset, angle.sin() * offset, 0.0);
    new_transform.rotate_z(angle);
    new_transform.translation += spawn_planet_pos.translation;

    info!(?new_transform, ?joined_player, "set player's position!");

    commands
        .entity(joined_player)
        .insert(new_transform)
        .insert(SpawnPartRequest(
            asset_server.load("config/parts/hearty.part.toml"),
        ))
        .insert(PlayerThrust::default())
        .insert(PlayerStorage {
            fuel_capacity: 25.0,
            fuel: 25.0,
            power_capacity: 25.0,
            power: 25.0,
        })
        .insert(Player {
            client: joined_player,
        })
        .remove::<PendingPlayer>();
}

#[derive(Component)]
pub struct PendingPlayer;

pub fn handle_new_players(
    mut commands: Commands,
    q_new_clients: Query<Entity, Added<ConnectedGameEntity>>,
    world_config: Res<WorldConfigResource>,
    planets: Query<(&Transform, &Planet)>,
    asset_server: Res<AssetServer>,
) {
    let Some(wc) = &world_config.config else {
        warn!("got a joined player, but world config is not loaded! waiting until it is...");
        for joined_player in &q_new_clients {
            commands.entity(joined_player).insert(PendingPlayer);
        }
        return;
    };
    for joined_player in &q_new_clients {
        join_player(joined_player, commands.reborrow(), wc, planets, &asset_server);
    }
}
pub fn handle_pending_players(
    mut commands: Commands,
    pending_players: Query<Entity, With<PendingPlayer>>,
    world_config: Res<WorldConfigResource>,
    planets: Query<(&Transform, &Planet)>,
    asset_server: Res<AssetServer>,
) {
    let Some(wc) = &world_config.config else {
        warn!("there are pending players, but world config is not loaded! waiting until it is...");
        return;
    };

    for pending_player in &pending_players {
        warn!(?pending_player, "reprocessing pending player");
        join_player(pending_player, commands.reborrow(), wc, planets, &asset_server);
    }
}

pub fn ls_magically_invent_player(
    mut commands: Commands,
    world_config: Res<WorldConfigResource>,
    planets: Query<(&Transform, &Planet)>,
    asset_server: Res<AssetServer>,
) {
    // Magically invent a player for listenserver
    let fake_network_entity = commands.spawn(Replicated).id();
    let local_player = commands.spawn((
        ConnectedGameEntity {
            network_entity: fake_network_entity,
        },
        Replicated,
        crate::client::Me
    )).id();
    debug!(?fake_network_entity, ?local_player, "listenserver: magically invented a player");
    //join_player(local_player, commands.reborrow(), wc, planets, &asset_server);
}
\ No newline at end of file

M crates/unified/src/server_plugins.rs => crates/unified/src/server_plugins.rs +1 -13
@@ 28,24 28,12 @@ impl PluginGroup for ServerPluginGroup {
                    .with_length_unit(100.0)
                    .set(PhysicsInterpolationPlugin::interpolate_all())
            )
            .add(StatesPlugin)
            .add(TaskPoolPlugin::default())
            .add(FrameCountPlugin)
            .add(TimePlugin)
            .add(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f32(
                1.0 / self.tick_rate,
            )))
            .add_group(RepliconPlugins)
            .add(WebSocketServerPlugin)
            .add(AeronetRepliconServerPlugin)
            /* Assets */
            .add(AssetPlugin::default())
            .add(TomlAssetPlugin::<GlobalWorldConfig>::new(&["wc.toml"]))
            .add(TomlAssetPlugin::<PlanetConfigCollection>::new(&["pc.toml"]))
            .add(TomlAssetPlugin::<PartConfig>::new(&["part.toml"]))
            .add(crate::server::ServerPlugin {
                bind: self.bind,
                max_clients: self.max_clients,
            })

    }
}

M crates/unified/src/shared_plugins.rs => crates/unified/src/shared_plugins.rs +11 -1
@@ 1,11 1,15 @@
use avian2d::PhysicsPlugins;
use crate::attachment::{Joint, JointOf, PartInShip, Peer, Ship, SnapOf, SnapOfJoint};
use crate::config::planet::Planet;
use crate::config::planet::{Planet, PlanetConfigCollection};
use crate::ecs::{DragRequestEvent, Part, Particles, Player, PlayerStorage, ThrustEvent};
use bevy::app::{App, PluginGroup, PluginGroupBuilder};
use bevy::ui::UiPlugin;
use bevy_common_assets::toml::TomlAssetPlugin;
use crate::prelude::*;
//use bevy_rapier2d::prelude::*;
use bevy_replicon::prelude::{AppRuleExt, Channel, ClientMessageAppExt};
use crate::config::part::PartConfig;
use crate::config::world::GlobalWorldConfig;
use crate::physics::register_physics_components_for_replication;

pub struct SharedPluginGroup;


@@ 13,10 17,16 @@ pub struct SharedPluginGroup;
impl PluginGroup for SharedPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add_group(RepliconPlugins)
            //.add(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
            .add(physics_setup_plugin)
            .add(register_everything)
            .add(register_physics_components_for_replication)

            /* Assets */
            .add(TomlAssetPlugin::<GlobalWorldConfig>::new(&["wc.toml"]))
            .add(TomlAssetPlugin::<PlanetConfigCollection>::new(&["pc.toml"]))
            .add(TomlAssetPlugin::<PartConfig>::new(&["part.toml"]))
    }
}


A rust-toolchain.toml => rust-toolchain.toml +2 -0
@@ 0,0 1,2 @@
[toolchain]
channel = "nightly"
\ No newline at end of file