This commit is contained in:
Matthieu Jolimaitre 2024-11-15 04:57:59 +01:00
commit f978b8af17
21 changed files with 4015 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

3314
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
Cargo.toml Normal file
View file

@ -0,0 +1,25 @@
[package]
name = "d5"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy = { version = "0.14.2", default-features = false, features = [
"multi_threaded",
] }
bevy_eventwork = "0.9.0"
itermore = { version = "0.7.1", features = ["array_chunks"] }
rand = "0.8.5"
serde = { version = "1.0.215", features = ["derive"] }
sysinfo = { version = "0.32.0", default-features = false, features = [
"system",
] }
termion = "4.0.3"
[[bin]]
name = "server"
path = "src/server.rs"
[[bin]]
name = "client"
path = "src/client.rs"

2
rustfmt.toml Normal file
View file

@ -0,0 +1,2 @@
hard_tabs = true
max_width = 120

19
src/client.rs Normal file
View file

@ -0,0 +1,19 @@
#![allow(dead_code)]
use std::time::Duration;
use bevy::{app::ScheduleRunnerPlugin, prelude::*};
use lib_client::{display::DisplayPlugin, input::InputPlugin, net::NetPlugin};
mod common;
mod lib_client;
fn main() {
App::new()
// Core.
.add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_millis(10)))
.add_plugins(NetPlugin)
.add_plugins(DisplayPlugin)
.add_plugins(InputPlugin)
.run();
}

31
src/common.rs Normal file
View file

@ -0,0 +1,31 @@
use bevy::math::IVec2;
use bevy_eventwork::NetworkMessage;
use serde::{Deserialize, Serialize};
pub mod server_msg {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct MapUpdates(pub Vec<(IVec2, (char, char))>);
netify!(MapUpdates);
}
pub mod client_msg {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
pub struct Move(pub IVec2);
netify!(Move);
}
macro_rules! netify {
($name:ident) => {
impl NetworkMessage for $name {
const NAME: &'static str = stringify!($name);
}
};
}
pub(crate) use netify;
pub const RENDER_DISTANCE: i32 = 12;

48
src/lib_client/display.rs Normal file
View file

@ -0,0 +1,48 @@
use bevy::prelude::*;
use bevy_eventwork::{tcp::TcpProvider, AppNetworkMessage, NetworkData};
use termion::{clear, cursor::Goto};
use crate::common::{self, server_msg::MapUpdates};
pub struct DisplayPlugin;
impl Plugin for DisplayPlugin {
fn build(&self, app: &mut App) {
app.listen_for_message::<MapUpdates, TcpProvider>()
.add_systems(Update, handle_map_updates);
}
}
struct Display;
impl Display {
fn draw(&self, pos: IVec2, (l, r): (char, char)) {
let pos = pos + IVec2::new(RENDER_DISTANCE, RENDER_DISTANCE);
let x = (pos.x * 2) + 1;
let y = ((2 * RENDER_DISTANCE) - pos.y) + 1;
print!("{}{}{}", Goto(x as _, y as _), l, r);
}
}
const RENDER_DISTANCE: i32 = common::RENDER_DISTANCE;
const RENDER_DISTANCE_SQ: i32 = RENDER_DISTANCE * RENDER_DISTANCE;
fn handle_map_updates(mut updates: EventReader<NetworkData<MapUpdates>>) {
for update in updates.read() {
print!("{}", clear::All);
for x in -RENDER_DISTANCE..RENDER_DISTANCE {
for y in -RENDER_DISTANCE..RENDER_DISTANCE {
let pos = IVec2::new(x, y);
if pos.distance_squared(IVec2::ZERO) <= RENDER_DISTANCE_SQ {
Display.draw(pos, ('.', ' '));
}
}
}
for (pos, chars) in &update.0 {
Display.draw(*pos, *chars);
}
println!();
}
}

77
src/lib_client/input.rs Normal file
View file

