summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/gfx.rs141
-rw-r--r--src/gfx/map.rs447
-rw-r--r--src/gfx/media.rs42
-rw-r--r--src/gfx/state.rs233
-rw-r--r--src/gfx/util.rs63
-rw-r--r--src/main.rs154
-rw-r--r--src/net.rs252
7 files changed, 1216 insertions, 116 deletions
diff --git a/src/gfx.rs b/src/gfx.rs
new file mode 100644
index 0000000..e3fb073
--- /dev/null
+++ b/src/gfx.rs
@@ -0,0 +1,141 @@
+use crate::{GfxEvent::*, NetEvent};
+use cgmath::Rad;
+use std::time::Instant;
+use tokio::sync::mpsc;
+use winit::{
+ event::{DeviceEvent::*, Event::*, WindowEvent::*},
+ event_loop::ControlFlow::ExitWithCode,
+ platform::run_return::EventLoopExtRunReturn,
+ window::CursorGrabMode,
+};
+
+mod map;
+mod media;
+mod state;
+mod util;
+
+pub async fn run(
+ mut event_loop: winit::event_loop::EventLoop<crate::GfxEvent>,
+ net_events: mpsc::UnboundedSender<NetEvent>,
+) {
+ let window = winit::window::WindowBuilder::new()
+ .build(&event_loop)
+ .unwrap();
+
+ window
+ .set_cursor_grab(CursorGrabMode::Locked)
+ .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Confined))
+ .unwrap();
+
+ window.set_cursor_visible(false);
+
+ let mut state = state::State::new(&window).await;
+ let mut map = None;
+ let mut media = media::MediaMgr::new();
+
+ let mut nodedefs = None;
+
+ let mut last_frame = Instant::now();
+
+ event_loop.run_return(move |event, _, flow| match event {
+ MainEventsCleared => window.request_redraw(),
+ RedrawRequested(id) if id == window.id() => {
+ let now = Instant::now();
+ let dt = now - last_frame;
+ last_frame = now;
+
+ state.update(dt);
+ net_events
+ .send(NetEvent::PlayerPos(
+ state.camera.position.into(),
+ Rad(state.camera.yaw).into(),
+ Rad(state.camera.pitch).into(),
+ ))
+ .ok();
+
+ use wgpu::SurfaceError::*;
+ match state.render(&map) {
+ Ok(_) => {}
+ Err(Lost) => state.configure_surface(),
+ Err(OutOfMemory) => *flow = ExitWithCode(0),
+ Err(err) => eprintln!("gfx error: {err:?}"),
+ }
+ }
+ WindowEvent {
+ ref event,
+ window_id: id,
+ } if id == window.id() => match event {
+ CloseRequested => *flow = ExitWithCode(0),
+ Resized(size) => state.resize(*size),
+ ScaleFactorChanged { new_inner_size, .. } => state.resize(**new_inner_size),
+ KeyboardInput {
+ input:
+ winit::event::KeyboardInput {
+ virtual_keycode: Some(key),
+ state: key_state,
+ ..
+ },
+ ..
+ } => {
+ use fps_camera::Actions;
+ use winit::event::{ElementState::*, VirtualKeyCode as Key};
+
+ let actions = match key {
+ Key::W => Actions::MOVE_FORWARD,
+ Key::A => Actions::STRAFE_LEFT,
+ Key::S => Actions::MOVE_BACKWARD,
+ Key::D => Actions::STRAFE_RIGHT,
+ Key::Space => Actions::FLY_UP,
+ Key::LShift => Actions::FLY_DOWN,
+ _ => Actions::empty(),
+ };
+
+ match key_state {
+ Pressed => state.camera.enable_actions(actions),
+ Released => state.camera.disable_action(actions),
+ }
+ }
+ _ => {}
+ },
+ DeviceEvent {
+ event: MouseMotion { delta },
+ ..
+ } => {
+ state.camera.update_mouse(delta.0 as f32, delta.1 as f32);
+ window
+ .set_cursor_position(winit::dpi::PhysicalPosition::new(
+ state.config.width / 2,
+ state.config.height / 2,
+ ))
+ .ok();
+ }
+ UserEvent(event) => match event {
+ Close => *flow = ExitWithCode(0),
+ NodeDefs(defs) => nodedefs = Some(defs),
+ MapBlock(pos, blk) => {
+ if let Some(map) = map.as_mut() {
+ map.add_block(&mut state, pos, blk);
+ }
+ }
+ Media(files, finished) => {
+ media.add_server_media(files);
+
+ if finished {
+ map = Some(map::MapRender::new(
+ &mut state,
+ &media,
+ nodedefs.take().unwrap_or_default(),
+ ));
+
+ net_events.send(NetEvent::Ready).ok();
+ }
+ }
+ PlayerPos(pos, pitch, yaw) => {
+ state.camera.position = pos.into();
+ state.camera.pitch = Rad::<f32>::from(pitch).0;
+ state.camera.yaw = Rad::<f32>::from(yaw).0;
+ }
+ },
+ _ => {}
+ });
+}
diff --git a/src/gfx/map.rs b/src/gfx/map.rs
new file mode 100644
index 0000000..d95a4d2
--- /dev/null
+++ b/src/gfx/map.rs
@@ -0,0 +1,447 @@
+use super::{media::MediaMgr, state::State, util::MatrixUniform};
+use cgmath::{prelude::*, Matrix4, Point3, Vector3};
+use mt_net::{MapBlock, NodeDef};
+use rand::Rng;
+use std::{collections::HashMap, ops::Range};
+use wgpu::util::DeviceExt;
+
+pub struct MapRender {
+ pipeline: wgpu::RenderPipeline,
+ textures: HashMap<String, [Range<f32>; 2]>,
+ nodes: HashMap<u16, NodeDef>,
+ atlas: wgpu::BindGroup,
+ model: wgpu::BindGroupLayout,
+ blocks: HashMap<[i16; 3], BlockMesh>,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
+struct Vertex {
+ pos: [f32; 3],
+ tex_coords: [f32; 2],
+}
+
+impl Vertex {
+ const ATTRIBS: [wgpu::VertexAttribute; 2] =
+ wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2];
+
+ fn desc<'a>() -> wgpu::VertexBufferLayout<'a> {
+ wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &Self::ATTRIBS,
+ }
+ }
+}
+
+struct BlockMesh {
+ vertex_buffer: wgpu::Buffer,
+ num_vertices: u32,
+ model: MatrixUniform,
+}
+
+impl MapRender {
+ pub fn render<'a>(&'a self, state: &'a State, pass: &mut wgpu::RenderPass<'a>) {
+ pass.set_pipeline(&self.pipeline);
+ pass.set_bind_group(0, &self.atlas, &[]);
+ pass.set_bind_group(1, &state.camera_uniform.bind_group, &[]);
+
+ for mesh in self.blocks.values() {
+ pass.set_bind_group(2, &mesh.model.bind_group, &[]);
+ pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
+ pass.draw(0..mesh.num_vertices, 0..1);
+ }
+ }
+
+ pub fn add_block(&mut self, state: &mut State, pos: Point3<i16>, block: Box<MapBlock>) {
+ let mut vertices = Vec::with_capacity(10000);
+ for (index, content) in block.param_0.iter().enumerate() {
+ let def = match self.nodes.get(content) {
+ Some(x) => x,
+ None => continue,
+ };
+
+ use lerp::Lerp;
+ use mt_net::DrawType;
+ use std::array::from_fn as array;
+
+ match def.draw_type {
+ DrawType::Cube => {
+ let pos: [i16; 3] = array(|i| ((index >> (4 * i)) & 0xf) as i16);
+ for (f, face) in CUBE.iter().enumerate() {
+ let dir = FACE_DIR[f];
+ let npos: [i16; 3] = array(|i| dir[i] + pos[i]);
+ if npos.iter().all(|x| (0..16).contains(x)) {
+ let nindex = npos[0] | (npos[1] << 4) | (npos[2] << 8);
+
+ if let Some(ndef) = self.nodes.get(&block.param_0[nindex as usize]) {
+ if ndef.draw_type == DrawType::Cube {
+ continue;
+ }
+ }
+ }
+
+ let tile = &def.tiles[f];
+ let rect = self.textures.get(&tile.texture).unwrap();
+
+ for vertex in face.iter() {
+ /*println!(
+ "{:?} {:?} {:?} {:?}",
+ (vertex.1[0], vertex.1[1]),
+ (rect[0].start, rect[1].start),
+ (rect[0].end, rect[1].end),
+ (
+ vertex.1[0].lerp(rect[0].start, rect[0].end),
+ vertex.1[1].lerp(rect[1].start, rect[1].end)
+ )
+ );*/
+ vertices.push(Vertex {
+ pos: array(|i| pos[i] as f32 - 8.5 + vertex.0[i]),
+ tex_coords: array(|i| rect[i].start.lerp(rect[i].end, vertex.1[i])),
+ })
+ }
+ }
+ }
+ DrawType::None => {}
+ _ => {
+ // TODO
+ }
+ }
+ }
+
+ self.blocks.insert(
+ pos.into(),
+ BlockMesh {
+ vertex_buffer: state
+ .device
+ .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some("mapblock.vertex_buffer"),
+ contents: bytemuck::cast_slice(&vertices),
+ usage: wgpu::BufferUsages::VERTEX,
+ }),
+ num_vertices: vertices.len() as u32,
+ model: MatrixUniform::new(
+ &state.device,
+ &self.model,
+ Matrix4::from_translation(
+ pos.cast::<f32>().unwrap().to_vec() * 16.0 + Vector3::new(8.5, 8.5, 8.5),
+ ),
+ "mapblock",
+ false,
+ ),
+ },
+ );
+ }
+
+ pub fn new(state: &mut State, media: &MediaMgr, nodes: HashMap<u16, NodeDef>) -> Self {
+ let mut rng = rand::thread_rng();
+ let mut atlas_map = HashMap::new();
+ let mut atlas_alloc = guillotiere::SimpleAtlasAllocator::new(guillotiere::size2(1, 1));
+
+ for node in nodes.values() {
+ let tiles = node
+ .tiles
+ .iter()
+ .chain(node.overlay_tiles.iter())
+ .chain(node.special_tiles.iter());
+
+ let load_texture = |texture: &str| {
+ let payload = media
+ .get(texture)
+ .ok_or_else(|| format!("texture not found: {texture}"))?;
+
+ image::load_from_memory(payload)
+ .or_else(|_| {
+ image::load_from_memory_with_format(payload, image::ImageFormat::Tga)
+ })
+ .map_err(|e| format!("failed to load texture {texture}: {e}"))
+ .map(|x| image::imageops::flip_vertical(&x))
+ };
+
+ let mut make_texture = |texture: &str| {
+ texture
+ .split('^')
+ .map(|part| match load_texture(part) {
+ Ok(v) => v,
+ Err(e) => {
+ if !texture.is_empty() && !texture.contains('[') {
+ eprintln!("{e}");
+ }
+
+ let mut img = image::RgbImage::new(1, 1);
+ rng.fill(&mut img.get_pixel_mut(0, 0).0);
+
+ image::DynamicImage::from(img).to_rgba8()
+ }
+ })
+ .reduce(|mut base, top| {
+ image::imageops::overlay(&mut base, &top, 0, 0);
+ base
+ })
+ .unwrap()
+ };
+
+ for tile in tiles {
+ atlas_map.entry(tile.texture.clone()).or_insert_with(|| {
+ let img = make_texture(&tile.texture);
+
+ let dimensions = img.dimensions();
+ let size = guillotiere::size2(dimensions.0 as i32, dimensions.1 as i32);
+
+ loop {
+ match atlas_alloc.allocate(size) {
+ None => {
+ let mut atlas_size = atlas_alloc.size();
+ atlas_size.width *= 2;
+ atlas_size.height *= 2;
+ atlas_alloc.grow(atlas_size);
+ }
+ Some(v) => return (img, v),
+ }
+ }
+ });
+ }
+ }
+
+ let atlas_size = atlas_alloc.size();
+ let mut atlas = image::RgbaImage::new(atlas_size.width as u32, atlas_size.height as u32);
+
+ let textures = atlas_map
+ .into_iter()
+ .map(|(name, (img, rect))| {
+ let w = atlas_size.width as f32;
+ let h = atlas_size.height as f32;
+
+ let x = (rect.min.x as f32 / w)..(rect.max.x as f32 / w);
+ let y = (rect.min.y as f32 / h)..(rect.max.y as f32 / h);
+
+ use image::GenericImage;
+ atlas
+ .copy_from(&img, rect.min.x as u32, rect.min.y as u32)
+ .unwrap();
+
+ (name, [x, y])
+ })
+ .collect();
+
+ let size = wgpu::Extent3d {
+ width: atlas_size.width as u32,
+ height: atlas_size.height as u32,
+ depth_or_array_layers: 1,
+ };
+
+ let atlas_texture = state.device.create_texture(&wgpu::TextureDescriptor {
+ size,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8UnormSrgb,
+ usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
+ label: Some("tile_atlas"),
+ view_formats: &[],
+ });
+
+ state.queue.write_texture(
+ wgpu::ImageCopyTexture {
+ texture: &atlas_texture,
+ mip_level: 0,
+ origin: wgpu::Origin3d::ZERO,
+ aspect: wgpu::TextureAspect::All,
+ },
+ &atlas,
+ wgpu::ImageDataLayout {
+ offset: 0,
+ bytes_per_row: std::num::NonZeroU32::new(4 * atlas_size.width as u32),
+ rows_per_image: std::num::NonZeroU32::new(atlas_size.height as u32),
+ },
+ size,
+ );
+
+ let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let atlas_sampler = state.device.create_sampler(&wgpu::SamplerDescriptor {
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ // "We've got you surrounded, stop using Nearest filter"
+ // - "I hate bilinear filtering I hate bilinear filtering I hate bilinear filtering"
+ mag_filter: wgpu::FilterMode::Nearest,
+ min_filter: wgpu::FilterMode::Nearest,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ ..Default::default()
+ });
+
+ let atlas_bind_group_layout =
+ state
+ .device
+ .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Texture {
+ multisampled: false,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
+ count: None,
+ },
+ ],
+ label: Some("atlas.bind_group_layout"),
+ });
+
+ let atlas_bind_group = state.device.create_bind_group(&wgpu::BindGroupDescriptor {
+ layout: &atlas_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(&atlas_view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::Sampler(&atlas_sampler),
+ },
+ ],
+ label: Some("atlas.bind_group"),
+ });
+
+ let model_bind_group_layout = MatrixUniform::layout(&state.device, "mapblock");
+
+ let shader = state
+ .device
+ .create_shader_module(wgpu::include_wgsl!("../../assets/shaders/map.wgsl"));
+
+ let pipeline_layout =
+ state
+ .device
+ .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: None,
+ bind_group_layouts: &[
+ &atlas_bind_group_layout,
+ &model_bind_group_layout,
+ &state.camera_bind_group_layout,
+ ],
+ push_constant_ranges: &[],
+ });
+
+ let pipeline = state
+ .device
+ .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: None,
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: "vs_main",
+ buffers: &[Vertex::desc()],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: "fs_main",
+ targets: &[Some(wgpu::ColorTargetState {
+ format: state.config.format,
+ blend: Some(wgpu::BlendState::REPLACE),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: Some(wgpu::Face::Back),
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: Some(wgpu::DepthStencilState {
+ format: wgpu::TextureFormat::Depth32Float,
+ depth_write_enabled: true,
+ depth_compare: wgpu::CompareFunction::Less,
+ stencil: wgpu::StencilState::default(),
+ bias: wgpu::DepthBiasState::default(),
+ }),
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ });
+
+ Self {
+ pipeline,
+ nodes,
+ textures,
+ atlas: atlas_bind_group,
+ model: model_bind_group_layout,
+ blocks: HashMap::new(),
+ }
+ }
+}
+
+#[rustfmt::skip]
+const CUBE: [[([f32; 3], [f32; 2]); 6]; 6] = [
+ [
+ ([-0.5, 0.5, -0.5], [ 0.0, 1.0]),
+ ([ 0.5, 0.5, 0.5], [ 1.0, 0.0]),
+ ([ 0.5, 0.5, -0.5], [ 1.0, 1.0]),
+ ([ 0.5, 0.5, 0.5], [ 1.0, 0.0]),
+ ([-0.5, 0.5, -0.5], [ 0.0, 1.0]),
+ ([-0.5, 0.5, 0.5], [ 0.0, 0.0]),
+ ],
+ [
+ ([-0.5, -0.5, -0.5], [ 0.0, 1.0]),
+ ([ 0.5, -0.5, -0.5], [ 1.0, 1.0]),
+ ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]),
+ ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]),
+ ([-0.5, -0.5, 0.5], [ 0.0, 0.0]),
+ ([-0.5, -0.5, -0.5], [ 0.0, 1.0]),
+ ],
+ [
+ ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]),
+ ([ 0.5, -0.5, -0.5], [ 0.0, 0.0]),
+ ([ 0.5, 0.5, -0.5], [ 0.0, 1.0]),
+ ([ 0.5, -0.5, -0.5], [ 0.0, 0.0]),
+ ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]),
+ ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]),
+ ],
+ [
+ ([-0.5, 0.5, 0.5], [ 1.0, 1.0]),
+ ([-0.5, 0.5, -0.5], [ 0.0, 1.0]),
+ ([-0.5, -0.5, -0.5], [ 0.0, 0.0]),
+ ([-0.5, -0.5, -0.5], [ 0.0, 0.0]),
+ ([-0.5, -0.5, 0.5], [ 1.0, 0.0]),
+ ([-0.5, 0.5, 0.5], [ 1.0, 1.0]),
+ ],
+ [
+ ([-0.5, -0.5, 0.5], [ 0.0, 0.0]),
+ ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]),
+ ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]),
+ ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]),
+ ([-0.5, 0.5, 0.5], [ 0.0, 1.0]),
+ ([-0.5, -0.5, 0.5], [ 0.0, 0.0]),
+ ],
+ [
+ ([-0.5, -0.5, -0.5], [ 0.0, 0.0]),
+ ([ 0.5, 0.5, -0.5], [ 1.0, 1.0]),
+ ([ 0.5, -0.5, -0.5], [ 1.0, 0.0]),
+ ([ 0.5, 0.5, -0.5], [ 1.0, 1.0]),
+ ([-0.5, -0.5, -0.5], [ 0.0, 0.0]),
+ ([-0.5, 0.5, -0.5], [ 0.0, 1.0]),
+ ],
+];
+
+#[rustfmt::skip]
+const FACE_DIR: [[i16; 3]; 6] = [
+ [ 0, 1, 0],
+ [ 0, -1, 0],
+ [ 1, 0, 0],
+ [-1, 0, 0],
+ [ 0, 0, 1],
+ [ 0, 0, -1],
+];
diff --git a/src/gfx/media.rs b/src/gfx/media.rs
new file mode 100644
index 0000000..ac5d158
--- /dev/null
+++ b/src/gfx/media.rs
@@ -0,0 +1,42 @@
+use std::collections::HashMap;
+
+#[derive(rust_embed::RustEmbed)]
+#[folder = "assets/textures"]
+pub struct BaseFolder; // copied from github.com/minetest/minetest
+
+pub struct MediaMgr {
+ packs: Vec<HashMap<String, Vec<u8>>>,
+ srv_idx: usize,
+}
+
+impl MediaMgr {
+ pub fn new() -> Self {
+ Self {
+ packs: [
+ BaseFolder::iter()
+ .map(|file| {
+ (
+ file.to_string(),
+ BaseFolder::get(&file).unwrap().data.into_owned(),
+ )
+ })
+ .collect(),
+ HashMap::new(),
+ ]
+ .into(),
+ srv_idx: 1,
+ }
+ }
+
+ pub fn add_server_media(&mut self, files: HashMap<String, Vec<u8>>) {
+ self.packs[self.srv_idx].extend(files.into_iter());
+ }
+
+ pub fn get(&self, file: &str) -> Option<&[u8]> {
+ self.packs
+ .iter()
+ .rev()
+ .find_map(|pack| pack.get(file))
+ .map(Vec::as_slice)
+ }
+}
diff --git a/src/gfx/state.rs b/src/gfx/state.rs
new file mode 100644
index 0000000..9d1cfd1
--- /dev/null
+++ b/src/gfx/state.rs
@@ -0,0 +1,233 @@
+use super::util::MatrixUniform;
+use cgmath::{prelude::*, Deg, Matrix4, Rad};
+use fps_camera::{FirstPerson, FirstPersonSettings};
+use std::time::Duration;
+
+pub struct State {
+ pub surface: wgpu::Surface,
+ pub device: wgpu::Device,
+ pub queue: wgpu::Queue,
+ pub config: wgpu::SurfaceConfiguration,
+ pub fov: Rad<f32>,
+ pub view: Matrix4<f32>,
+ pub proj: Matrix4<f32>,
+ pub camera: FirstPerson,
+ pub camera_uniform: MatrixUniform,
+ pub camera_bind_group_layout: wgpu::BindGroupLayout,
+ pub depth_texture: wgpu::Texture,
+ pub depth_view: wgpu::TextureView,
+ pub depth_sampler: wgpu::Sampler,
+}
+
+impl State {
+ pub async fn new(window: &winit::window::Window) -> Self {
+ let size = window.inner_size();
+
+ let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
+ backends: wgpu::Backends::all(),
+ dx12_shader_compiler: Default::default(),
+ });
+
+ let surface = unsafe { instance.create_surface(window) }.unwrap();
+
+ let adapter = instance
+ .request_adapter(&wgpu::RequestAdapterOptions {
+ power_preference: wgpu::PowerPreference::default(),
+ compatible_surface: Some(&surface),
+ force_fallback_adapter: false,
+ })
+ .await
+ .unwrap();
+
+ let (device, queue) = adapter
+ .request_device(
+ &wgpu::DeviceDescriptor {
+ features: wgpu::Features::empty(),
+ limits: Default::default(),
+ label: None,
+ },
+ None,
+ )
+ .await
+ .unwrap();
+
+ let surface_caps = surface.get_capabilities(&adapter);
+ let surface_format = surface_caps
+ .formats
+ .iter()
+ .copied()
+ .find(|f| f.describe().srgb)
+ .unwrap_or(surface_caps.formats[0]);
+
+ let config = wgpu::SurfaceConfiguration {
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
+ format: surface_format,
+ width: size.width,
+ height: size.height,
+ present_mode: surface_caps.present_modes[0],
+ alpha_mode: surface_caps.alpha_modes[0],
+ view_formats: vec![],
+ };
+
+ let (depth_texture, depth_view, depth_sampler) =
+ Self::create_depth_texture(&config, &device);
+
+ let camera = FirstPerson::new(
+ [0.0, 0.0, 0.0],
+ FirstPersonSettings {
+ speed_horizontal: 10.0,
+ speed_vertical: 10.0,
+ mouse_sensitivity_horizontal: 1.0,
+ mouse_sensitivity_vertical: 1.0,
+ },
+ );
+
+ let camera_bind_group_layout = MatrixUniform::layout(&device, "camera");
+
+ let camera_uniform = MatrixUniform::new(
+ &device,
+ &camera_bind_group_layout,
+ Matrix4::identity(),
+ "camera",
+ true,
+ );
+
+ let mut state = Self {
+ surface,
+ device,
+ queue,
+ config,
+ fov: Deg(90.0).into(),
+ proj: Matrix4::identity(),
+ view: Matrix4::identity(),
+ camera,
+ camera_uniform,
+ camera_bind_group_layout,
+ depth_texture,
+ depth_view,
+ depth_sampler,
+ };
+
+ state.resize(size);
+
+ state
+ }
+
+ pub fn create_depth_texture(
+ config: &wgpu::SurfaceConfiguration,
+ device: &wgpu::Device,
+ ) -> (wgpu::Texture, wgpu::TextureView, wgpu::Sampler) {
+ let depth_size = wgpu::Extent3d {
+ width: config.width,
+ height: config.height,
+ depth_or_array_layers: 1,
+ };
+ let depth_descriptor = wgpu::TextureDescriptor {
+ label: Some("depth texture"),
+ size: depth_size,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT // 3.
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ };
+ let depth_texture = device.create_texture(&depth_descriptor);
+
+ let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
+ let depth_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ address_mode_w: wgpu::AddressMode::ClampToEdge,
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ mipmap_filter: wgpu::FilterMode::Nearest,
+ compare: Some(wgpu::CompareFunction::LessEqual),
+ lod_min_clamp: 0.0,
+ lod_max_clamp: 100.0,
+ ..Default::default()
+ });
+
+ (depth_texture, depth_view, depth_sampler)
+ }
+
+ pub fn resize(&mut self, size: winit::dpi::PhysicalSize<u32>) {
+ if size.width > 0 && size.height > 0 {
+ self.config.width = size.width;
+ self.config.height = size.height;
+ self.configure_surface();
+ self.update_projection();
+ (self.depth_texture, self.depth_view, self.depth_sampler) =
+ Self::create_depth_texture(&self.config, &self.device);
+ }
+ }
+
+ pub fn configure_surface(&mut self) {
+ self.surface.configure(&self.device, &self.config);
+ }
+
+ pub fn update_projection(&mut self) {
+ self.proj = cgmath::perspective(
+ self.fov,
+ self.config.width as f32 / self.config.height as f32,
+ 0.1,
+ 100000.0,
+ );
+ }
+
+ pub fn update(&mut self, dt: Duration) {
+ let cam = self.camera.camera(dt.as_secs_f32());
+ self.camera.position = cam.position;
+ self.view = Matrix4::from(cam.orthogonal());
+
+ self.camera_uniform.set(&self.queue, self.proj * self.view);
+ }
+
+ pub fn render(&self, map: &Option<super::map::MapRender>) -> Result<(), wgpu::SurfaceError> {
+ let output = self.surface.get_current_texture()?;
+ let view = output
+ .texture
+ .create_view(&wgpu::TextureViewDescriptor::default());
+
+ let mut encoder = self
+ .device
+ .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
+
+ {
+ let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: None,
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: &view,
+ resolve_target: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color {
+ r: 0x87 as f64 / 255.0,
+ g: 0xCE as f64 / 255.0,
+ b: 0xEB as f64 / 255.0,
+ a: 1.0,
+ }),
+ store: true,
+ },
+ })],
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+ view: &self.depth_view,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(1.0),
+ store: true,
+ }),
+ stencil_ops: None,
+ }),
+ });
+
+ if let Some(map) = map.as_ref() {
+ map.render(self, &mut render_pass);
+ }
+ }
+
+ self.queue.submit(std::iter::once(encoder.finish()));
+ output.present();
+
+ Ok(())
+ }
+}
diff --git a/src/gfx/util.rs b/src/gfx/util.rs
new file mode 100644
index 0000000..f93accd
--- /dev/null
+++ b/src/gfx/util.rs
@@ -0,0 +1,63 @@
+use cgmath::Matrix4;
+use wgpu::util::DeviceExt;
+
+pub struct MatrixUniform {
+ buffer: wgpu::Buffer,
+ pub bind_group: wgpu::BindGroup,
+}
+
+impl MatrixUniform {
+ pub fn new(
+ device: &wgpu::Device,
+ bind_group_layout: &wgpu::BindGroupLayout,
+ init: Matrix4<f32>,
+ name: &str,
+ writable: bool,
+ ) -> Self {
+ let uniform: [[f32; 4]; 4] = init.into();
+
+ let mut usage = wgpu::BufferUsages::UNIFORM;
+
+ if writable {
+ usage |= wgpu::BufferUsages::COPY_DST;
+ }
+
+ let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+ label: Some(&format!("{name}.buffer")),
+ contents: bytemuck::cast_slice(&[uniform]),
+ usage,
+ });
+
+ let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ layout: bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: buffer.as_entire_binding(),
+ }],
+ label: Some(&format!("{name}.bind_group")),
+ });
+
+ Self { buffer, bind_group }
+ }
+
+ pub fn layout(device: &wgpu::Device, name: &str) -> wgpu::BindGroupLayout {
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ count: None,
+ }],
+ label: Some(&format!("{name}.bind_group_layout")),
+ })
+ }
+
+ pub fn set(&self, queue: &wgpu::Queue, to: Matrix4<f32>) {
+ let uniform: [[f32; 4]; 4] = to.into();
+ queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[uniform]));
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 5b8817e..f517ee2 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,124 +1,46 @@
-use mt_net::{MtReceiver, MtSender, RemoteSrv, ToCltPkt, ToSrvPkt};
-use rand::RngCore;
-use sha2::Sha256;
-use srp::{client::SrpClient, groups::G_2048};
-use std::time::Duration;
-use tokio::sync::oneshot;
-
-async fn handle(tx: MtSender<RemoteSrv>, rx: &mut MtReceiver<RemoteSrv>) {
- let mut username = "hydra".to_string();
- let password = "password";
-
- let (init_tx, mut init_rx) = oneshot::channel();
-
- {
- let tx = tx.clone();
- let pkt = ToSrvPkt::Init {
- serialize_version: 29,
- min_proto_version: 40,
- max_proto_version: 40,
- player_name: username.clone(),
- send_full_item_meta: false,
- };
-
- tokio::spawn(async move {
- let mut interval = tokio::time::interval(Duration::from_millis(100));
- while tokio::select! {
- _ = &mut init_rx => false,
- _ = interval.tick() => true,
- } {
- tx.send(&pkt).await.unwrap()
- }
- });
- }
-
- let mut init_tx = Some(init_tx);
- let mut auth = None;
-
- while let Some(res) = rx.recv().await {
- match res {
- Ok(pkt) => {
- use ToCltPkt::*;
-
- match pkt {
- Hello {
- auth_methods,
- username: name,
- ..
- } => {
- use mt_net::AuthMethod;
-
- if let Some(chan) = init_tx.take() {
- chan.send(()).unwrap();
-
- let client = SrpClient::<Sha256>::new(&G_2048);
-
- let mut rand_bytes = vec![0; 32];
- rand::thread_rng().fill_bytes(&mut rand_bytes);
-
- username = name;
+mod gfx;
+mod net;
+
+use cgmath::{Deg, Point3};
+use std::collections::HashMap;
+use tokio::sync::mpsc;
+
+#[derive(Debug, Clone)]
+pub enum GfxEvent {
+ Close,
+ Media(HashMap<String, Vec<u8>>, bool),
+ NodeDefs(HashMap<u16, mt_net::NodeDef>),
+ MapBlock(Point3<i16>, Box<mt_net::MapBlock>),
+ PlayerPos(Point3<f32>, Deg<f32>, Deg<f32>),
+}
- if auth_methods.contains(AuthMethod::FirstSrp) {
- let verifier = client.compute_verifier(
- username.to_lowercase().as_bytes(),
- password.as_bytes(),
- &rand_bytes,
- );
+#[derive(Debug, Clone)]
+pub enum NetEvent {
+ PlayerPos(Point3<f32>, Deg<f32>, Deg<f32>),
+ Ready,
+}
- tx.send(&ToSrvPkt::FirstSrp {
- salt: rand_bytes,
- verifier,
- empty_passwd: password.is_empty(),
- })
- .await
- .unwrap();
- } else if auth_methods.contains(AuthMethod::Srp) {
- let a = client.compute_public_ephemeral(&rand_bytes);
- auth = Some((rand_bytes, client));
+fn main() {
+ println!(include_str!("../assets/ascii-art.txt"));
+ println!("Early WIP. Expext breakage. Trans rights <3");
- tx.send(&ToSrvPkt::SrpBytesA { a, no_sha1: true })
- .await
- .unwrap();
- } else {
- panic!("unsupported auth methods: {auth_methods:?}");
- }
- }
- }
- SrpBytesSaltB { salt, b } => {
- if let Some((a, client)) = auth.take() {
- let m = client
- .process_reply(
- &a,
- username.to_lowercase().as_bytes(),
- password.as_bytes(),
- &salt,
- &b,
- )
- .unwrap()
- .proof()
- .into();
+ let (net_tx, net_rx) = mpsc::unbounded_channel();
+ let event_loop = winit::event_loop::EventLoopBuilder::<GfxEvent>::with_user_event().build();
+ let event_loop_proxy = event_loop.create_proxy();
- tx.send(&ToSrvPkt::SrpBytesM { m }).await.unwrap();
- }
- }
- x => println!("{x:?}"),
- }
- }
- Err(err) => eprintln!("{err}"),
- }
- }
-}
+ let runtime = tokio::runtime::Builder::new_multi_thread()
+ .enable_io()
+ .enable_time()
+ .thread_name("network")
+ .build()
+ .unwrap();
-#[tokio::main]
-async fn main() {
- let (tx, mut rx) = mt_net::connect("localhost:30000").await.unwrap();
+ let net_thread = runtime.spawn(net::run(event_loop_proxy, net_rx));
- tokio::select! {
- _ = tokio::signal::ctrl_c() => println!("canceled"),
- _ = handle(tx, &mut rx) => {
- println!("disconnected");
- }
- }
+ // graphics code is pseudo async: the winit event loop is blocking
+ // so we can't really use async capabilities
+ futures::executor::block_on(gfx::run(event_loop, net_tx));
- rx.close().await;
+ // wait for net to finish
+ runtime.block_on(net_thread).unwrap();
}
diff --git a/src/net.rs b/src/net.rs
new file mode 100644
index 0000000..323fe06
--- /dev/null
+++ b/src/net.rs
@@ -0,0 +1,252 @@
+use crate::{GfxEvent, NetEvent};
+use cgmath::{Deg, Point3, Vector3};
+use futures::future::OptionFuture;
+use mt_net::{CltSender, ReceiverExt, SenderExt, ToCltPkt, ToSrvPkt};
+use rand::RngCore;
+use sha2::Sha256;
+use srp::{client::SrpClient, groups::G_2048};
+use std::{future::Future, time::Duration};
+use tokio::{
+ sync::mpsc,
+ time::{interval, Instant, Interval},
+};
+use winit::event_loop::EventLoopProxy;
+
+enum AuthState {
+ Init(Interval),
+ Verify(Vec<u8>, SrpClient<'static, Sha256>),
+ Done,
+}
+
+struct Conn {
+ tx: CltSender,
+ auth: AuthState,
+ send_pos_iv: Option<Interval>,
+ username: String,
+ password: String,
+ pos: Point3<f32>,
+ pitch: Deg<f32>,
+ yaw: Deg<f32>,
+ events: EventLoopProxy<GfxEvent>,
+}
+
+fn maybe_tick(iv: Option<&mut Interval>) -> OptionFuture<impl Future<Output = Instant> + '_> {
+ OptionFuture::from(iv.map(Interval::tick))
+}
+
+pub(crate) async fn run(
+ evt_out: EventLoopProxy<GfxEvent>,
+ mut evt_in: mpsc::UnboundedReceiver<NetEvent>,
+) {
+ let (tx, mut rx, worker) = mt_net::connect("localhost:30000").await.unwrap();
+
+ let mut conn = Conn {
+ tx,
+ auth: AuthState::Init(interval(Duration::from_millis(100))),
+ send_pos_iv: None,
+ username: "shrek".into(), // shrek is love, shrek is life <3
+ password: "boobies".into(),
+ pos: Point3::new(0.0, 0.0, 0.0),
+ pitch: Deg(0.0),
+ yaw: Deg(0.0),
+ events: evt_out,
+ };
+
+ let init_pkt = ToSrvPkt::Init {
+ serialize_version: 29,
+ proto_version: 40..=40,
+ player_name: conn.username.clone(),
+ send_full_item_meta: false,
+ };
+
+ let worker_thread = tokio::spawn(worker.run());
+
+ loop {
+ tokio::select! {
+ pkt = rx.recv() => match pkt {
+ None => break,
+ Some(Err(e)) => eprintln!("{e}"),
+ Some(Ok(v)) => conn.handle_pkt(v).await,
+ },
+ Some(_) = maybe_tick(match &mut conn.auth {
+ AuthState::Init(iv) => Some(iv),
+ _ => None,
+ }) => {
+ conn.tx.send(&init_pkt).await.unwrap();
+ }
+ Some(_) = maybe_tick(conn.send_pos_iv.as_mut()) => {
+ conn.tx
+ .send(&ToSrvPkt::PlayerPos(mt_net::PlayerPos {
+ pos: conn.pos,
+ vel: Vector3::new(0.0, 0.0, 0.0),
+ pitch: conn.pitch,
+ yaw: conn.yaw,
+ keys: mt_net::enumset::EnumSet::empty(),
+ fov: Deg(90.0).into(),
+ wanted_range: 12,
+ }))
+ .await
+ .unwrap();
+ }
+ evt = evt_in.recv() => {
+ match evt {
+ Some(NetEvent::PlayerPos(pos, yaw, pitch)) => {
+ conn.pos = pos;
+ conn.yaw = yaw;
+ conn.pitch = pitch;
+ },
+ Some(NetEvent::Ready) => {
+ conn.tx
+ .send(&ToSrvPkt::CltReady {
+ major: 0,
+ minor: 1,
+ patch: 0,
+ reserved: 0,
+ version: format!("Minetest Rust {}", env!("CARGO_PKG_VERSION")),
+ formspec: 4,
+ })
+ .await
+ .unwrap();
+ }
+ None => conn.tx.close(),
+ }
+ }
+ _ = tokio::signal::ctrl_c() => {
+ conn.tx.close();
+ }
+ }
+ }
+
+ conn.events.send_event(GfxEvent::Close).ok(); // TODO: make sure to send this on panic
+ worker_thread.await.unwrap();
+}
+
+impl Conn {
+ async fn handle_pkt(&mut self, pkt: ToCltPkt) {
+ use ToCltPkt::*;
+
+ match pkt {
+ Hello {
+ auth_methods,
+ username: name,
+ ..
+ } => {
+ use mt_net::AuthMethod;
+
+ if !matches!(self.auth, AuthState::Init(_)) {
+ return;
+ }
+
+ let srp = SrpClient::<Sha256>::new(&G_2048);
+
+ let mut rand_bytes = vec![0; 32];
+ rand::thread_rng().fill_bytes(&mut rand_bytes);
+
+ if self.username != name {
+ panic!("username changed");
+ }
+
+ if auth_methods.contains(AuthMethod::FirstSrp) {
+ let verifier = srp.compute_verifier(
+ self.username.to_lowercase().as_bytes(),
+ self.password.as_bytes(),
+ &rand_bytes,
+ );
+
+ self.tx
+ .send(&ToSrvPkt::FirstSrp {
+ salt: rand_bytes,
+ verifier,
+ empty_passwd: self.password.is_empty(),
+ })
+ .await
+ .unwrap();
+
+ self.auth = AuthState::Done;
+ } else if auth_methods.contains(AuthMethod::Srp) {
+ let a = srp.compute_public_ephemeral(&rand_bytes);
+
+ self.tx
+ .send(&ToSrvPkt::SrpBytesA { a, no_sha1: true })
+ .await
+ .unwrap();
+
+ self.auth = AuthState::Verify(rand_bytes, srp);
+ } else {
+ panic!("unsupported auth methods: {auth_methods:?}");
+ }
+ }
+ SrpBytesSaltB { salt, b } => {
+ if let AuthState::Verify(a, srp) = &self.auth {
+ let m = srp
+ .process_reply(
+ a,
+ self.username.to_lowercase().as_bytes(),
+ self.password.as_bytes(),
+ &salt,
+ &b,
+ )
+ .unwrap()
+ .proof()
+ .into();
+
+ self.tx.send(&ToSrvPkt::SrpBytesM { m }).await.unwrap();
+
+ self.auth = AuthState::Done;
+ }
+ }
+ NodeDefs(defs) => {
+ self.events.send_event(GfxEvent::NodeDefs(defs.0)).ok();
+ }
+ Kick(reason) => {
+ println!("kicked: {reason}");
+ }
+ AcceptAuth { player_pos, .. } => {
+ self.tx
+ .send(&ToSrvPkt::Init2 {
+ lang: "en_US".into(), // localization is unironically overrated
+ })
+ .await
+ .unwrap();
+
+ self.pos = player_pos;
+ self.send_pos_iv = Some(interval(Duration::from_millis(100)));
+ }
+ MovePlayer { pos, pitch, yaw } => {
+ self.pos = pos;
+ self.pitch = pitch;
+ self.yaw = yaw;
+
+ self.events
+ .send_event(GfxEvent::PlayerPos(self.pos, self.pitch, self.yaw))
+ .ok();
+ }
+ BlockData { pos, block } => {
+ self.events.send_event(GfxEvent::MapBlock(pos, block)).ok();
+ self.tx
+ .send(&ToSrvPkt::GotBlocks {
+ blocks: Vec::from([pos]),
+ })
+ .await
+ .unwrap();
+ }
+ AnnounceMedia { files, .. } => {
+ self.tx
+ .send(&ToSrvPkt::RequestMedia {
+ filenames: files.into_keys().collect(), // TODO: cache
+ })
+ .await
+ .ok();
+ }
+ Media { files, n, i } => {
+ self.events
+ .send_event(GfxEvent::Media(files, i + 1 == n))
+ .ok();
+ }
+ ChatMsg { text, .. } => {
+ println!("{text}");
+ }
+ _ => {}
+ }
+ }
+}