Skip to content

Instantly share code, notes, and snippets.

@michidk
Created May 10, 2024 19:29
Show Gist options
  • Save michidk/8647ba23facbb33bd8626376a0ae2b4d to your computer and use it in GitHub Desktop.
Save michidk/8647ba23facbb33bd8626376a0ae2b4d to your computer and use it in GitHub Desktop.
Fix meh plz
use std::borrow::BorrowMut;
use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
use async_trait::async_trait;
use log::debug;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::widgets::canvas::*;
use ratatui::widgets::{Block, Borders, Widget};
use ratatui::Terminal;
use russh::server::*;
use russh::{Channel, ChannelId};
use tokio::sync::Mutex;
type SshTerminal = Terminal<CrosstermBackend<TerminalHandle>>;
#[derive(Clone)]
struct TerminalHandle {
handle: Handle,
// The sink collects the data which is finally flushed to the handle.
sink: Vec<u8>,
channel_id: ChannelId,
}
// The crossterm backend writes to the terminal handle.
impl std::io::Write for TerminalHandle {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.sink.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
let handle = self.handle.clone();
let channel_id = self.channel_id;
let data = self.sink.clone().into();
futures::executor::block_on(async move {
let result = handle.data(channel_id, data).await;
if result.is_err() {
eprintln!("Failed to send data: {:?}", result);
}
});
self.sink.clear();
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Position2d {
x: u16,
y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Padel {
position: f64,
}
#[derive(Debug, Clone)]
struct GameState {
left_padel: Padel,
right_padel: Padel,
ball_position: Position2d,
}
impl GameState {
fn new() -> Self {
Self {
left_padel: Padel { position: 0f64 },
right_padel: Padel { position: 0f64 },
ball_position: Position2d { x: 5, y: 5 },
}
}
}
#[derive(Clone)]
struct Lobby {
name: String,
left_player: Option<Arc<Mutex<Player>>>,
right_player: Option<Arc<Mutex<Player>>>,
spectators: Vec<Arc<Mutex<Player>>>,
state: GameState,
}
#[derive(Clone)]
struct Player {
terminal: SshTerminal,
lobby: Arc<Mutex<Lobby>>,
}
impl Player {
fn new(terminal: SshTerminal, lobby: Arc<Mutex<Lobby>>) -> Self {
Self { terminal, lobby }
}
}
#[derive(Clone)]
struct GameServer {
players: Arc<Mutex<HashMap<usize, Arc<Mutex<Player>>>>>,
lobby: Arc<Mutex<Lobby>>,
id: usize,
}
impl GameServer {
pub fn new() -> Self {
Self {
players: Arc::new(Mutex::new(HashMap::new())),
lobby: Arc::new(Mutex::new(Lobby {
name: "lobby".to_string(),
left_player: None,
right_player: None,
spectators: Vec::new(),
state: GameState::new(),
})),
id: 0,
}
}
fn draw(state: GameState, area: Rect) -> impl Widget {
Canvas::default()
.block(Block::default().title("Game").borders(Borders::ALL))
.paint(move |ctx| {
ctx.draw(&Rectangle {
x: 0.0,
y: state.left_padel.position + 5.0,
width: 2.0,
height: 10.0,
color: Color::Red,
});
ctx.draw(&Rectangle {
x: area.width as f64 - 2.0,
y: state.right_padel.position + 5.0,
width: 2.0,
height: 10.0,
color: Color::Green,
});
ctx.draw(&Rectangle {
x: state.ball_position.x as f64,
y: state.ball_position.y as f64,
width: 2.0,
height: 2.0,
color: Color::Blue,
});
})
.x_bounds([0.0, area.width as f64])
.y_bounds([0.0, area.height as f64])
}
async fn render(lobby: &Lobby, player: &mut Player) {
player
.terminal
.draw(|f| {
let area = f.size();
f.render_widget(Self::draw(lobby.state.clone(), area), area);
})
.expect("Failed to draw");
}
pub async fn run(&mut self) -> Result<(), anyhow::Error> {
let players = self.players.clone();
let lobby = self.lobby.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
for (_, player) in players.lock().await.iter_mut() {
Self::render(&*lobby.lock().await, &mut *player.lock().await).await;
}
}
});
let config = Config {
inactivity_timeout: Some(std::time::Duration::from_secs(3600)),
auth_rejection_time: std::time::Duration::from_secs(3),
auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)),
keys: vec![russh_keys::key::KeyPair::generate_ed25519().unwrap()],
..Default::default()
};
self.run_on_address(Arc::new(config), ("0.0.0.0", 2222))
.await?;
Ok(())
}
}
impl Server for GameServer {
type Handler = Self;
fn new_client(&mut self, _: Option<std::net::SocketAddr>) -> Self {
let s = self.clone();
self.id += 1;
s
}
}
#[async_trait]
impl Handler for GameServer {
type Error = anyhow::Error;
async fn channel_open_session(
&mut self,
channel: Channel<Msg>,
session: &mut Session,
) -> Result<bool, Self::Error> {
{
{
let mut players = self.players.lock().await;
let terminal_handle = TerminalHandle {
handle: session.handle(),
sink: Vec::new(),
channel_id: channel.id(),
};
let backend = CrosstermBackend::new(terminal_handle.clone());
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let player = Arc::new(Mutex::new(Player::new(terminal, self.lobby.clone())));
players.insert(self.id, player.clone());
// assign player to lobby
let mut lobby = self.lobby.lock().await;
if lobby.left_player.is_none() {
lobby.left_player = Some(player);
debug!("left player assigned");
} else if lobby.right_player.is_none() {
lobby.right_player = Some(player);
debug!("right player assigned");
} else {
lobby.spectators.push(player);
debug!("spectator assigned");
}
}
// self.send_update().await;
}
Ok(true)
}
async fn auth_none(&mut self, _: &str) -> Result<Auth, Self::Error> {
Ok(russh::server::Auth::Accept)
}
async fn data(
&mut self,
channel: ChannelId,
data: &[u8],
session: &mut Session,
) -> Result<(), Self::Error> {
debug!("Got data: {:?}", data);
{
let mut players = self.players.lock().await;
let player_ref = players.get_mut(&self.id).expect("player not found");
let player = player_ref.lock().await;
let mut lobby = player.lobby.lock().await;
let mut padel: Option<&mut Padel> = None;
let update_left_padel = lobby
.left_player
.as_ref()
.map_or(false, |left_player| Arc::ptr_eq(left_player, &player_ref));
let update_right_padel = lobby
.right_player
.as_ref()
.map_or(false, |right_player| Arc::ptr_eq(right_player, &player_ref));
// Now, perform the mutable operation outside the checks
if update_left_padel {
padel = Some(&mut lobby.state.left_padel);
debug!("left padel updated");
}
if update_right_padel {
padel = Some(&mut lobby.state.right_padel);
debug!("right padel updated");
}
if let Some(padel) = padel {
// match arrow keys and wasd
if data.len() >= 3 {
// ANSI escape code
if data[0] == 0x1b && data[1] == 0x5b {
match data[2] {
0x41 => padel.position += 1.0,
0x42 => padel.position -= 1.0,
_ => {}
}
}
} else {
// Raw input
match data[0] {
b'w' => padel.position += 1.0,
b's' => padel.position -= 1.0,
b'q' => {
session.close(channel);
}
_ => {}
}
}
debug!("padel updated, new position: {:?}", padel.position);
}
}
// self.send_update().await;
Ok(())
}
/// The client's window size has changed.
async fn window_change_request(
&mut self,
_: ChannelId,
col_width: u32,
row_height: u32,
_: u32,
_: u32,
_: &mut Session,
) -> Result<(), Self::Error> {
{
let mut players = self.players.lock().await;
let player = players.get_mut(&self.id).unwrap();
let rect = Rect {
x: 0,
y: 0,
width: col_width as u16,
height: row_height as u16,
};
player.lock().await.terminal.resize(rect)?;
}
Ok(())
}
}
#[tokio::main]
async fn main() {
env_logger::builder()
.filter_level(log::LevelFilter::Debug)
.filter_module("russh::cipher", log::LevelFilter::Info)
.filter_module("russh::server", log::LevelFilter::Info)
.filter_module("russh::ssh_read", log::LevelFilter::Info)
.filter_module("russh::negotiation", log::LevelFilter::Info)
.init();
let mut server = GameServer::new();
server.run().await.expect("Failed running server");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment