diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | light-client/Cargo.toml | 1 | ||||
| -rw-r--r-- | light-client/src/atlas.rs | 154 | ||||
| -rw-r--r-- | light-client/src/game.rs | 36 | ||||
| -rw-r--r-- | light-client/src/main.rs | 44 | ||||
| -rw-r--r-- | light-client/src/network.rs | 48 | ||||
| -rw-r--r-- | light-client/textures/makefile | 4 | 
7 files changed, 256 insertions, 32 deletions
| @@ -996,6 +996,7 @@ checksum = "3e281a65eeba3d4503a2839252f86374528f9ceafe6fed97c1d3b52e1fb625c1"  name = "light-client"  version = "0.1.0"  dependencies = [ + "anyhow",   "bincode",   "env_logger",   "hurrycurry-protocol", diff --git a/light-client/Cargo.toml b/light-client/Cargo.toml index ad50130c..534ab300 100644 --- a/light-client/Cargo.toml +++ b/light-client/Cargo.toml @@ -11,3 +11,4 @@ serde_json = "1.0.120"  bincode = "2.0.0-rc.3"  log = "0.4.22"  env_logger = "0.11.3" +anyhow = "1.0.86" diff --git a/light-client/src/atlas.rs b/light-client/src/atlas.rs new file mode 100644 index 00000000..e75430c5 --- /dev/null +++ b/light-client/src/atlas.rs @@ -0,0 +1,154 @@ +use hurrycurry_protocol::{ +    glam::{IVec2, Vec2}, +    ClientGamedata, ItemIndex, TileIndex, +}; +use sdl2::{ +    pixels::PixelFormatEnum, +    rect::Rect, +    render::{Canvas, Texture, TextureAccess, TextureCreator}, +    video::{Window, WindowContext}, +}; +use std::collections::HashMap; + +pub struct SpriteRenderer<'a> { +    texture: Texture<'a>, + +    tiles: Vec<Rect>, +    items: Vec<Rect>, + +    view_scale: u32, +    view_offset: Vec2, + +    sprites: Vec<DrawItem>, +} + +pub struct DrawItem { +    z_order: i32, +    src: Rect, +    dst: Rect, +} + +impl<'a> SpriteRenderer<'a> { +    pub fn init(texture_creator: &'a TextureCreator<WindowContext>) -> Self { +        let palette = include_str!("../textures/palette.csv") +            .split('\n') +            .filter(|l| !l.is_empty()) +            .map(|s| { +                let mut toks = s.split(","); +                ( +                    toks.next().unwrap().chars().next().unwrap(), +                    [ +                        toks.next().unwrap().parse::<u8>().unwrap(), +                        toks.next().unwrap().parse::<u8>().unwrap(), +                        toks.next().unwrap().parse::<u8>().unwrap(), +                        toks.next().unwrap().parse::<u8>().unwrap(), +                    ], +                ) +            }) +            .collect::<HashMap<_, _>>(); + +        let mut texels = vec![255; 1024 * 1024 * 4]; + +        for (y, line) in include_str!("../textures/atlas.ta").lines().enumerate() { +            if line.is_empty() { +                continue; +            } +            for (x, char) in line.chars().enumerate() { +                let color = palette.get(&char).unwrap(); +                let base = (y * 1024 + x) * 4; +                texels[base..base + 4].copy_from_slice(color); +            } +        } + +        let mut texture = texture_creator +            .create_texture( +                Some(PixelFormatEnum::RGBA8888), +                TextureAccess::Streaming, +                1024, +                1024, +            ) +            .unwrap(); + +        texture.update(None, &texels, 1024 * 4).unwrap(); + +        Self { +            texture, +            items: vec![], +            tiles: vec![], +            sprites: vec![], +            view_offset: Vec2::ZERO, +            view_scale: 32, +        } +    } + +    pub fn set_sprite_map(&mut self, data: ClientGamedata) { +        let meta = include_str!("../textures/atlas.meta.csv") +            .lines() +            .filter(|l| !l.is_empty()) +            .map(|l| { +                let mut toks = l.split(","); +                let x: i32 = toks.next().unwrap().parse().unwrap(); +                let y: i32 = toks.next().unwrap().parse().unwrap(); +                let w: u32 = toks.next().unwrap().parse().unwrap(); +                let h: u32 = toks.next().unwrap().parse().unwrap(); +                let name = toks.next().unwrap().to_string(); +                (name, Rect::new(x, y, w, h)) +            }) +            .collect::<HashMap<_, _>>(); + +        self.items = data +            .item_names +            .iter() +            .map(|i| meta.get(i).copied().unwrap_or(Rect::new(0, 0, 100, 100))) +            .collect(); +        self.tiles = data +            .tile_names +            .iter() +            .map(|i| meta.get(i).copied().unwrap_or(Rect::new(0, 0, 100, 100))) +            .collect(); +    } + +    pub fn draw_tile(&mut self, TileIndex(i): TileIndex, position: IVec2) { +        let p = (self.view_offset.as_ivec2() + position) * self.view_scale as i32; +        self.sprites.push(DrawItem { +            z_order: position.y, +            src: self.tiles[i], +            dst: Rect::from_center((p.x as i32, p.y as i32), self.view_scale, self.view_scale), +        }); +    } +    pub fn draw_item(&mut self, ItemIndex(i): ItemIndex, position: Vec2) { +        self.sprites.push(DrawItem { +            z_order: position.y as i32, +            src: self.tiles[i], +            dst: Rect::from_center( +                (position.x as i32, position.y as i32), +                self.view_scale, +                self.view_scale, +            ), +        }) +    } + +    pub fn submit(&mut self, canvas: &mut Canvas<Window>) { +        self.sprites.sort(); +        for DrawItem { src, dst, .. } in self.sprites.drain(..) { +            canvas.copy(&self.texture, src, dst).unwrap(); +        } +    } +} + +impl Ord for DrawItem { +    fn cmp(&self, other: &Self) -> std::cmp::Ordering { +        self.z_order.cmp(&other.z_order) +    } +} +impl PartialOrd for DrawItem { +    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { +        Some(self.cmp(&other)) +    } +} +impl Eq for DrawItem {} +impl PartialEq for DrawItem { +    fn eq(&self, other: &Self) -> bool { +        self.z_order == other.z_order && self.src == other.src && self.dst == other.dst +    } +} diff --git a/light-client/src/game.rs b/light-client/src/game.rs index fd045f53..e432c517 100644 --- a/light-client/src/game.rs +++ b/light-client/src/game.rs @@ -1,4 +1,38 @@ +use crate::atlas::SpriteRenderer; +use hurrycurry_protocol::{glam::IVec2, PacketC, TileIndex}; +use std::collections::HashMap;  pub struct Game { -     +    tiles: HashMap<IVec2, TileIndex>, +} + +impl Game { +    pub fn new() -> Self { +        Self { +            tiles: HashMap::new(), +        } +    } + +    pub fn packet_in(&mut self, packet: PacketC) { +        match packet { +            PacketC::UpdateMap { +                tile, +                kind, +                neighbors: _, +            } => { +                if let Some(kind) = kind { +                    self.tiles.insert(tile, kind); +                } else { +                    self.tiles.remove(&tile); +                } +            } +            _ => (), +        } +    } + +    pub fn render(&self, ctx: &mut SpriteRenderer) { +        for (p, tile) in &self.tiles { +            ctx.draw_tile(*tile, *p) +        } +    }  } diff --git a/light-client/src/main.rs b/light-client/src/main.rs index cb7e6caf..2145e9b6 100644 --- a/light-client/src/main.rs +++ b/light-client/src/main.rs @@ -1,3 +1,6 @@ +use atlas::SpriteRenderer; +use game::Game; +use hurrycurry_protocol::PacketC;  /*      Hurry Curry! - a game about cooking      Copyright 2024 metamuffin @@ -16,23 +19,15 @@  */  use network::Network; -use sdl2::{ -    event::Event, -    image::InitFlag, -    keyboard::Keycode, -    pixels::{Color, PixelFormatEnum}, -    render::TextureAccess, -}; +use sdl2::{event::Event, keyboard::Keycode, pixels::Color}; +pub mod atlas;  pub mod game;  pub mod network;  fn main() { -    let net = Network::connect("ws://127.0.0.1:27032/"); -      let sdl_context = sdl2::init().unwrap();      let video_subsystem = sdl_context.video().unwrap(); -    let _image_context = sdl2::image::init(InitFlag::WEBP).unwrap();      let window = video_subsystem          .window("Hurry Curry! Light Client", 1280, 720)          .position_centered() @@ -48,23 +43,28 @@ fn main() {          .map_err(|e| e.to_string())          .unwrap();      let texture_creator = canvas.texture_creator(); -    let mut texture = texture_creator -        .create_texture( -            Some(PixelFormatEnum::RGBA8888), -            TextureAccess::Streaming, -            1024, -            1024, -        ) -        .unwrap(); -    texture.update(None, &vec![128; 1024 * 1024], 1024).unwrap(); +    let mut net = Network::connect("ws://127.0.0.1/").unwrap(); +    let mut game = Game::new(); +    let mut renderer = SpriteRenderer::init(&texture_creator);      'mainloop: loop { -        canvas.set_draw_color(Color::BLACK); -        canvas.clear(); +        net.poll(); -        canvas.copy(&texture, None, None).unwrap(); +        for packet in net.queue_in.drain(..) { +            match packet { +                PacketC::Data { data } => { +                    renderer.set_sprite_map(data); +                } +                _ => game.packet_in(packet), +            } +        } + +        game.render(&mut renderer); +        canvas.set_draw_color(Color::BLACK); +        canvas.clear(); +        renderer.submit(&mut canvas);          canvas.present();          for event in sdl_context.event_pump().unwrap().poll_iter() { diff --git a/light-client/src/network.rs b/light-client/src/network.rs index dc6e894f..e3cd1eb5 100644 --- a/light-client/src/network.rs +++ b/light-client/src/network.rs @@ -1,22 +1,56 @@ +use anyhow::Result;  use hurrycurry_protocol::{PacketC, PacketS, BINCODE_CONFIG};  use log::warn;  use std::{collections::VecDeque, net::TcpStream}; -use tungstenite::{stream::MaybeTlsStream, Message, WebSocket}; +use tungstenite::{ +    client::{uri_mode, IntoClientRequest}, +    client_tls_with_config, +    handshake::client::Request, +    stream::{MaybeTlsStream, Mode}, +    Message, WebSocket, +};  pub struct Network {      sock: WebSocket<MaybeTlsStream<TcpStream>>, -    queue_in: VecDeque<PacketC>, -    queue_out: VecDeque<PacketS>, +    pub queue_in: VecDeque<PacketC>, +    pub queue_out: VecDeque<PacketS>,  }  impl Network { -    pub fn connect(addr: &str) -> Self { -        let (sock, _resp) = tungstenite::connect(addr).unwrap(); -        Self { +    pub fn connect(addr: &str) -> Result<Self> { +        let (parts, _) = addr.into_client_request().unwrap().into_parts(); +        let uri = parts.uri.clone(); + +        let mut builder = Request::builder() +            .uri(uri.clone()) +            .method(parts.method.clone()) +            .version(parts.version); +        *builder.headers_mut().unwrap() = parts.headers.clone(); +        let request = builder.body(()).unwrap(); + +        let uri = request.uri(); +        let mode = uri_mode(uri)?; +        let host = request.uri().host().unwrap(); +        let host = if host.starts_with('[') { +            &host[1..host.len() - 1] +        } else { +            host +        }; +        let port = uri.port_u16().unwrap_or(match mode { +            Mode::Plain => 27032, +            Mode::Tls => 443, +        }); +        let stream = TcpStream::connect((host, port))?; +        stream.set_nodelay(true).unwrap(); +        // stream.set_nonblocking(true).unwrap(); + +        let (sock, _) = client_tls_with_config(request, stream, None, None).unwrap(); + +        Ok(Self {              sock,              queue_in: VecDeque::new(),              queue_out: VecDeque::new(), -        } +        })      }      pub fn poll(&mut self) {          self.queue_in.extend(match self.sock.read() { diff --git a/light-client/textures/makefile b/light-client/textures/makefile index d130ec97..9b3c1097 100644 --- a/light-client/textures/makefile +++ b/light-client/textures/makefile @@ -1,7 +1,7 @@ -ALL_TA = $(patsubst %.png,%.ta,$(shell find -name '*.png')) -ALL_PNG = $(patsubst %.ta,%.png,$(shell find -name '*.ta')) +ALL_TA = $(patsubst %.png,%.ta,$(shell find tiles -name '*.png')) +ALL_PNG = $(patsubst %.ta,%.png,$(shell find tiles -name '*.ta'))  .PHONY: tex_export tex_import clean  tex_import: $(ALL_TA) | 