@ -0,0 +1,77 @@
use bevy_eventwork::{tcp::TcpProvider, ConnectionId, Network};
use std::{
io::{stdin, stdout, Stdout},
process,
sync::{mpsc, Arc, Mutex},
thread::{self, JoinHandle},
};
use termion::{
cursor::{self, HideCursor},
event::Key,
input::TermRead,
raw::{IntoRawMode, RawTerminal},
};
use bevy::prelude::*;
use crate::common::client_msg::Move;
use super::net::Server;
pub struct InputPlugin;
impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
let (rec, _thread) = spawn_reader();
let rec = Arc::new(Mutex::new(rec));
app.insert_resource(InputReceiver(rec))
.add_event::<KeyEvent>()
.add_systems(Update, try_read_keys)
.add_systems(Update, on_move);
}
}
#[derive(Resource)]
pub struct InputReceiver(Arc<Mutex<mpsc::Receiver<Key>>>);
#[derive(Debug, Event)]
pub struct KeyEvent(Key);
fn spawn_reader() -> (mpsc::Receiver<Key>, JoinHandle<()>) {
let (tx, rx) = mpsc::channel();
let thread = thread::spawn(move || {
let stdout = stdout().into_raw_mode().unwrap();
println!("{}", cursor::Hide);
let mut keys = stdin().keys();
while let Some(Ok(key)) = keys.next() {
if key == Key::Esc {
println!("exitting.");
println!("{}", cursor::Restore);
drop(stdout);
process::exit(0);
}
tx.send(key).ok();
}
});
(rx, thread)
}
fn try_read_keys(receiver: Res<InputReceiver>, mut writer: EventWriter<KeyEvent>) {
let receiver = receiver.0.lock().expect("Poisoned threads doomed the state.");
writer.send_batch(receiver.try_iter().map(KeyEvent));
}
fn on_move(mut keys: EventReader<KeyEvent>, server: Res<Server>, net: Res<Network<TcpProvider>>) {
if let Some(server) = server.0 {
for key in keys.read() {
let dir = match key.0 {
Key::Char('z') => IVec2::Y,
Key::Char('s') => -IVec2::Y,
Key::Char('q') => -IVec2::X,
Key::Char('d') => IVec2::X,
_ => continue,
};
net.send_message(server, Move(dir)).unwrap();
}
}
}

3
src/lib_client/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod display;
pub mod input;
pub mod net;

50
src/lib_client/net.rs Normal file
View file

@ -0,0 +1,50 @@
use bevy::{
prelude::*,
tasks::{TaskPool, TaskPoolBuilder},
};
use bevy_eventwork::{
tcp::{NetworkSettings, TcpProvider},
ConnectionId, EventworkPlugin, EventworkRuntime, Network, NetworkEvent,
};
use termion::clear;
pub struct NetPlugin;
impl Plugin for NetPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(EventworkPlugin::<TcpProvider, TaskPool>::default())
.insert_resource(EventworkRuntime(TaskPoolBuilder::new().num_threads(2).build()))
.insert_resource(NetworkSettings::default())
.add_systems(Update, handle_net_event)
.init_resource::<Server>()
.add_systems(Startup, do_connect);
}
}
#[derive(Debug, Resource, Default)]
pub struct Server(pub Option<ConnectionId>);
fn handle_net_event(mut events: EventReader<NetworkEvent>, mut server: ResMut<Server>) {
for event in events.read() {
match event {
NetworkEvent::Connected(connection_id) => {
println!("Connected '{connection_id:?}'.");
server.0 = Some(*connection_id);
}
NetworkEvent::Disconnected(connection_id) => {
print!("{}", clear::All);
println!("Disconnected '{connection_id:?}'.");
panic!();
}
NetworkEvent::Error(network_error) => println!("Error '{network_error}'."),
}
}
}
fn do_connect(
net: Res<Network<TcpProvider>>,
settings: Res<NetworkSettings>,
task_pool: Res<EventworkRuntime<TaskPool>>,
) {
net.connect(([0, 0, 0, 0], 9000).into(), &task_pool.0, &settings);
}

24
src/lib_server/counter.rs Normal file
View file

@ -0,0 +1,24 @@
use bevy::prelude::*;
pub struct CounterPlugin;
impl Plugin for CounterPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, sys_setup_counter)
.add_systems(Update, sys_count);
}
}
#[derive(Debug, Component)]
struct Counter(usize);
fn sys_count(mut query: Query<&mut Counter>) {
for mut c in &mut query {
println!("{c:?}");
c.0 += 1;
}
}
fn sys_setup_counter(mut commands: Commands) {
commands.spawn(Counter(0));
}

67
src/lib_server/display.rs Normal file
View file

