Compare commits

...

47 Commits

Author SHA1 Message Date
efb21705c3 update iris 2025-12-09 02:04:02 -05:00
34f57ce0b5 update iris 2025-12-09 01:53:00 -05:00
4aa7219d49 finally usable state stuff ig 2025-12-08 00:12:30 -05:00
9511b4443b update stuff 2025-12-07 17:32:13 -05:00
089ed08c16 switch to WidgetRef 2025-12-07 15:18:39 -05:00
f9376eb6d4 start account ui 2025-12-07 00:32:48 -05:00
a1928edb66 update iris 2025-12-06 20:48:19 -05:00
09be172e36 nothing 2025-12-05 00:05:07 -05:00
d8a96bc956 update iris (window name) 2025-12-04 14:53:21 -05:00
02696b1f75 switch to default iris app 2025-12-04 14:31:34 -05:00
1904560244 warning 2025-12-03 22:52:16 -05:00
24bb65bf7b accounts are now real 2025-12-03 22:51:57 -05:00
4aa22de61b handle ctrlc 2025-11-28 20:24:26 -05:00
7557507f27 persistence + proper disconnect 2025-11-28 17:29:33 -05:00
029d62cb53 debug stuff + switch to cranelift 2025-11-25 15:00:07 -05:00
e1d0a423de single line textedit 2025-11-22 22:04:57 -05:00
c1040f9789 iris update 2025-11-22 21:38:25 -05:00
cd348dd978 iris update 2025-11-22 20:49:50 -05:00
f7e9346187 update iris 2025-11-22 20:37:45 -05:00
c31c3d90eb remove debug print 2025-11-22 20:23:22 -05:00
80e328e1aa iris update 2025-11-22 20:22:37 -05:00
7e15bfa494 iris stuff 2025-11-22 18:45:12 -05:00
3b38df6f39 fix wrapping text selection 2025-11-22 18:29:52 -05:00
4faa1b601a word selection 2025-11-22 15:33:36 -05:00
2928482322 iris update for more text controls 2025-11-22 15:02:38 -05:00
ac6ee0b119 make debug mode run similar speed (opt-level=1 + no validation layers) 2025-11-22 04:02:29 -05:00
7644aa83bb iris update to fix crash 2025-11-22 03:17:51 -05:00
b53a01cc46 fix text selection issue 2025-11-22 00:44:50 -05:00
90f9c13dd7 TEXT SELECTION 2025-11-22 00:15:02 -05:00
1008f97a52 warning 2025-11-21 20:26:43 -05:00
c8e70c0141 iris update 2025-11-21 20:26:04 -05:00
c2891654a3 remove default client data (where text hints go) 2025-11-21 20:20:51 -05:00
bbe8ea3a85 (iris) hint gaming 2025-11-21 20:18:52 -05:00
528a7503fc hint gaming 2025-11-21 20:18:29 -05:00
c0de059241 iris update 2025-11-21 18:19:03 -05:00
d93ced057a only load latest 50 msgs for testing 2025-11-21 14:38:27 -05:00
775d8c4891 remove testing msg limit + add top padding 2025-11-21 13:39:02 -05:00
8eda92f5f5 remove left aligns for text (default now) 2025-11-21 13:34:28 -05:00
6eac4f4576 add fuit background 2025-11-21 03:00:31 -05:00
ce07c80782 proper text entry box 2025-11-21 02:45:11 -05:00
6405c1bb3b new scrolling 2025-11-21 01:40:22 -05:00
bdcdb01524 update ui framework 2025-11-20 23:01:53 -05:00
d9f1620e8d uhhh.. that previous commit was for ui (iris), not this 2025-11-20 15:45:05 -05:00
0c707c62d1 the great orientation refactor (move to x & y UiScalars/Spans) + don't call full size in align 2025-11-20 15:44:30 -05:00
416d67103d finally text wrapping works™️ dear god 2025-11-20 00:20:49 -05:00
d8e8b1701d ui update 2025-11-18 17:52:55 -05:00
d4f8e1859d small ui stuff 2025-11-18 17:52:11 -05:00
26 changed files with 1722 additions and 911 deletions

911
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
cargo-features = ["codegen-backend"]
[package]
name = "openworm"
version = "0.1.0"
@@ -18,6 +20,11 @@ winit = "0.30.12"
bincode = "2.0.1"
zstd = "0.13.3"
ron = "0.12.0"
sled = "0.34.7"
clap = { version = "4.5.53", features = ["derive"] }
scrypt = "0.11.0"
ed25519-dalek = { version = "3.0.0-pre.2", features = ["rand_core"] }
rand = { version = "0.10.0-rc.5", features = ["chacha"] }
[[bin]]
name = "openworm-client"
@@ -26,3 +33,9 @@ path = "src/bin/client/main.rs"
[[bin]]
name = "openworm-server"
path = "src/bin/server/main.rs"
[profile.dev]
codegen-backend = "cranelift"
[profile.dev.package."*"]
opt-level = 1

2
iris

Submodule iris updated: 9febd03067...7f4846a2d3

22
src/bin/client/account.rs Normal file
View File

@@ -0,0 +1,22 @@
use ed25519_dalek::SigningKey;
use rand::{
SeedableRng,
rngs::{OsRng, StdRng},
};
pub struct Account {
device_key: SigningKey,
account_key: SigningKey,
}
impl Account {
pub fn new() -> Account {
let mut csprng = StdRng::try_from_rng(&mut OsRng).unwrap();
let device_key = SigningKey::generate(&mut csprng);
let account_key = SigningKey::generate(&mut csprng);
Account {
device_key,
account_key,
}
}
}

View File

@@ -1,67 +0,0 @@
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::{Window, WindowId},
};
use crate::ClientEvent;
use super::Client;
#[derive(Clone)]
pub struct AppHandle {
pub proxy: EventLoopProxy<ClientEvent>,
pub window: Arc<Window>,
}
pub struct App {
client: Option<Client>,
proxy: EventLoopProxy<ClientEvent>,
}
impl App {
pub fn run() {
let event_loop = EventLoop::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy();
event_loop
.run_app(&mut App {
client: Default::default(),
proxy,
})
.unwrap();
}
}
impl ApplicationHandler<ClientEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.client.is_none() {
let client = Client::new(event_loop, self.proxy.clone());
self.client = Some(client);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
let client = self.client.as_mut().unwrap();
client.window_event(event, event_loop);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: ClientEvent) {
let client = self.client.as_mut().unwrap();
client.event(event, event_loop);
}
fn exiting(&mut self, _: &ActiveEventLoop) {
let client = self.client.as_mut().unwrap();
client.exit();
}
}
impl AppHandle {
pub fn send(&self, event: ClientEvent) {
self.proxy.send_event(event).unwrap_or_else(|_| panic!());
self.window.request_redraw();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

28
src/bin/client/debug.rs Normal file
View File

@@ -0,0 +1,28 @@
use crate::Client;
use iris::prelude::*;
pub fn debug(_client: &mut Client, ui: &mut Ui, _state: &UiState) {
ui.debug_layers();
// let mut file = std::fs::OpenOptions::new()
// .write(true)
// .create(true)
// .truncate(true)
// .open("./old_msgs")
// .unwrap();
// bincode::encode_into_std_write(
// self.msgs.clone(),
// &mut file,
// openworm::net::BINCODE_CONFIG,
// )
// .unwrap();
// let mut file = std::fs::OpenOptions::new()
// .read(true)
// .open("./old_msgs")
// .unwrap();
// let msgs: Vec<NetMsg> =
// bincode::decode_from_std_read(&mut file, openworm::net::BINCODE_CONFIG).unwrap();
// for msg in msgs {
// println!("{msg:?}");
// }
// client.ui.debug_layers();
}

View File

@@ -1,83 +0,0 @@
use iris::{
core::{CursorState, Modifiers},
layout::Vec2,
};
use winit::{
event::{MouseButton, MouseScrollDelta, WindowEvent},
keyboard::{Key, NamedKey},
};
use super::Client;
#[derive(Default)]
pub struct Input {
cursor: CursorState,
pub modifiers: Modifiers,
}
impl Input {
pub fn event(&mut self, event: &WindowEvent) -> bool {
match event {
WindowEvent::CursorMoved { position, .. } => {
self.cursor.pos = Vec2::new(position.x as f32, position.y as f32);
self.cursor.exists = true;
}
WindowEvent::MouseInput { state, button, .. } => {
let buttons = &mut self.cursor.buttons;
let pressed = state.is_pressed();
match button {
MouseButton::Left => buttons.left.update(pressed),
MouseButton::Right => buttons.right.update(pressed),
MouseButton::Middle => buttons.middle.update(pressed),
_ => (),
}
}
WindowEvent::MouseWheel { delta, .. } => {
let mut delta = match *delta {
MouseScrollDelta::LineDelta(x, y) => Vec2::new(x, y),
MouseScrollDelta::PixelDelta(pos) => Vec2::new(pos.x as f32, pos.y as f32),
};
if delta.x == 0.0 && self.modifiers.shift {
delta.x = delta.y;
delta.y = 0.0;
}
self.cursor.scroll_delta = delta;
}
WindowEvent::CursorLeft { .. } => {
self.cursor.exists = false;
self.modifiers.clear();
}
WindowEvent::KeyboardInput { event, .. } => {
if let Key::Named(named) = event.logical_key {
let pressed = event.state.is_pressed();
match named {
NamedKey::Control => {
self.modifiers.control = pressed;
}
NamedKey::Shift => {
self.modifiers.shift = pressed;
}
_ => (),
}
}
}
_ => return false,
}
true
}
pub fn end_frame(&mut self) {
self.cursor.end_frame();
}
}
impl Client {
pub fn window_size(&self) -> Vec2 {
let size = self.renderer.window().inner_size();
(size.width, size.height).into()
}
pub fn cursor_state(&self) -> &CursorState {
&self.input.cursor
}
}

View File

@@ -1,203 +1,157 @@
#![windows_subsystem = "windows"]
use crate::{
app::App,
net::NetSender,
net::{NetHandle, NetSender},
rsc::{CLIENT_DATA, ClientData},
state::{ClientState, LoggedIn, Login},
ui::*,
};
pub use app::AppHandle;
use arboard::Clipboard;
use input::Input;
use iris::prelude::*;
use openworm::{
net::{ClientMsg, ServerMsg, install_crypto_provider},
rsc::DataDir,
};
use render::Renderer;
use std::sync::Arc;
use winit::{
event::{Ime, WindowEvent},
event_loop::{ActiveEventLoop, EventLoopProxy},
window::Window,
event::{ElementState, MouseButton, WindowEvent},
window::WindowAttributes,
};
mod app;
mod input;
mod account;
mod debug;
mod net;
mod render;
mod rsc;
mod state;
mod ui;
fn main() {
install_crypto_provider();
App::run();
DefaultApp::<Client>::run();
}
pub struct Client {
dir: DataDir,
data: ClientData,
state: ClientState,
main_ui: WidgetRef<WidgetPtr>,
notif: WidgetRef<WidgetPtr>,
proxy: Proxy<ClientEvent>,
}
pub enum ClientEvent {
Connect { send: NetSender, username: String },
Connect { send: NetSender },
ServerMsg(ServerMsg),
Err(String),
}
pub struct Client {
renderer: Renderer,
input: Input,
ui: Ui,
focus: Option<WidgetId<TextEdit>>,
channel: Option<WidgetId<Span>>,
username: String,
clipboard: Clipboard,
dir: DataDir,
data: ClientData,
handle: AppHandle,
error: Option<WidgetId<WidgetPtr>>,
ime: usize,
}
impl DefaultAppState for Client {
type Event = ClientEvent;
impl Client {
pub fn new(event_loop: &ActiveEventLoop, proxy: EventLoopProxy<ClientEvent>) -> Self {
let window = Arc::new(
event_loop
.create_window(Window::default_attributes().with_title("OPENWORM"))
.unwrap(),
);
let renderer = Renderer::new(window.clone());
fn window_attrs() -> WindowAttributes {
WindowAttributes::default().with_title("OPENWORM")
}
fn new(ui: &mut Ui, _state: &UiState, proxy: Proxy<Self::Event>) -> Self {
let dir = DataDir::default();
let handle = AppHandle { proxy, window };
let notif = WidgetPtr::default().add(ui);
let main_ui = WidgetPtr::default().add(ui);
let bg = (
image(include_bytes!("./assets/fuit.jpg")),
rect(Color::BLACK.alpha((0.8 * 255.0) as u8)),
)
.stack();
(
bg,
main_ui.clone(),
notif.clone().pad(Padding::top(10)).align(Align::TOP_CENTER),
)
.stack()
.set_root(ui);
let mut s = Self {
handle,
renderer,
input: Input::default(),
ui: Ui::new(),
data: dir.load(CLIENT_DATA),
state: Default::default(),
dir,
channel: None,
focus: None,
username: "<unknown>".to_string(),
clipboard: Clipboard::new().unwrap(),
error: None,
ime: 0,
main_ui: main_ui.clone(),
notif,
proxy,
};
ui::init(&mut s);
start_ui(&mut s, ui).set_ptr(&s.main_ui, ui);
s
}
pub fn event(&mut self, event: ClientEvent, _: &ActiveEventLoop) {
fn event(&mut self, event: ClientEvent, ui: &mut Ui, _state: &UiState) {
match event {
ClientEvent::Connect { send, username } => {
self.username = username;
send.send(ClientMsg::RequestMsgs);
main_view(self, send).set_root(&mut self.ui);
ClientEvent::Connect { send } => {
let ClientState::Connect(connect) = self.state.take() else {
panic!("invalid state");
};
let th = connect.handle.unwrap();
self.state = ClientState::Login(Login {
handle: NetHandle {
send: send.clone(),
thread: th,
},
});
// login_screen(self, ui).set_ptr(&self.main_ui, ui);
}
ClientEvent::ServerMsg(msg) => match msg {
ServerMsg::SendMsg(msg) => {
if let Some(msg_area) = &self.channel {
let msg = msg_widget(msg).add(&mut self.ui);
self.ui[msg_area].children.push(msg.any());
if let ClientState::LoggedIn(state) = &mut self.state
&& let Some(msg_area) = &state.channel
{
let msg = msg_widget(&msg.user, &msg.content).add(ui);
msg_area.get_mut().children.push(msg);
}
}
ServerMsg::LoadMsgs(msgs) => {
if let Some(msg_area) = &self.channel {
if let ClientState::LoggedIn(state) = &mut self.state
&& let Some(msg_area) = &state.channel
{
for msg in msgs {
let msg = msg_widget(msg).add(&mut self.ui);
self.ui[msg_area].children.push(msg.any());
state.msgs.push(msg.clone());
let msg = msg_widget(&msg.user, &msg.content).add(ui);
msg_area.get_mut().children.push(msg);
}
}
}
ServerMsg::Login { username } => {
let ClientState::Login(state) = self.state.take() else {
panic!("invalid state");
};
state.handle.send(ClientMsg::RequestMsgs);
self.state = ClientState::LoggedIn(LoggedIn {
network: state.handle,
channel: None,
msgs: Vec::new(),
username,
});
main_view(self, ui).set_ptr(&self.main_ui, ui);
}
ServerMsg::Error(error) => {
let msg = format!("{error:?}");
self.notif.get_mut().inner = Some(werror(ui, &msg));
}
},
ClientEvent::Err(msg) => {
if let Some(err) = &self.error {
self.ui[err].inner = Some(ui::error(&mut self.ui, &msg));
}
self.notif.get_mut().inner = Some(werror(ui, &msg));
}
}
}
pub fn window_event(&mut self, event: WindowEvent, event_loop: &ActiveEventLoop) {
let input_changed = self.input.event(&event);
let cursor_state = self.cursor_state().clone();
if let Some(focus) = &self.focus
&& cursor_state.buttons.left.is_start()
{
self.ui.text(focus).deselect();
self.focus = None;
}
if input_changed {
let window_size = self.window_size();
self.run_sensors(&cursor_state, window_size);
self.ui.run_sensors(&cursor_state, window_size);
}
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::RedrawRequested => {
self.ui.update();
self.renderer.update(&mut self.ui);
self.renderer.draw();
}
WindowEvent::Resized(size) => {
self.ui.resize((size.width, size.height));
self.renderer.resize(&size)
}
WindowEvent::KeyboardInput { event, .. } => {
if let Some(sel) = &self.focus
&& event.state.is_pressed()
{
let sel = &sel.clone();
let mut text = self.ui.text(sel);
match text.apply_event(&event, &self.input.modifiers) {
TextInputResult::Unfocus => {
self.focus = None;
self.handle.window.set_ime_allowed(false);
}
TextInputResult::Submit => {
self.run_event(sel, Submit, ());
}
TextInputResult::Paste => {
if let Ok(t) = self.clipboard.get_text() {
text.insert(&t);
}
self.run_event(sel, Edited, ());
}
TextInputResult::Used => {
self.run_event(sel, Edited, ());
}
TextInputResult::Unused => (),
}
}
}
WindowEvent::Ime(ime) => {
if let Some(sel) = &self.focus {
let mut text = self.ui.text(sel);
match ime {
Ime::Enabled | Ime::Disabled => (),
Ime::Preedit(content, _pos) => {
// TODO: highlight once that's real
text.replace(self.ime, &content);
self.ime = content.chars().count();
}
Ime::Commit(content) => {
text.insert(&content);
}
}
}
}
_ => (),
}
if self.ui.needs_redraw() {
self.renderer.window().request_redraw();
}
self.input.end_frame();
}
pub fn exit(&mut self) {
fn exit(&mut self, _ui: &mut Ui, _state: &UiState) {
self.state.exit();
self.dir.save(CLIENT_DATA, &self.data);
}
}
impl UiCtx for Client {
fn ui(&mut self) -> &mut Ui {
&mut self.ui
fn window_event(&mut self, event: WindowEvent, ui: &mut Ui, state: &UiState) {
if let WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Middle,
..
} = event
{
debug::debug(self, ui, state);
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::{AppHandle, ClientEvent};
use crate::ClientEvent;
use openworm::net::{
ClientMsg, RecvHandler, SERVER_NAME, ServerMsg, SkipServerVerification, recv_uni, send_uni,
};
@@ -9,36 +9,77 @@ use quinn::{
use std::{
net::{Ipv6Addr, SocketAddr, SocketAddrV6, ToSocketAddrs},
sync::Arc,
thread::JoinHandle,
time::Duration,
};
use tokio::sync::mpsc::UnboundedSender;
use winit::{event_loop::EventLoopProxy, window::Window};
pub const CLIENT_SOCKET: SocketAddr =
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0));
pub struct ConnectInfo {
pub ip: String,
pub username: String,
}
pub fn connect(handle: AppHandle, info: ConnectInfo) {
pub struct NetHandle {
pub send: NetSender,
pub thread: JoinHandle<()>,
}
#[derive(Clone)]
pub struct AppHandle {
pub proxy: EventLoopProxy<ClientEvent>,
pub window: Arc<Window>,
}
impl AppHandle {
pub fn send(&self, event: ClientEvent) {
self.proxy.send_event(event).unwrap_or_else(|_| panic!());
self.window.request_redraw();
}
}
pub fn connect(handle: AppHandle, info: ConnectInfo) -> JoinHandle<()> {
std::thread::spawn(move || {
if let Err(msg) = connect_the(handle.clone(), info) {
handle.send(ClientEvent::Err(msg));
}
});
})
}
type NetResult<T> = Result<T, String>;
type MsgPayload = ClientMsg;
#[derive(Clone)]
pub struct NetSender {
send: UnboundedSender<MsgPayload>,
send: UnboundedSender<NetCtrlMsg>,
}
pub enum NetCtrlMsg {
Send(ClientMsg),
Exit,
}
impl From<ClientMsg> for NetCtrlMsg {
fn from(value: ClientMsg) -> Self {
Self::Send(value)
}
}
impl NetSender {
pub fn send(&self, msg: ClientMsg) {
let _ = self.send.send(msg);
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
let _ = self.send.send(msg.into());
}
}
impl NetHandle {
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
self.send.send(msg.into());
}
pub fn exit(self) {
self.send(NetCtrlMsg::Exit);
let _ = self.thread.join();
}
}
@@ -68,7 +109,7 @@ impl NetSender {
// .map_err(|e| format!("failed to connect: {}", e))
// }
async fn connection_no_cert(addr: SocketAddr) -> NetResult<Connection> {
async fn connection_no_cert(addr: SocketAddr) -> NetResult<(Endpoint, Connection)> {
let mut endpoint = Endpoint::client(CLIENT_SOCKET).map_err(|e| e.to_string())?;
let quic = QuicClientConfig::try_from(
@@ -91,16 +132,17 @@ async fn connection_no_cert(addr: SocketAddr) -> NetResult<Connection> {
endpoint.set_default_client_config(config);
// connect to server
endpoint
let con = endpoint
.connect(addr, SERVER_NAME)
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())
.map_err(|e| e.to_string())?;
Ok((endpoint, con))
}
#[tokio::main]
async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
let (send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel::<MsgPayload>();
let (send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel::<NetCtrlMsg>();
let addr = info
.ip
@@ -108,11 +150,10 @@ async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
.map_err(|e| e.to_string())?
.next()
.ok_or("no addresses found".to_string())?;
let conn = connection_no_cert(addr).await?;
let (endpoint, conn) = connection_no_cert(addr).await?;
let conn_ = conn.clone();
handle.send(ClientEvent::Connect {
username: info.username,
send: NetSender { send },
});
@@ -120,9 +161,18 @@ async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
tokio::spawn(recv_uni(conn_, recv.into()));
while let Some(msg) = ui_recv.recv().await {
if send_uni(&conn, msg).await.is_err() {
println!("disconnected from server");
break;
match msg {
NetCtrlMsg::Send(msg) => {
if send_uni(&conn, msg).await.is_err() {
println!("disconnected from server");
break;
}
}
NetCtrlMsg::Exit => {
conn.close(0u32.into(), &[]);
endpoint.wait_idle().await;
break;
}
}
}

View File

@@ -1,151 +0,0 @@
use iris::{
layout::Ui,
render::{UiLimits, UiRenderer},
};
use pollster::FutureExt;
use std::sync::Arc;
use wgpu::{util::StagingBelt, *};
use winit::{dpi::PhysicalSize, window::Window};
pub const CLEAR_COLOR: Color = Color::BLACK;
pub struct Renderer {
window: Arc<Window>,
surface: Surface<'static>,
device: Device,
queue: Queue,
config: SurfaceConfiguration,
encoder: CommandEncoder,
staging_belt: StagingBelt,
pub ui: UiRenderer,
}
impl Renderer {
pub fn update(&mut self, updates: &mut Ui) {
self.ui.update(&self.device, &self.queue, updates);
}
pub fn draw(&mut self) {
let output = self.surface.get_current_texture().unwrap();
let view = output
.texture
.create_view(&TextureViewDescriptor::default());
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
{
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &view,
resolve_target: None,
ops: Operations {
load: LoadOp::Clear(CLEAR_COLOR),
store: StoreOp::Store,
},
depth_slice: None,
})],
..Default::default()
});
self.ui.draw(render_pass);
}
self.queue.submit(std::iter::once(encoder.finish()));
self.staging_belt.finish();
output.present();
self.staging_belt.recall();
}
pub fn resize(&mut self, size: &PhysicalSize<u32>) {
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.ui.resize(size, &self.queue);
}
fn create_encoder(device: &Device) -> CommandEncoder {
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render Encoder"),
})
}
pub fn new(window: Arc<Window>) -> Self {
let size = window.inner_size();
let instance = Instance::new(&InstanceDescriptor {
backends: Backends::PRIMARY,
..Default::default()
});
let surface = instance
.create_surface(window.clone())
.expect("Could not create window surface!");
let adapter = instance
.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.block_on()
.expect("Could not get adapter!");
let ui_limits = UiLimits::default();
let (device, queue) = adapter
.request_device(&DeviceDescriptor {
required_features: Features::TEXTURE_BINDING_ARRAY
| Features::PARTIALLY_BOUND_BINDING_ARRAY
| Features::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING,
required_limits: Limits {
max_binding_array_elements_per_shader_stage: ui_limits
.max_binding_array_elements_per_shader_stage(),
max_binding_array_sampler_elements_per_shader_stage: ui_limits
.max_binding_array_sampler_elements_per_shader_stage(),
..Default::default()
},
..Default::default()
})
.block_on()
.expect("Could not get device!");
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let config = SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: PresentMode::AutoVsync,
alpha_mode: surface_caps.alpha_modes[0],
desired_maximum_frame_latency: 2,
view_formats: vec![],
};
surface.configure(&device, &config);
let staging_belt = StagingBelt::new(4096 * 4);
let encoder = Self::create_encoder(&device);
let shape_pipeline = UiRenderer::new(&device, &queue, &config, ui_limits);
Self {
surface,
device,
queue,
config,
encoder,
staging_belt,
ui: shape_pipeline,
window,
}
}
pub fn window(&self) -> &Window {
self.window.as_ref()
}
}

View File

@@ -1,16 +1,11 @@
pub const CLIENT_DATA: &str = "client_data";
#[derive(bincode::Encode, bincode::Decode)]
#[derive(Debug, Default, bincode::Encode, bincode::Decode)]
pub struct ClientData {
pub ip: String,
pub username: String,
}
impl Default for ClientData {
fn default() -> Self {
Self {
ip: "localhost:39420".to_string(),
username: "your [NOVEMBER]".to_string(),
}
}
/// TODO: not store this as plain string?
/// need to figure out crypto stuff
/// or store session token
pub password: String,
}

51
src/bin/client/state.rs Normal file
View File

@@ -0,0 +1,51 @@
use iris::prelude::*;
use openworm::net::NetServerMsg;
use std::thread::JoinHandle;
use crate::net::NetHandle;
#[derive(Default)]
pub struct Connect {
pub handle: Option<JoinHandle<()>>,
}
pub struct Login {
pub handle: NetHandle,
}
pub struct LoggedIn {
pub network: NetHandle,
pub msgs: Vec<NetServerMsg>,
pub channel: Option<WidgetRef<Span>>,
pub username: String,
}
pub enum ClientState {
Connect(Connect),
Login(Login),
LoggedIn(LoggedIn),
}
impl Default for ClientState {
fn default() -> Self {
Self::Connect(Default::default())
}
}
impl ClientState {
pub fn take(&mut self) -> Self {
std::mem::take(self)
}
pub fn exit(&mut self) {
let s = self.take();
match s {
ClientState::Connect(_) => (),
ClientState::Login(Login { handle }) => {
handle.exit();
}
ClientState::LoggedIn(state) => {
state.network.exit();
}
}
}
}

View File

@@ -1,187 +0,0 @@
use iris::prelude::*;
use len_fns::*;
use openworm::net::{ClientMsg, Msg};
use winit::dpi::{LogicalPosition, LogicalSize};
use crate::{
Client,
net::{ConnectInfo, NetSender, connect},
};
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Submit;
#[derive(Eq, PartialEq, Hash, Clone)]
pub struct Edited;
impl DefaultEvent for Submit {
type Data = ();
}
impl DefaultEvent for Edited {
type Data = ();
}
pub fn init(client: &mut Client) {
login_screen(client).set_root(&mut client.ui);
}
pub fn main_view(client: &mut Client, network: NetSender) -> WidgetId {
let msg_panel = msg_panel(client, network);
let side_bar = rect(Color::BLACK.brighter(0.05)).width(80);
(side_bar, msg_panel)
.span(Dir::RIGHT)
.add(&mut client.ui)
.any()
}
pub fn error(ui: &mut Ui, msg: &str) -> WidgetId {
text(msg)
.size(20)
.color(Color::RED.brighter(0.3))
.pad(10)
.add(ui)
.any()
}
fn login_screen(client: &mut Client) -> WidgetId {
let Client {
ui, handle, data, ..
} = client;
let mut field = |name| text(name).editable().size(20).add(ui);
let ip = field(&data.ip);
let username = field(&data.username);
// let password = field("password");
let fbx = |field: WidgetId<TextEdit>| {
field
.clone()
.pad(10)
.background(rect(Color::BLACK.brighter(0.1)).radius(15))
.on(CursorSense::click(), focus_other(field))
};
// I LAV NOT HAVING ERGONOMIC CLONES
let handle = handle.clone();
let ip_ = ip.clone();
let username_ = username.clone();
let color = Color::GREEN;
let submit = rect(color)
.radius(15)
.id_on(CursorSense::click(), move |id, client: &mut Client, _| {
client.ui[id].color = color.darker(0.3);
let ip = client.ui[&ip_].content();
let username = client.ui[&username_].content();
connect(handle.clone(), ConnectInfo { ip, username });
})
.height(40);
let modal = (
text("login").size(30),
fbx(ip
.id_on(Edited, |id, client: &mut Client, _| {
client.data.ip = client.ui[id].content();
})
.add(ui)),
fbx(username
.id_on(Edited, |id, client: &mut Client, _| {
client.data.username = client.ui[id].content();
})
.add(ui)),
// fbx(password),
submit,
)
.span(Dir::DOWN)
.gap(10)
.pad(15)
.background(rect(Color::BLACK.brighter(0.2)).radius(15))
.width(400)
.align(Align::Center);
let err = WidgetPtr::default().add(ui);
client.error = Some(err.clone());
(modal, err.align(Align::Top)).stack().add(ui).any()
}
pub fn msg_widget(msg: Msg) -> impl WidgetLike<FnTag> {
let content = text(msg.content)
.editable()
.size(20)
.text_align(Align::Left)
.wrap(true)
.id_on(CursorSense::click(), |i, c, d| focus(i.clone(), c, d));
let header = text(msg.user).size(20);
(
image(include_bytes!("./assets/sungals.png"))
.sized((70, 70))
.align(Align::TopLeft),
(header.align(Align::TopLeft), content.align(Align::TopLeft))
.span(Dir::DOWN)
.gap(10),
)
.span(Dir::RIGHT)
.gap(10)
}
pub fn focus(id: WidgetId<TextEdit>, client: &mut Client, data: CursorData) {
client.ui.text(&id).select(data.cursor, data.size);
if let Some(region) = client.ui.window_region(&id) {
client.handle.window.set_ime_allowed(true);
client.handle.window.set_ime_cursor_area(
LogicalPosition::<f32>::from(region.top_left.tuple()),
LogicalSize::<f32>::from(region.size().tuple()),
);
}
client.focus = Some(id);
}
pub fn focus_other(id: WidgetId<TextEdit>) -> impl Fn(&mut Client, CursorData) {
move |client: &mut Client, data: CursorData| {
focus(id.clone(), client, data);
}
}
pub fn msg_panel(client: &mut Client, network: NetSender) -> impl WidgetFn<Stack> + use<> {
let Client { ui, channel, .. } = client;
let msg_area = Span::empty(Dir::DOWN).gap(15).add(ui);
*channel = Some(msg_area.clone());
let send_text = text("")
.editable()
.size(20)
.wrap(true)
.text_align(Align::Left)
.add(ui);
(
msg_area
.clone()
.align(Align::BotLeft)
.scroll()
.pad(Padding::x(15))
.height(rest(1)),
send_text
.clone()
.id_on(Submit, move |id, client: &mut Client, _| {
let content = client.ui.text(id).take();
let msg = Msg {
content: content.clone(),
user: client.username.clone(),
};
network.send(ClientMsg::SendMsg(msg.clone()));
let msg = msg_widget(msg).add(&mut client.ui);
client.ui[&msg_area].children.push(msg.any());
})
.pad(15)
.on(CursorSense::click(), focus_other(send_text))
.background(rect(Color::BLACK.brighter(0.05)).radius(15))
.pad(15)
.height(80)
.z_offset(1),
)
.span(Dir::DOWN)
.width(rest(1))
.background(rect(Color::BLACK.brighter(0.1)))
}

View File

@@ -0,0 +1,4 @@
use super::*;
pub const MODAL_BG: UiColor = UiColor::BLACK.brighter(0.05);
pub const GREEN: UiColor = UiColor::rgb(0, 150, 0);

View File

@@ -0,0 +1,151 @@
use super::*;
pub fn start_ui(client: &mut Client, ui: &mut Ui) -> WidgetRef {
let mut accounts = Span::empty(Dir::DOWN);
accounts.push(
wtext("no accounts")
.size(20)
.center_text()
.color(Color::GRAY)
.height(60)
.add(ui),
);
let connect = Button::submit("connect", ui);
let create = Button::submit("create", ui);
let connect_ = connect.clone();
let create_ = create.clone();
ui.on(connect.view(), Submit, move |_| {
connect_.disable();
create_.disable();
});
let connect_ = connect.clone();
let create_ = create.clone();
ui.on(create.view(), Submit, move |ctx| {
connect_.disable();
create_.disable();
create_ui(ctx.ui).set_ptr(&ctx.state.main_ui, ctx.ui);
});
(
wtext("Select Account").text_align(Align::CENTER).size(30),
accounts,
(connect, create).span(Dir::RIGHT).gap(10),
)
.span(Dir::DOWN)
.gap(30)
.modal(400)
.add(ui)
}
pub fn create_ui(ui: &mut Ui) -> WidgetRef {
wtext("hi").add(ui)
}
// pub fn connect_screen(client: &mut Client, ui: &mut Ui, state: &UiState) -> WidgetRef {
// let Client { data, proxy, .. } = client;
// let ip = field_widget(&data.ip, "ip", ui);
// let ip_ = ip.clone();
// let handle = AppHandle {
// proxy: proxy.clone(),
// window: state.window.clone(),
// };
//
// let submit = Button::submit("connect", ui);
//
// submit.on(Submit, move |ctx| {
// let ClientState::Connect(state) = &mut ctx.state.state else {
// return;
// };
// let ip = ip_.get().content();
// state.handle = Some(connect(handle.clone(), ConnectInfo { ip }));
// });
//
// (
// wtext("connect to a server")
// .text_align(Align::CENTER)
// .size(30),
// field_box(
// // NOTE: should probably do this on submit
// ip.on(Edited, |ctx| {
// ctx.state.data.ip = ctx.widget.get().content();
// })
// .add(ui),
// ui,
// ),
// submit,
// )
// .span(Dir::DOWN)
// .gap(10)
// .pad(15)
// .background(rect(Color::BLACK.brighter(0.2)).radius(15))
// .width(400)
// .align(Align::CENTER)
// .add(ui)
// }
// pub fn login_screen(client: &mut Client, ui: &mut Ui) -> WidgetRef {
// let Client { data, .. } = client;
// let username = field_widget(&data.username, "username", ui);
// let password = field_widget(&data.password, "password", ui);
// let username_ = username.clone();
// let password_ = password.clone();
// let submit = Button::submit("login", ui);
// submit
// .on(move |client, _ui| {
// let ClientState::Login(state) = &mut client.state else {
// return;
// };
// let username = username_.get().content();
// let password = password_.get().content();
// state.handle.send(ClientMsg::Login { username, password });
// })
// .add(ui);
// let username_ = username.clone();
// let password_ = password.clone();
// let create_button = Button::submit(
// "create account",
// move |client, _ui| {
// let ClientState::Login(state) = &mut client.state else {
// return;
// };
// let username = username_.get().content();
// let password = password_.get().content();
// state
// .handle
// .send(ClientMsg::CreateAccount { username, password });
// },
// ui,
// );
// create_button.on()
// (
// wtext("login to server").text_align(Align::CENTER).size(30),
// field_box(
// username
// .on(Edited, |ctx| {
// ctx.state.data.username = ctx.widget.get().content();
// })
// .add(ui),
// ui,
// ),
// field_box(
// password
// .on(Edited, |ctx| {
// ctx.state.data.password = ctx.widget.get().content();
// })
// .add(ui),
// ui,
// ),
// submit,
// create_button,
// )
// .span(Dir::DOWN)
// .gap(10)
// .pad(15)
// .background(rect(Color::BLACK.brighter(0.2)).radius(15))
// .width(400)
// .align(Align::CENTER)
// .add(ui)
// }

82
src/bin/client/ui/main.rs Normal file
View File

@@ -0,0 +1,82 @@
use super::*;
use crate::state::{ClientState, LoggedIn};
use openworm::net::{ClientMsg, NetClientMsg};
pub const SIZE: u32 = 20;
pub fn main_view(client: &mut Client, ui: &mut Ui) -> WidgetRef {
let ClientState::LoggedIn(state) = &mut client.state else {
panic!("we ain't logged in buh");
};
let msg_panel = msg_panel(ui, state);
let side_bar = rect(Color::BLACK.brighter(0.05)).width(80);
(side_bar, msg_panel).span(Dir::RIGHT).add(ui)
}
pub fn msg_widget(username: &str, content: &str) -> impl WidgetRet {
let content = wtext(content)
.editable(false)
.size(SIZE)
.wrap(true)
.attr::<Selectable>(());
let header = wtext(username).size(SIZE);
(
image(include_bytes!("../assets/sungals.png"))
.sized((70, 70))
.align(Align::TOP),
(header, content)
.span(Dir::DOWN)
.gap(10)
.width(rest(1))
.align(Align::TOP),
)
.span(Dir::RIGHT)
.gap(10)
.to_any()
}
pub fn msg_panel(ui: &mut Ui, state: &mut LoggedIn) -> WidgetRef {
let msg_area = Span::empty(Dir::DOWN).gap(15).add(ui);
state.channel = Some(msg_area.clone());
let send_text = wtext("")
.editable(false)
.size(SIZE)
.wrap(true)
.hint(hint("send message"))
.add(ui);
(
msg_area
.clone()
.scroll()
.pad(Padding::x(15).with_top(15))
.height(rest(1)),
send_text
.clone()
.on(Submit, move |ctx| {
let ClientState::LoggedIn(state) = &mut ctx.state.state else {
panic!("we ain't logged in buh");
};
let content = ctx.widget.get_mut().take();
let msg = NetClientMsg {
content: content.clone(),
};
state.network.send(ClientMsg::SendMsg(msg.clone()));
let msg = msg_widget(&state.username, &content).add(ctx.ui);
msg_area.get_mut().children.push(msg);
})
.pad(15)
.attr::<Selector>(send_text)
.scroll()
.masked()
.background(rect(Color::BLACK.brighter(0.05)).radius(15))
.pad(15)
.max_height(rel(0.5))
.layer_offset(1),
)
.span(Dir::DOWN)
.width(rest(1))
.add(ui)
}

113
src/bin/client/ui/misc.rs Normal file
View File

@@ -0,0 +1,113 @@
use super::*;
pub fn werror(ui: &mut Ui, msg: &str) -> WidgetRef {
wtext(msg)
.size(20)
.pad(10)
.background(rect(Color::RED).radius(10))
.add(ui)
}
pub fn hint(msg: impl Into<String>) -> TextBuilder {
wtext(msg).size(20).color(Color::GRAY)
}
pub fn field_widget(name: &str, hint_text: &str, ui: &mut Ui) -> WidgetRef<TextEdit> {
wtext(name)
.editable(true)
.size(20)
.hint(hint(hint_text))
.add(ui)
}
pub fn field_box(field: WidgetRef<TextEdit>, ui: &mut Ui) -> WidgetRef {
field
.clone()
.pad(10)
.background(rect(Color::BLACK.brighter(0.1)).radius(15))
.attr::<Selector>(field)
.add(ui)
}
#[derive(Clone)]
pub struct Button {
color: UiColor,
root: WidgetRef,
rect: WidgetRef<Rect>,
enabled: Handle<bool>,
}
impl WidgetView for Button {
fn view(&self) -> &WidgetRef<Self::Widget> {
&self.root
}
}
impl Button {
pub fn new(text: &str, color: UiColor, ui: &mut Ui) -> Self {
let rect = rect(color).radius(15).add(ui);
let enabled = Handle::from(true);
let enabled_ = enabled.clone();
let enabled__ = enabled.clone();
let root = rect
.clone()
.on(
CursorSense::HoverStart | CursorSense::unclick(),
move |ctx| {
if !*enabled_.get() {
return;
}
ctx.widget.get_mut().color = color.brighter(0.1);
},
)
.on(CursorSense::HoverEnd, move |ctx| {
if !*enabled__.get() {
return;
}
ctx.widget.get_mut().color = color;
})
.height(60)
.foreground(wtext(text).size(25).text_align(Align::CENTER))
.add(ui);
let root_ = root.clone();
let enabled_ = enabled.clone();
rect.clone()
.on(CursorSense::click(), move |ctx| {
if !*enabled_.get() {
return;
}
ctx.widget.get_mut().color = color.darker(0.2);
ctx.ui.run_event(ctx.state, &root_, Submit, ());
})
.add(ui);
Self {
root,
rect,
color,
enabled,
}
}
pub fn submit(text: &str, ui: &mut Ui) -> Self {
Self::new(text, color::GREEN, ui)
}
pub fn disable(&self) {
*self.enabled.get_mut() = false;
self.rect.get_mut().color = self.color.darker(0.8);
}
}
widget_trait! {
pub trait Stuff;
fn modal(self, width: impl UiNum) -> impl WidgetIdFn {
|ui| {
self
.pad(15)
.background(rect(color::MODAL_BG).radius(15))
.width(width)
.align(Align::CENTER)
.add(ui)
}
}
}

14
src/bin/client/ui/mod.rs Normal file
View File

@@ -0,0 +1,14 @@
use crate::Client;
use iris::prelude::*;
use len_fns::*;
mod connect;
mod main;
mod misc;
pub mod color;
pub use connect::*;
pub use main::*;
pub use misc::*;
event_ctx!(Client);

3
src/bin/server/data.rs Normal file
View File

@@ -0,0 +1,3 @@
pub struct DBMsg {
}

167
src/bin/server/db.rs Normal file
View File

@@ -0,0 +1,167 @@
use std::{
marker::PhantomData,
ops::{Deref, DerefMut},
path::Path,
};
use bincode::{Decode, Encode};
use openworm::net::BINCODE_CONFIG;
use sled::Tree;
pub const DB_VERSION: u64 = 0;
#[derive(Encode, Decode)]
pub struct User {
pub username: String,
pub password_hash: String,
}
#[derive(Clone, Encode, Decode)]
pub struct Msg {
pub user: u64,
pub content: String,
}
#[derive(Clone)]
pub struct Db {
pub db: sled::Db,
pub msgs: DbMap<u64, Msg>,
pub users: DbMap<u64, User>,
pub usernames: DbMap<String, u64>,
}
pub struct DbMap<K, V> {
tree: Tree,
_pd: PhantomData<(K, V)>,
}
pub trait Key {
type Output<'a>: AsRef<[u8]>
where
Self: 'a;
fn bytes(&self) -> Self::Output<'_>;
}
impl Key for String {
type Output<'a> = &'a Self;
fn bytes(&self) -> Self::Output<'_> {
self
}
}
impl Key for str {
type Output<'a> = &'a Self;
fn bytes(&self) -> Self::Output<'_> {
self
}
}
impl Key for u64 {
type Output<'a> = [u8; 8];
fn bytes(&self) -> Self::Output<'_> {
self.to_be_bytes()
}
}
impl<K: Key, V: Encode + Decode<()>> DbMap<K, V> {
pub fn insert(&self, k: &K, v: &V) {
self.tree.insert_(k, v);
}
pub fn get(&self, k: &K) -> Option<V> {
self.tree.get_(k)
}
pub fn init_unique(&self, k: &K) -> bool {
self.tree
.compare_and_swap(k.bytes(), None as Option<&[u8]>, Some(&[0]))
.unwrap()
.is_ok()
}
pub fn iter_all(&self) -> impl Iterator<Item = V> {
self.tree.iter_all()
}
}
pub fn open_db(path: impl AsRef<Path>) -> Db {
let db = sled::open(path).expect("failed to open database");
if !db.was_recovered() {
println!("no previous db found, creating new");
db.insert_("version", DB_VERSION);
db.flush().unwrap();
} else {
let version: u64 = db.get_("version").expect("failed to read db version");
println!("found existing db version {version}");
if version != DB_VERSION {
panic!("non matching db version! (auto update in the future)");
}
}
Db {
msgs: open_tree("msg", &db),
users: open_tree("user", &db),
usernames: open_tree("username", &db),
db,
}
}
trait DbUtil {
fn insert_<V: Encode>(&self, k: &(impl Key + ?Sized), v: V);
fn get_<V: Decode<()>>(&self, k: &(impl Key + ?Sized)) -> Option<V>;
fn iter_all<V: Decode<()>>(&self) -> impl Iterator<Item = V>;
}
impl DbUtil for Tree {
fn insert_<V: Encode>(&self, k: &(impl Key + ?Sized), v: V) {
let bytes = bincode::encode_to_vec(v, BINCODE_CONFIG).unwrap();
self.insert(k.bytes(), bytes).unwrap();
}
fn get_<V: Decode<()>>(&self, k: &(impl Key + ?Sized)) -> Option<V> {
let bytes = self.get(k.bytes()).unwrap()?;
Some(
bincode::decode_from_slice(&bytes, BINCODE_CONFIG)
.unwrap()
.0,
)
}
fn iter_all<V: Decode<()>>(&self) -> impl Iterator<Item = V> {
self.iter().map(|r| {
bincode::decode_from_slice(&r.unwrap().1, BINCODE_CONFIG)
.unwrap()
.0
})
}
}
pub fn open_tree<K, V>(name: &str, db: &sled::Db) -> DbMap<K, V> {
DbMap {
tree: db.open_tree(name).unwrap(),
_pd: PhantomData,
}
}
impl Deref for Db {
type Target = sled::Db;
fn deref(&self) -> &Self::Target {
&self.db
}
}
impl DerefMut for Db {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.db
}
}
impl<K, V> Clone for DbMap<K, V> {
fn clone(&self) -> Self {
Self {
tree: self.tree.clone(),
_pd: self._pd,
}
}
}

View File

@@ -1,10 +1,21 @@
// mod data;
mod db;
mod net;
use crate::db::{Db, Msg, User, open_db};
use clap::Parser;
use net::{ClientSender, ConAccepter, listen};
use openworm::{
net::{ClientMsg, DisconnectReason, Msg, RecvHandler, ServerMsg, install_crypto_provider},
net::{
ClientMsg, DisconnectReason, NetServerMsg, RecvHandler, ServerError, ServerMsg,
install_crypto_provider,
},
rsc::DataDir,
};
use scrypt::{
Scrypt,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use std::{
collections::HashMap,
sync::{
@@ -12,40 +23,66 @@ use std::{
atomic::{AtomicU64, Ordering},
},
};
use tokio::sync::RwLock;
use tokio::{signal::ctrl_c, sync::RwLock};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// port to listen on
#[arg(short, long)]
port: u16,
}
fn main() {
let args = Args::parse();
install_crypto_provider();
run_server();
run_server(args.port);
}
#[tokio::main]
pub async fn run_server() {
pub async fn run_server(port: u16) {
let dir = DataDir::default();
let path = dir.get();
let db: Db = open_db(path.join("server.db"));
let handler = ServerListener {
msgs: Default::default(),
senders: Default::default(),
count: 0.into(),
db: db.clone(),
};
listen(path, handler).await;
let (endpoint, handle) = listen(port, path, handler);
let _ = ctrl_c().await;
println!("stopping server");
println!("closing connections...");
endpoint.close(0u32.into(), &[]);
let _ = handle.await;
endpoint.wait_idle().await;
println!("saving...");
db.flush_async().await.unwrap();
println!("saved");
}
type ClientId = u64;
struct ServerListener {
msgs: Arc<RwLock<Vec<Msg>>>,
db: Db,
senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>,
count: AtomicU64,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum ClientState {
Login,
Authed(u64),
}
impl ConAccepter for ServerListener {
async fn accept(&self, send: ClientSender) -> impl RecvHandler<ClientMsg> {
let id = self.count.fetch_add(1, Ordering::Release);
self.senders.write().await.insert(id, send.clone());
ClientHandler {
msgs: self.msgs.clone(),
db: self.db.clone(),
senders: self.senders.clone(),
state: Arc::new(RwLock::new(ClientState::Login)),
send,
id,
}
@@ -53,18 +90,36 @@ impl ConAccepter for ServerListener {
}
struct ClientHandler {
msgs: Arc<RwLock<Vec<Msg>>>,
db: Db,
send: ClientSender,
senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>,
id: ClientId,
state: Arc<RwLock<ClientState>>,
}
impl RecvHandler<ClientMsg> for ClientHandler {
async fn connect(&self) -> () {
println!("connected: {:?}", self.send.remote().ip());
}
async fn msg(&self, msg: ClientMsg) {
match msg {
ClientMsg::SendMsg(msg) => {
self.msgs.write().await.push(msg.clone());
let ClientState::Authed(uid) = &*self.state.read().await else {
let _ = self.send.send(ServerError::NotLoggedIn).await;
return;
};
let msg = Msg {
user: *uid,
content: msg.content,
};
let id = self.db.generate_id().unwrap();
self.db.msgs.insert(&id, &msg);
let mut handles = Vec::new();
let user: User = self.db.users.get(uid).unwrap();
let msg = NetServerMsg {
content: msg.content,
user: user.username,
};
for (&id, send) in self.senders.read().await.iter() {
if id == self.id {
continue;
@@ -72,7 +127,7 @@ impl RecvHandler<ClientMsg> for ClientHandler {
let send = send.clone();
let msg = msg.clone();
let fut = async move {
let _ = send.send(ServerMsg::SendMsg(msg)).await;
let _ = send.send(msg).await;
};
handles.push(tokio::spawn(fut));
}
@@ -81,16 +136,85 @@ impl RecvHandler<ClientMsg> for ClientHandler {
}
}
ClientMsg::RequestMsgs => {
let msgs = self.msgs.read().await.clone();
let ClientState::Authed(_uid) = &*self.state.read().await else {
let _ = self.send.send(ServerError::NotLoggedIn).await;
return;
};
let msgs = self
.db
.msgs
.iter_all()
.map(|msg| {
let user = self
.db
.users
.get(&msg.user)
.map(|user| user.username.to_string())
.unwrap_or("deleted user".to_string());
NetServerMsg {
content: msg.content,
user,
}
})
.collect();
let _ = self.send.send(ServerMsg::LoadMsgs(msgs)).await;
}
ClientMsg::CreateAccount { username, password } => {
if !self.db.usernames.init_unique(&username) {
let _ = self.send.send(ServerError::UsernameTaken).await;
return;
}
let id = self.db.generate_id().unwrap();
let salt = SaltString::generate(&mut OsRng);
let params = scrypt::Params::new(11, 8, 1, 32).unwrap();
let hash = Scrypt
.hash_password_customized(password.as_bytes(), None, None, params, &salt)
.unwrap()
.to_string();
self.db.users.insert(
&id,
&User {
username: username.clone(),
password_hash: hash,
},
);
println!("account created: \"{username}\"");
self.db.usernames.insert(&username, &id);
*self.state.write().await = ClientState::Authed(id);
let _ = self.send.send(ServerMsg::Login { username }).await;
}
ClientMsg::Login { username, password } => {
let Some(id) = self.db.usernames.get(&username) else {
let _ = self.send.send(ServerError::UnknownUsername).await;
return;
};
let Some(user) = self.db.users.get(&id) else {
panic!("invalid state! (should be a user)");
};
let hash = PasswordHash::new(&user.password_hash).unwrap();
if Scrypt.verify_password(password.as_bytes(), &hash).is_err() {
println!("invalid password: \"{username}\"");
let _ = self.send.send(ServerError::InvalidPassword).await;
return;
}
println!("login: \"{username}\"");
*self.state.write().await = ClientState::Authed(id);
let _ = self.send.send(ServerMsg::Login { username }).await;
}
}
}
async fn disconnect(&self, reason: DisconnectReason) -> () {
println!("disconnected: {:?}", self.send.remote().ip());
match reason {
DisconnectReason::Closed | DisconnectReason::Timeout => (),
DisconnectReason::Other(e) => println!("connection issue: {e}"),
}
}
}
impl ClientState {
pub fn is_authed(&self) -> bool {
matches!(self, Self::Authed(_))
}
}

View File

@@ -10,14 +10,12 @@ use std::{
net::{Ipv6Addr, SocketAddr, SocketAddrV6},
sync::Arc,
};
use tokio::task::JoinHandle;
use tracing::Instrument;
pub const DEFAULT_PORT: u16 = 16839;
pub const SERVER_HOST: Ipv6Addr = Ipv6Addr::UNSPECIFIED;
pub const SERVER_SOCKET: SocketAddr =
SocketAddr::V6(SocketAddrV6::new(SERVER_HOST, DEFAULT_PORT, 0, 0));
pub fn init_endpoint(data_path: &Path) -> Endpoint {
pub fn init_endpoint(port: u16, data_path: &Path) -> Endpoint {
let cert_path = data_path.join("cert.der");
let key_path = data_path.join("key.der");
let (cert, key) = match fs::read(&cert_path).and_then(|x| Ok((x, fs::read(&key_path)?))) {
@@ -51,7 +49,8 @@ pub fn init_endpoint(data_path: &Path) -> Endpoint {
// let transport_config = Arc::get_mut(&mut server_config.transport).unwrap();
// transport_config.max_concurrent_uni_streams(0_u8.into());
quinn::Endpoint::server(server_config, SERVER_SOCKET).unwrap()
let server_socket: SocketAddr = SocketAddr::V6(SocketAddrV6::new(SERVER_HOST, port, 0, 0));
quinn::Endpoint::server(server_config, server_socket).unwrap()
}
#[derive(Clone)]
@@ -60,7 +59,11 @@ pub struct ClientSender {
}
impl ClientSender {
pub async fn send(&self, msg: ServerMsg) -> SendResult {
pub fn remote(&self) -> SocketAddr {
self.conn.remote_address()
}
pub async fn send(&self, msg: impl Into<ServerMsg>) -> SendResult {
let msg = msg.into();
send_uni(&self.conn, msg).await
}
}
@@ -72,19 +75,31 @@ pub trait ConAccepter: Send + Sync + 'static {
) -> impl Future<Output = impl RecvHandler<ClientMsg>> + Send;
}
pub async fn listen(data_path: &Path, accepter: impl ConAccepter) {
pub fn listen(
port: u16,
data_path: &Path,
accepter: impl ConAccepter,
) -> (Endpoint, JoinHandle<()>) {
let accepter = Arc::new(accepter);
let endpoint = init_endpoint(data_path);
println!("listening on {}", endpoint.local_addr().unwrap());
let endpoint = init_endpoint(port, data_path);
let res = endpoint.clone();
while let Some(conn) = endpoint.accept().await {
let fut = handle_connection(conn, accepter.clone());
tokio::spawn(async move {
if let Err(e) = fut.await {
eprintln!("connection failed: {reason}", reason = e)
}
});
}
let handle = tokio::spawn(async move {
println!("listening on {}", endpoint.local_addr().unwrap());
let mut tasks = Vec::new();
while let Some(conn) = endpoint.accept().await {
let fut = handle_connection(conn, accepter.clone());
tasks.push(tokio::spawn(async move {
if let Err(e) = fut.await {
eprintln!("connection failed: {reason}", reason = e)
}
}));
}
for task in tasks {
let _ = task.await;
}
});
(res, handle)
}
async fn handle_connection(
@@ -93,6 +108,7 @@ async fn handle_connection(
) -> std::io::Result<()> {
let conn = conn.await?;
let handler = Arc::new(accepter.accept(ClientSender { conn: conn.clone() }).await);
handler.connect().await;
let span = tracing::info_span!(
"connection",
remote = %conn.remote_address(),

View File

@@ -11,24 +11,53 @@ pub const BINCODE_CONFIG: Configuration = bincode::config::standard();
#[derive(Debug, bincode::Encode, bincode::Decode)]
pub enum ClientMsg {
SendMsg(Msg),
SendMsg(NetClientMsg),
RequestMsgs,
CreateAccount { username: String, password: String },
Login { username: String, password: String },
}
#[derive(Debug, bincode::Encode, bincode::Decode)]
pub enum ServerMsg {
SendMsg(Msg),
LoadMsgs(Vec<Msg>),
SendMsg(NetServerMsg),
LoadMsgs(Vec<NetServerMsg>),
Login { username: String },
Error(ServerError),
}
#[derive(Debug, bincode::Encode, bincode::Decode)]
pub enum ServerError {
NotLoggedIn,
UnknownUsername,
InvalidPassword,
UsernameTaken,
}
impl From<ServerError> for ServerMsg {
fn from(value: ServerError) -> Self {
Self::Error(value)
}
}
pub type ServerResp<T> = Result<T, String>;
#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
pub struct Msg {
pub struct NetClientMsg {
pub content: String,
}
#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
pub struct NetServerMsg {
pub content: String,
pub user: String,
}
impl From<NetServerMsg> for ServerMsg {
fn from(value: NetServerMsg) -> Self {
Self::SendMsg(value)
}
}
pub fn install_crypto_provider() {
quinn::rustls::crypto::ring::default_provider()
.install_default()

View File

@@ -6,9 +6,13 @@ use tokio::io::{AsyncReadExt as _, AsyncWriteExt};
use tracing::Instrument as _;
pub trait RecvHandler<M>: Send + Sync + 'static {
fn connect(&self) -> impl Future<Output = ()> + Send {
async {}
}
fn msg(&self, msg: M) -> impl Future<Output = ()> + Send;
#[allow(unused)]
fn disconnect(&self, reason: DisconnectReason) -> impl Future<Output = ()> + Send {
async { drop(reason) }
async {}
}
}
@@ -39,6 +43,7 @@ pub async fn recv_uni<M: bincode::Decode<()>>(
Err(quinn::ConnectionError::ApplicationClosed { .. }) => {
return DisconnectReason::Closed;
}
Err(quinn::ConnectionError::LocallyClosed) => return DisconnectReason::Closed,
Err(quinn::ConnectionError::TimedOut) => {
return DisconnectReason::Timeout;
}

View File

@@ -7,6 +7,7 @@ use directories_next::ProjectDirs;
use crate::net::BINCODE_CONFIG;
#[derive(Clone)]
pub struct DataDir {
dirs: ProjectDirs,
}