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) |