@ -0,0 +1,67 @@
use crate::common::{self, server_msg::MapUpdates};
use bevy::{prelude::*, utils::hashbrown::HashSet};
use bevy_eventwork::{tcp::TcpProvider, ConnectionId, Network};
use super::{
physics::{update_physics_collisions, Pos},
player::Player,
};
pub struct DisplayPlugin;
impl Plugin for DisplayPlugin {
fn build(&self, app: &mut App) {
app.add_event::<RenderUpdate>()
// .add_systems(Update, rerender_players)
.add_systems(Update, on_updates.after(update_physics_collisions));
}
}
#[derive(Debug, Component)]
pub struct Display(pub (char, char));
#[derive(Debug, Event)]
pub struct RenderUpdate(pub IVec2);
const RENDER_DISTANCE: i32 = common::RENDER_DISTANCE;
const RENDER_DISTANCE_SQ: i32 = RENDER_DISTANCE * RENDER_DISTANCE;
fn on_updates(
mut updates: EventReader<RenderUpdate>,
players: Query<(&Player, &Pos)>,
displayables: Query<(&Pos, &Display)>,
net: Res<Network<TcpProvider>>,
) {
let mut to_render = HashSet::new();
for RenderUpdate(event_pos) in updates.read() {
for (Player(connection_id), player_pos) in &players {
if player_pos.pos().distance_squared(*event_pos) <= RENDER_DISTANCE_SQ {
to_render.insert((*connection_id, player_pos.pos()));
}
}
}
for (id, pos) in to_render {
render(&net, id, pos, &displayables);
}
}
fn render(
net: &Res<Network<TcpProvider>>,
id: ConnectionId,
player_pos: IVec2,
displayables: &Query<(&Pos, &Display)>,
) {
let updates = displayables
.iter()
.filter(|(pos, _)| pos.pos().distance_squared(player_pos) <= RENDER_DISTANCE_SQ)
.map(|(pos, Display(display))| (pos.pos() - player_pos, *display))
.collect();
net.send_message(id, MapUpdates(updates)).ok();
}
fn rerender_players(players: Query<&Pos, With<Player>>, mut writer: EventWriter<RenderUpdate>) {
for Pos(pos) in &players {
writer.send(RenderUpdate(pos.as_ivec2()));
}
}

54
src/lib_server/map.rs Normal file
View file

@ -0,0 +1,54 @@
use std::ops::Not;
use bevy::prelude::*;
use itermore::IterArrayChunks;
use super::{
display::Display,
physics::{Mass, Pos},
};
fn spawn_block(cmd: &mut Commands, pos: IVec2, display: (char, char), mass: usize) {
cmd.spawn((Pos(pos.as_vec2()), Mass(mass), Display(display)));
}
fn str_to_struct(text: &str) -> Vec<(IVec2, (char, char), usize)> {
text.lines()
.enumerate()
.flat_map(|(y, line)| {
line.chars().array_chunks().enumerate().filter_map(move |(x, [l, r])| {
(l == ' ' && r == ' ')
.not()
.then_some((IVec2::new(x as _, y as _), (l, r), mass_of((l, r))))
})
})
.collect()
}
fn mass_of(tiles: (char, char)) -> usize {
match tiles {
('#', '#') => 100,
('X', 'X') => 10,
('[', ']') => 5,
('(', ')') => 1,
_ => 20,
}
}
pub fn spawn_debug_map(mut cmd: Commands) {
let raw = "
()
########## ()
()## ##
## []
## ##
##########()
";
for (pos, display, mass) in str_to_struct(raw) {
spawn_block(&mut cmd, pos, display, mass);
}
}

27
src/lib_server/metrics.rs Normal file
View file

@ -0,0 +1,27 @@
use bevy::prelude::*;
use sysinfo::{CpuRefreshKind, RefreshKind, System};
use termion::cursor;
use super::player::Player;
pub struct MetricsPlugin;
#[derive(Debug, Resource)]
pub struct SysRes(System);
impl Plugin for MetricsPlugin {
fn build(&self, app: &mut App) {
let sys = System::new_with_specifics(RefreshKind::new().with_cpu(CpuRefreshKind::new().with_cpu_usage()));
app.add_systems(Update, show_metrics).insert_resource(SysRes(sys));
}
}
fn show_metrics(time: Res<Time>, mut sys: ResMut<SysRes>, players: Query<(), With<Player>>) {
sys.0.refresh_all();
let cpu = sys.0.global_cpu_usage();
let player_count = players.iter().count();
let time_delta = time.delta_seconds();
let up = cursor::Up(1);
println!(" | players {player_count:2} | tick {time_delta:2.3} | cpu {cpu:2.1}{up} ");
}

