Replicate Sounds (#68)
This commit is contained in:
92
crates/server/src/backpack/backpack_ui.rs
Normal file
92
crates/server/src/backpack/backpack_ui.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use shared::{
|
||||
GameState,
|
||||
backpack::{
|
||||
BackbackSwapEvent, Backpack, UiHeadState,
|
||||
backpack_ui::{BackpackUiState, HEAD_SLOTS},
|
||||
},
|
||||
control::ControlState,
|
||||
protocol::PlaySound,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
sync_on_change.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
app.add_systems(FixedUpdate, swap_head_inputs);
|
||||
}
|
||||
|
||||
fn swap_head_inputs(
|
||||
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
|
||||
mut commands: Commands,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (controls, backpack) in player.iter() {
|
||||
if state.count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if controls.backpack_toggle {
|
||||
state.open = !state.open;
|
||||
commands.trigger(PlaySound::Backpack { open: state.open });
|
||||
}
|
||||
|
||||
if !state.open {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
if controls.backpack_left && state.current_slot > 0 {
|
||||
state.current_slot -= 1;
|
||||
changed = true;
|
||||
}
|
||||
if controls.backpack_right && state.current_slot < state.count.saturating_sub(1) {
|
||||
state.current_slot += 1;
|
||||
changed = true;
|
||||
}
|
||||
if controls.backpack_swap {
|
||||
commands.trigger(BackbackSwapEvent(state.current_slot));
|
||||
}
|
||||
|
||||
if changed {
|
||||
commands.trigger(PlaySound::Selection);
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_on_change(
|
||||
backpack: Query<Ref<Backpack>>,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for backpack in backpack.iter() {
|
||||
if backpack.is_changed() || backpack.reloading() {
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sync(backpack: &Backpack, state: &mut Single<&mut BackpackUiState>, time: f32) {
|
||||
state.count = backpack.heads.len();
|
||||
|
||||
state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS));
|
||||
|
||||
if state.current_slot >= state.scroll + HEAD_SLOTS {
|
||||
state.scroll = state.current_slot.saturating_sub(HEAD_SLOTS - 1);
|
||||
}
|
||||
if state.current_slot < state.scroll {
|
||||
state.scroll = state.current_slot;
|
||||
}
|
||||
|
||||
for i in 0..HEAD_SLOTS {
|
||||
if let Some(head) = backpack.heads.get(i + state.scroll) {
|
||||
state.heads[i] = Some(UiHeadState::new(*head, time));
|
||||
} else {
|
||||
state.heads[i] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/server/src/backpack/mod.rs
Normal file
7
crates/server/src/backpack/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub mod backpack_ui;
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(backpack_ui::plugin);
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
app::plugin_group,
|
||||
audio::Volume,
|
||||
core_pipeline::tonemapping::Tonemapping,
|
||||
log::{BoxedLayer, tracing_subscriber::Layer},
|
||||
prelude::*,
|
||||
};
|
||||
use bevy::{app::plugin_group, core_pipeline::tonemapping::Tonemapping, prelude::*};
|
||||
use bevy_common_assets::ron::RonAssetPlugin;
|
||||
use bevy_sprite3d::Sprite3dPlugin;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
@@ -14,10 +8,12 @@ use lightyear::prelude::server::ServerPlugins;
|
||||
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||
use std::time::Duration;
|
||||
|
||||
mod backpack;
|
||||
mod config;
|
||||
mod player;
|
||||
mod server;
|
||||
mod tb_entities;
|
||||
mod utils;
|
||||
|
||||
plugin_group! {
|
||||
pub struct DefaultPlugins {
|
||||
@@ -44,7 +40,6 @@ plugin_group! {
|
||||
bevy::ui:::UiPlugin,
|
||||
bevy::pbr:::PbrPlugin,
|
||||
bevy::gltf:::GltfPlugin,
|
||||
bevy::audio:::AudioPlugin,
|
||||
bevy::gilrs:::GilrsPlugin,
|
||||
bevy::animation:::AnimationPlugin,
|
||||
bevy::gizmos:::GizmoPlugin,
|
||||
@@ -54,23 +49,6 @@ plugin_group! {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_to_file_layer(_app: &mut App) -> Option<BoxedLayer> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open("server.log")
|
||||
.ok()?;
|
||||
Some(
|
||||
bevy::log::tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::sync::Mutex::new(file))
|
||||
.with_ansi(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
|
||||
@@ -88,7 +66,7 @@ fn main() {
|
||||
filter: "info,lightyear_replication=off".into(),
|
||||
level: bevy::log::Level::INFO,
|
||||
// provide custom log layer to receive logging events
|
||||
custom_layer: log_to_file_layer,
|
||||
..default()
|
||||
}));
|
||||
|
||||
app.add_plugins(ServerPlugins {
|
||||
@@ -102,17 +80,6 @@ fn main() {
|
||||
app.add_plugins(UiGradientsPlugin);
|
||||
app.add_plugins(RonAssetPlugin::<HeadDatabaseAsset>::new(&["headsdb.ron"]));
|
||||
|
||||
#[cfg(feature = "dbg")]
|
||||
{
|
||||
app.add_plugins(PhysicsDebugPlugin::default());
|
||||
|
||||
// app.add_plugins(bevy::pbr::wireframe::WireframePlugin)
|
||||
// .insert_resource(bevy::pbr::wireframe::WireframeConfig {
|
||||
// global: true,
|
||||
// default_color: bevy::color::palettes::css::WHITE.into(),
|
||||
// });
|
||||
}
|
||||
|
||||
app.add_plugins(shared::abilities::plugin);
|
||||
app.add_plugins(shared::ai::plugin);
|
||||
app.add_plugins(shared::aim::plugin);
|
||||
@@ -127,7 +94,6 @@ fn main() {
|
||||
app.add_plugins(shared::gates::plugin);
|
||||
app.add_plugins(shared::head_drop::plugin);
|
||||
app.add_plugins(shared::heads::plugin);
|
||||
app.add_plugins(shared::heal_effect::plugin);
|
||||
app.add_plugins(shared::hitpoints::plugin);
|
||||
app.add_plugins(shared::keys::plugin);
|
||||
app.add_plugins(shared::loading_assets::LoadingPlugin);
|
||||
@@ -137,7 +103,6 @@ fn main() {
|
||||
app.add_plugins(shared::platforms::plugin);
|
||||
app.add_plugins(shared::player::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
app.add_plugins(shared::sounds::plugin);
|
||||
app.add_plugins(shared::steam::plugin);
|
||||
app.add_plugins(shared::tb_entities::plugin);
|
||||
app.add_plugins(shared::utils::auto_rotate::plugin);
|
||||
@@ -149,20 +114,20 @@ fn main() {
|
||||
app.add_plugins(shared::utils::plugin);
|
||||
app.add_plugins(shared::water::plugin);
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
app.add_plugins(config::plugin);
|
||||
app.add_plugins(player::plugin);
|
||||
app.add_plugins(server::plugin);
|
||||
app.add_plugins(tb_entities::plugin);
|
||||
app.add_plugins(utils::plugin);
|
||||
|
||||
app.init_state::<GameState>();
|
||||
|
||||
app.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 400.,
|
||||
..Default::default()
|
||||
});
|
||||
app.insert_resource(ClearColor(Color::BLACK));
|
||||
//TODO: let user control this
|
||||
app.insert_resource(GlobalVolume::new(Volume::Linear(0.4)));
|
||||
app.add_systems(PostStartup, setup_panic_handler);
|
||||
|
||||
app.run();
|
||||
}
|
||||
|
||||
fn setup_panic_handler() {
|
||||
_ = std::panic::take_hook();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use shared::{
|
||||
cash::CashResource,
|
||||
character::AnimatedCharacter,
|
||||
control::{ControlState, controller_common::PlayerCharacterController},
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
head_drop::HeadDrops,
|
||||
heads::{ActiveHeads, HeadChanged, HeadState, heads_ui::UiActiveHeads},
|
||||
@@ -13,19 +14,23 @@ use shared::{
|
||||
hitpoints::{Hitpoints, Kill},
|
||||
npc::SpawnCharacter,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
protocol::{
|
||||
PlaySound, PlayerId, channels::UnorderedReliableChannel, events::ClientHeadChanged,
|
||||
},
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_update_head_mesh);
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
mut commands: Commands,
|
||||
owner: Entity,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
asset_server: Res<AssetServer>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) {
|
||||
let Some(spawn) = query.iter().next() else {
|
||||
return;
|
||||
};
|
||||
) -> Option<Entity> {
|
||||
let spawn = query.iter().next()?;
|
||||
|
||||
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
||||
|
||||
@@ -47,6 +52,7 @@ pub fn spawn(
|
||||
transform,
|
||||
Visibility::default(),
|
||||
PlayerCharacterController,
|
||||
PlayerId { id: 0 },
|
||||
),
|
||||
ActionState::<ControlState>::default(),
|
||||
Backpack::default(),
|
||||
@@ -67,12 +73,12 @@ pub fn spawn(
|
||||
));
|
||||
player.observe(on_kill);
|
||||
|
||||
commands.spawn((
|
||||
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
|
||||
PlaybackSettings::DESPAWN,
|
||||
));
|
||||
let id = player.id();
|
||||
|
||||
commands.trigger(PlaySound::Head("angry demonstrator".to_string()));
|
||||
commands.trigger(SpawnCharacter(transform.translation));
|
||||
|
||||
Some(id)
|
||||
}
|
||||
|
||||
fn on_kill(
|
||||
@@ -92,3 +98,27 @@ fn on_kill(
|
||||
commands.trigger(HeadChanged(new_head));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_update_head_mesh(
|
||||
trigger: Trigger<HeadChanged>,
|
||||
mut commands: Commands,
|
||||
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
|
||||
mut sender: Single<&mut TriggerSender<ClientHeadChanged>>,
|
||||
animated_characters: Query<&AnimatedCharacter>,
|
||||
mut player: Single<&mut ActiveHead, With<Player>>,
|
||||
) -> Result {
|
||||
let animated_char = mesh_children
|
||||
.iter()
|
||||
.find(|child| animated_characters.contains(*child))
|
||||
.ok_or("tried to update head mesh before AnimatedCharacter was readded")?;
|
||||
|
||||
player.0 = trigger.0;
|
||||
|
||||
commands
|
||||
.entity(animated_char)
|
||||
.insert(AnimatedCharacter::new(trigger.0));
|
||||
|
||||
sender.trigger::<UnorderedReliableChannel>(ClientHeadChanged(trigger.0 as u64));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,42 +1,54 @@
|
||||
use crate::config::ServerConfig;
|
||||
use bevy::prelude::*;
|
||||
use lightyear::{
|
||||
connection::client::PeerMetadata,
|
||||
link::LinkConditioner,
|
||||
prelude::{
|
||||
server::{NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
|
||||
server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
|
||||
*,
|
||||
},
|
||||
};
|
||||
use shared::{GameState, global_observer, heads_database::HeadsDatabase, tb_entities::SpawnPoint};
|
||||
use shared::{
|
||||
GameState, global_observer,
|
||||
heads_database::HeadsDatabase,
|
||||
protocol::{
|
||||
ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
|
||||
},
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(Startup, (start_server, setup_timeout_timer));
|
||||
app.add_systems(
|
||||
OnEnter(GameState::Playing),
|
||||
(start_server, setup_timeout_timer),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
notify_started.run_if(in_state(GameState::Playing)),
|
||||
run_timeout,
|
||||
),
|
||||
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
global_observer!(app, handle_new_client);
|
||||
global_observer!(app, on_client_connected);
|
||||
global_observer!(app, on_client_playing);
|
||||
global_observer!(app, close_on_disconnect);
|
||||
global_observer!(app, cancel_timeout);
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ClientPlayerId(u8);
|
||||
|
||||
fn handle_new_client(
|
||||
trigger: Trigger<OnAdd, Connected>,
|
||||
trigger: Trigger<OnAdd, Linked>,
|
||||
mut commands: Commands,
|
||||
id: Query<&PeerAddr>,
|
||||
asset_server: Res<AssetServer>,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) -> Result {
|
||||
let id = id.get(trigger.target())?;
|
||||
let Ok(id) = id.get(trigger.target()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Client connected on IP: {}", id.ip());
|
||||
|
||||
@@ -46,11 +58,38 @@ fn handle_new_client(
|
||||
incoming_loss: 0.0,
|
||||
});
|
||||
|
||||
commands
|
||||
.entity(trigger.target())
|
||||
.insert((ReplicationSender::default(), Link::new(Some(conditioner))));
|
||||
commands.entity(trigger.target()).insert((
|
||||
ReplicationSender::default(),
|
||||
Link::new(Some(conditioner)),
|
||||
ClientPlayerId(0),
|
||||
));
|
||||
|
||||
crate::player::spawn(commands, trigger.target(), query, asset_server, heads_db);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_client_connected(
|
||||
trigger: Trigger<OnAdd, ClientOf>,
|
||||
mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
|
||||
) -> Result {
|
||||
// `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
|
||||
// send the client player id until now.
|
||||
let (id, mut sender) = assign_player.get_mut(trigger.target())?;
|
||||
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_client_playing(
|
||||
trigger: Trigger<RemoteTrigger<ClientEnteredPlaying>>,
|
||||
commands: Commands,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
peers: Res<PeerMetadata>,
|
||||
) -> Result {
|
||||
let Some(&client) = peers.mapping.get(&trigger.from) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
crate::player::spawn(commands, client, query, heads_db).ok_or("failed to spawn player")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,6 +100,7 @@ fn close_on_disconnect(
|
||||
mut writer: EventWriter<AppExit>,
|
||||
) {
|
||||
if config.close_on_client_disconnect {
|
||||
info!("client disconnected, exiting");
|
||||
writer.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
@@ -103,10 +143,12 @@ fn run_timeout(mut timer: ResMut<TimeoutTimer>, mut writer: EventWriter<AppExit>
|
||||
timer.0 -= time.delta_secs();
|
||||
|
||||
if timer.0 <= 0.0 {
|
||||
info!("client timed out, exiting");
|
||||
writer.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_timeout(_trigger: Trigger<OnAdd, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
||||
info!("client connected, cancelling timeout");
|
||||
timer.0 = f32::INFINITY;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::{ActionsChannel, Connected, MessageSender};
|
||||
use lightyear::prelude::{Connected, MessageSender};
|
||||
use shared::{
|
||||
GameState, global_observer,
|
||||
protocol::{DespawnTbMapEntity, TbMapEntityId},
|
||||
protocol::{TbMapEntityId, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -38,6 +38,6 @@ fn send_new_client_despawned_cache(
|
||||
) {
|
||||
let mut send = send.get_mut(trigger.target()).unwrap();
|
||||
for &id in cache.0.iter() {
|
||||
send.send::<ActionsChannel>(DespawnTbMapEntity(id));
|
||||
send.send::<UnorderedReliableChannel>(DespawnTbMapEntity(id));
|
||||
}
|
||||
}
|
||||
|
||||
37
crates/server/src/utils.rs
Normal file
37
crates/server/src/utils.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use bevy::{
|
||||
ecs::{archetype::Archetypes, component::Components, entity::Entities},
|
||||
prelude::*,
|
||||
};
|
||||
use shared::global_observer;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, report_entity_components);
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct ReportEntityComponents(pub Entity);
|
||||
|
||||
fn report_entity_components(
|
||||
trigger: Trigger<ReportEntityComponents>,
|
||||
entities: &Entities,
|
||||
components: &Components,
|
||||
archetypes: &Archetypes,
|
||||
) {
|
||||
let Some(location) = entities.get(trigger.event().0) else {
|
||||
warn!("failed to report entity components; had no location");
|
||||
return;
|
||||
};
|
||||
let Some(archetype) = archetypes.get(location.archetype_id) else {
|
||||
warn!("failed to report entity components; had no archetype");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut output = format!("Entity {:?} Components: ", trigger.event().0);
|
||||
for component in archetype.components() {
|
||||
if let Some(name) = components.get_name(component) {
|
||||
output.push_str(&format!("{name}, "));
|
||||
}
|
||||
}
|
||||
|
||||
info!("{}; Caller: {}", output, trigger.caller());
|
||||
}
|
||||
Reference in New Issue
Block a user