summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--light-client/Cargo.toml1
-rw-r--r--light-client/src/atlas.rs154
-rw-r--r--light-client/src/game.rs36
-rw-r--r--light-client/src/main.rs44
-rw-r--r--light-client/src/network.rs48
-rw-r--r--light-client/textures/makefile4
7 files changed, 256 insertions, 32 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 9314fcd0..ab7577aa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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)