7
src/lib_server/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod counter;
pub mod display;
pub mod map;
pub mod metrics;
pub mod net;
pub mod physics;
pub mod player;

55
src/lib_server/net.rs Normal file
View file

@ -0,0 +1,55 @@
use bevy::{
prelude::*,
tasks::{TaskPool, TaskPoolBuilder},
};
use bevy_eventwork::{
tcp::{NetworkSettings, TcpProvider},
EventworkPlugin, EventworkRuntime, Network, NetworkEvent,
};
use crate::lib_server::player::{remove_player, spawn_player};
use super::{display::RenderUpdate, player::Player};
pub struct NetPlugin;
impl Plugin for NetPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(EventworkPlugin::<TcpProvider, TaskPool>::default())
.insert_resource(EventworkRuntime(TaskPoolBuilder::new().num_threads(2).build()))
.insert_resource(NetworkSettings::default())
.add_systems(Update, handle_net_event)
.add_systems(Startup, do_listen);
}
}
fn handle_net_event(
mut cmd: Commands,
mut events: EventReader<NetworkEvent>,
mut render_update: EventWriter<RenderUpdate>,
players: Query<(Entity, &Player)>,
) {
for event in events.read() {
match event {
NetworkEvent::Connected(conn_id) => {
println!("New connection, {conn_id:?}.");
spawn_player(&mut cmd, *conn_id, IVec2::ZERO);
render_update.send(RenderUpdate(IVec2::ZERO));
}
NetworkEvent::Disconnected(conn_id) => {
println!("Lost connection, {conn_id:?}.");
remove_player(&mut cmd, *conn_id, &players);
}
NetworkEvent::Error(network_error) => println!("Network failure '{network_error}'."),
}
}
}
fn do_listen(
mut net: ResMut<Network<TcpProvider>>,
settings: Res<NetworkSettings>,
task_pool: Res<EventworkRuntime<TaskPool>>,
) {
net.listen(([0, 0, 0, 0], 9000).into(), &task_pool.0, &settings)
.unwrap();
}

97
src/lib_server/physics.rs Normal file
View file

@ -0,0 +1,97 @@
use bevy::prelude::*;
use super::{display::RenderUpdate, player};
pub struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
update_physics_forces.after(player::handle_move),
update_physics_center.after(update_physics_forces),
update_physics_collisions.after(update_physics_center),
),
);
}
}
#[derive(Debug, Component, Clone)]
pub struct Pos(pub Vec2);
impl Pos {
pub fn fpos(&self) -> Vec2 {
self.0
}
pub fn pos_center(&self) -> Vec2 {
self.pos().as_vec2()
}
pub fn pos(&self) -> IVec2 {
self.0.round().as_ivec2()
}
pub fn dir_to_center(&self) -> Vec2 {
self.pos_center() - self.fpos()
}
}
#[derive(Debug, Component)]
pub struct Mass(pub usize);
#[derive(Debug, Component)]
pub struct Forces(Vec2);
impl Forces {
pub fn zero() -> Self {
Self::new(Vec2::ZERO)
}
pub fn new(forces: Vec2) -> Self {
Self(forces)
}
pub fn add(&mut self, force: Vec2) {
self.0 += force;
}
}
const MAX_MOVEMENT_PER_TICK: f32 = 0.3;
fn update_physics_forces(mut blocks: Query<(&mut Pos, &mut Forces)>, mut updater: EventWriter<RenderUpdate>) {
for (mut pos, mut forces) in &mut blocks {
let old_pos = pos.clone();
let applied = forces.0;
let applied = applied.clamp_length_max(MAX_MOVEMENT_PER_TICK);
pos.0 += applied;
forces.0 -= applied;
if old_pos.pos() != pos.pos() {
updater.send(RenderUpdate(old_pos.pos()));
}
}
}
pub fn update_physics_collisions(mut blocks: Query<(&mut Pos, &Mass)>) {
let mut combinations = blocks.iter_combinations_mut();
while let Some([(mut a_pos, Mass(a_mass)), (mut b_pos, Mass(b_mass))]) = combinations.fetch_next() {
if a_pos.pos() == b_pos.pos() {
let a_to_b = (b_pos.fpos() - a_pos.fpos()).normalize_or(Vec2::Y);
let total_mass = (a_mass + b_mass) as f32;
let a_mass_frac = (*a_mass) as f32 / total_mass;
let b_mass_frac = (*b_mass) as f32 / total_mass;
let a_movement = (-a_to_b) * b_mass_frac;
let b_movement = (a_to_b) * a_mass_frac;
a_pos.0 += a_movement;
b_pos.0 += b_movement;
}
}
}
pub fn update_physics_center(mut blocks: Query<(&Pos, &mut Forces)>) {
for (pos, mut forces) in &mut blocks {
let dir = pos.pos_center() - pos.0;
forces.add(dir * 0.75);
}
}

74
src/lib_server/player.rs Normal file
View file

@ -0,0 +1,74 @@
use crate::common::client_msg::Move;
use bevy::prelude::*;
use bevy_eventwork::{tcp::TcpProvider, AppNetworkMessage, ConnectionId, NetworkData};
use super::{
display::{Display, RenderUpdate},
physics::{Forces, Mass, Pos},
};
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app.listen_for_message::<Move, TcpProvider>()
.add_event::<RenderUpdate>()
.add_systems(Update, handle_move);
}
}
#[derive(Debug, Component)]
pub struct Player(pub ConnectionId);
#[derive(Debug, Component)]
pub struct Dir(pub IVec2);
fn player_display_for_dir(dir: IVec2) -> (char, char) {
match dir {
IVec2::Y => ('&', '/'),
IVec2::NEG_X => ('_', '&'),
IVec2::X => ('&', '_'),
_ => ('&', '\\'),
}
}
pub fn spawn_player(cmd: &mut Commands, conn_id: ConnectionId, pos: IVec2) {
cmd.spawn((
Player(conn_id),
Pos(pos.as_vec2()),
Display(player_display_for_dir(IVec2::X)),
Dir(IVec2::X),
Forces::zero(),
Mass(10),
));
}
pub fn remove_player(cmd: &mut Commands, conn_id: ConnectionId, players: &Query<(Entity, &Player)>) {
for (entity, player) in players {
if player.0 == conn_id {
cmd.entity(entity).despawn();
}
}
}
pub fn handle_move(
mut messages: EventReader<NetworkData<Move>>,
mut players: Query<(&Player, &mut Forces, &Pos, &mut Dir, &mut Display)>,
) {
for message in messages.read() {
let conn_id = message.source();
for (Player(id), mut forces, pos, mut dir, mut display) in &mut players {
if conn_id == id {
let dir_movement = message.0.as_vec2().normalize_or_zero();
let added_force = dir_movement + pos.dir_to_center();
forces.add(added_force);
let idir = dir_movement.as_ivec2();
if !(idir == IVec2::ZERO) {
dir.0 = idir;
display.0 = player_display_for_dir(idir);
}
}
}
}
}

30
src/server.rs Normal file
View file

@ -0,0 +1,30 @@
#![allow(dead_code)]
#![allow(unstable_name_collisions)]
use std::time::Duration;
use bevy::{app::ScheduleRunnerPlugin, prelude::*, time::TimePlugin};
use lib_server::{
display::DisplayPlugin, map, metrics::MetricsPlugin, net::NetPlugin, physics::PhysicsPlugin, player::PlayerPlugin,
};
mod common;
mod lib_server;
fn main() {
App::new()
// Core.
.add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_millis(50)))
.add_plugins(TimePlugin)
// World.
.add_plugins(PhysicsPlugin)
.add_plugins(DisplayPlugin)
.add_plugins(NetPlugin)
// Content.
.add_plugins(PlayerPlugin)
// Debug.
.add_plugins(MetricsPlugin)
// Startup.
.add_systems(Startup, map::spawn_debug_map)
.run();
}

5
watch_client Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/bash
set -e
cd "$(dirname "$(realpath "$0")")"
regar -sc 'sleep 1s; cargo run --release --bin=client' src

5
watch_server Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/bash
set -e
cd "$(dirname "$(realpath "$0")")"
regar -sc 'cargo run --release --bin=server' src