diff options
author | metamuffin <yvchraiqi@protonmail.com> | 2022-06-09 09:46:00 +0200 |
---|---|---|
committer | metamuffin <yvchraiqi@protonmail.com> | 2022-06-09 09:46:00 +0200 |
commit | 7a0d09e5cd0075e2a0d3db4505d7ec77dff35ae0 (patch) | |
tree | 5586745b7a9b871b31512cba9f964dabda4651f0 /client | |
download | twclient-7a0d09e5cd0075e2a0d3db4505d7ec77dff35ae0.tar twclient-7a0d09e5cd0075e2a0d3db4505d7ec77dff35ae0.tar.bz2 twclient-7a0d09e5cd0075e2a0d3db4505d7ec77dff35ae0.tar.zst |
(reset git)
Diffstat (limited to 'client')
-rw-r--r-- | client/Cargo.toml | 48 | ||||
-rw-r--r-- | client/src/client/helper.rs | 87 | ||||
-rw-r--r-- | client/src/client/mod.rs | 463 | ||||
-rw-r--r-- | client/src/lib.rs | 21 | ||||
-rw-r--r-- | client/src/main.rs | 41 | ||||
-rw-r--r-- | client/src/world/map.rs | 135 | ||||
-rw-r--r-- | client/src/world/mod.rs | 132 |
7 files changed, 927 insertions, 0 deletions
diff --git a/client/Cargo.toml b/client/Cargo.toml new file mode 100644 index 0000000..5f14df4 --- /dev/null +++ b/client/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "twclient" +version = "0.1.0" +edition = "2021" +authors = [ + "metamuffin <metamuffin@disroot.org>", + "heinrich5991 <heinrich5991@gmail.com>", +] +license = "AGPL-3.0-only" + +[dependencies] +socket = { git = "https://github.com/heinrich5991/libtw2" } +datafile = { git = "https://github.com/heinrich5991/libtw2" } +map = { git = "https://github.com/heinrich5991/libtw2" } +net = { git = "https://github.com/heinrich5991/libtw2" } +common = { git = "https://github.com/heinrich5991/libtw2" } +event_loop = { git = "https://github.com/heinrich5991/libtw2" } +gamenet_teeworlds_0_5 = { git = "https://github.com/heinrich5991/libtw2", optional = true } +gamenet_teeworlds_0_6 = { git = "https://github.com/heinrich5991/libtw2", optional = true } +gamenet_teeworlds_0_7 = { git = "https://github.com/heinrich5991/libtw2", optional = true } +gamenet_ddnet = { git = "https://github.com/heinrich5991/libtw2", optional = true } +logger = { git = "https://github.com/heinrich5991/libtw2" } +packer = { git = "https://github.com/heinrich5991/libtw2" } +# snapshot = { git = "https://github.com/heinrich5991/libtw2" } +snapshot = { path = "../snapshot" } + +ndarray = "0.9.1" +arrayvec = "0.5.2" +clap = "2.31.2" +hexdump = "0.1.1" +itertools = ">=0.3.0,<0.5.0" +log = "0.3.1" +rand = "0.8.3" +tempfile = "2.0.0" +warn = ">=0.1.1,<0.3.0" +env_logger = "0.9.0" +signal-hook = "0.3.14" +lazy_static = "1.4.0" +anyhow = "1.0.57" +crossbeam-channel = "0.5.4" + + +[features] +default = ["gamenet_ddnet_0_6"] +gamenet_0_5 = ["dep:gamenet_teeworlds_0_5", "snapshot/gamenet_teeworlds_0_5"] +gamenet_0_6 = ["dep:gamenet_teeworlds_0_6", "snapshot/gamenet_teeworlds_0_6"] +gamenet_0_7 = ["dep:gamenet_teeworlds_0_7", "snapshot/gamenet_teeworlds_0_7"] +gamenet_ddnet_0_6 = ["dep:gamenet_ddnet", "snapshot/gamenet_ddnet_0_6"] diff --git a/client/src/client/helper.rs b/client/src/client/helper.rs new file mode 100644 index 0000000..af19301 --- /dev/null +++ b/client/src/client/helper.rs @@ -0,0 +1,87 @@ +use arrayvec::ArrayVec; +use event_loop::{Chunk, Loop, PeerId}; +use gamenet::msg::{Game, System}; +use hexdump::hexdump_iter; +use itertools::Itertools; +use log::LogLevel; +use log::{log, log_enabled, warn}; +use packer::with_packer; +use snapshot::format::Item as SnapItem; +use std::{fmt, path::PathBuf}; + +pub trait LoopExt: Loop { + fn send_system<'a, S: Into<System<'a>>>(&mut self, pid: PeerId, msg: S) { + fn inner<L: Loop + ?Sized>(msg: System, pid: PeerId, evloop: &mut L) { + let mut buf: ArrayVec<[u8; 2048]> = ArrayVec::new(); + with_packer(&mut buf, |p| msg.encode(p).unwrap()); + evloop.send(Chunk { + pid, + vital: true, + data: &buf, + }) + } + inner(msg.into(), pid, self) + } + fn send_game<'a, G: Into<Game<'a>>>(&mut self, pid: PeerId, msg: G) { + fn inner<L: Loop + ?Sized>(msg: Game, pid: PeerId, evloop: &mut L) { + let mut buf: ArrayVec<[u8; 2048]> = ArrayVec::new(); + with_packer(&mut buf, |p| msg.encode(p).unwrap()); + evloop.send(Chunk { + pid, + vital: true, + data: &buf, + }) + } + inner(msg.into(), pid, self) + } +} +impl<L: Loop> LoopExt for L {} + +pub fn need_file(crc: i32, name: &str) -> bool { + let mut path = PathBuf::new(); + path.push("/tmp/maps"); + path.push(format!("{}_{:08x}.map", name, crc)); + !path.exists() +} + +pub fn hexdump(level: LogLevel, data: &[u8]) { + if log_enabled!(level) { + hexdump_iter(data).foreach(|s| log!(level, "{}", s)); + } +} +pub struct Warn<'a>(pub &'a [u8]); + +impl<'a, W: fmt::Debug> warn::Warn<W> for Warn<'a> { + fn warn(&mut self, w: W) { + warn!("{:?}", w); + hexdump(LogLevel::Warn, self.0); + } +} + +#[derive(Debug)] +pub struct WarnSnap<'a>(pub SnapItem<'a>); + +impl<'a, W: fmt::Debug> warn::Warn<W> for WarnSnap<'a> { + fn warn(&mut self, w: W) { + warn!("{:?} for {:?}", w, self.0); + } +} + +pub fn check_dummy_map(name: &[u8], crc: u32, size: i32) -> bool { + if name != b"dummy" { + return false; + } + match (crc, size) { + (0xbeae0b9f, 549) => {} + (0x6c760ac4, 306) => {} + _ => warn!("unknown dummy map, crc={}, size={}", crc, size), + } + true +} + +pub fn get_map_path(name: &str, crc: i32) -> PathBuf { + let mut path = PathBuf::new(); + path.push("/tmp/maps"); + path.push(format!("{}_{:08x}.map", name, crc)); + path +} diff --git a/client/src/client/mod.rs b/client/src/client/mod.rs new file mode 100644 index 0000000..c72cc0b --- /dev/null +++ b/client/src/client/mod.rs @@ -0,0 +1,463 @@ +pub mod helper; + +use self::helper::get_map_path; + +use super::gamenet::{ + enums::{Team, VERSION}, + msg::{ + self, + game::{ClSetTeam, ClStartInfo}, + system::{EnterGame, Info, Input, MapChange, MapData}, + system::{Ready, RequestMapData}, + Game, System, SystemOrGame, + }, + snap_obj::{obj_size, PlayerInput}, + SnapObj, +}; +use crate::client::helper::{check_dummy_map, hexdump, WarnSnap}; +use crate::{ + client::helper::{need_file, LoopExt, Warn}, + SHOULD_EXIT, +}; +use common::{num::Cast, pretty}; +use crossbeam_channel::{Receiver, Sender}; +use event_loop::{Addr, Application, Chunk, ConnlessChunk, Loop, PeerId, SocketLoop, Timeout}; +use log::LogLevel; +use log::{debug, error, info, log, warn}; +use packer::{IntUnpacker, Unpacker}; +use std::{ + borrow::{Borrow, Cow}, + collections::HashSet, + fs, + io::{self, Write}, + mem, + net::IpAddr, + sync::atomic::Ordering, + u32, +}; +use tempfile::{NamedTempFile, NamedTempFileOptions}; +use warn::Log; + +pub struct ClientConfig { + pub nick: String, + pub clan: String, + pub timeout: String, +} + +pub struct Client { + pid: PeerId, + config: ClientConfig, + + current_votes: HashSet<Vec<u8>>, + snaps: snapshot::Manager, + num_snaps_since_reset: u64, + dummy_map: bool, + state: ClientState, + download: Option<Download>, + + _interface_receive: Receiver<ClientMesgIn>, + interface_send: Sender<ClientMesgOut>, +} + +pub struct ClientInterface { + pub send: Sender<ClientMesgIn>, + pub receive: Receiver<ClientMesgOut>, +} + +pub enum ClientMesgIn {} +pub enum ClientMesgOut { + MapChange { name: String, crc: i32 }, + Snaps(Vec<(u16, SnapObj)>), +} + +#[derive(Clone, Copy, Debug)] +enum ClientState { + Connection, + MapChange, + MapData(i32, i32), + ConReady, + ReadyToEnter, +} + +impl Default for ClientState { + fn default() -> ClientState { + ClientState::Connection + } +} + +impl Client { + pub fn new_evloop() -> SocketLoop { + SocketLoop::client() + } + + pub fn new( + evloop: &mut SocketLoop, + ip: IpAddr, + port: u16, + config: ClientConfig, + ) -> (Self, ClientInterface) { + fs::create_dir_all("/tmp/maps").unwrap(); + fs::create_dir_all("/tmp/downloading").unwrap(); + let (a, b) = crossbeam_channel::unbounded(); + let (c, d) = crossbeam_channel::unbounded(); + ( + Client { + pid: evloop.connect(Addr { ip, port }), + config, + current_votes: HashSet::new(), + snaps: snapshot::Manager::new(), + num_snaps_since_reset: 0, + dummy_map: false, + state: ClientState::Connection, + download: None, + _interface_receive: b, + interface_send: c, + }, + ClientInterface { + receive: d, + send: a, + }, + ) + } + pub fn run(self, evloop: SocketLoop) { + evloop.run(self) + } +} + +impl<'a, L: Loop> Application<L> for Client { + fn needs_tick(&mut self) -> Timeout { + Timeout::inactive() + } + fn on_tick(&mut self, evloop: &mut L) { + if SHOULD_EXIT.load(Ordering::Relaxed) { + warn!("exiting peer {}", self.pid); + evloop.disconnect(self.pid, b"error"); + } + } + fn on_packet(&mut self, evloop: &mut L, chunk: Chunk) { + let pid = chunk.pid; + + let msg = match msg::decode(&mut Warn(chunk.data), &mut Unpacker::new(chunk.data)) { + Ok(m) => m, + Err(err) => { + warn!("decode error {:?}:", err); + hexdump(LogLevel::Warn, chunk.data); + return; + } + }; + debug!("{:?}", msg); + match msg { + SystemOrGame::Game(Game::SvMotd(..)) + | SystemOrGame::Game(Game::SvKillMsg(..)) + | SystemOrGame::Game(Game::SvTuneParams(..)) + | SystemOrGame::Game(Game::SvWeaponPickup(..)) + | SystemOrGame::System(System::InputTiming(..)) + | SystemOrGame::Game(Game::SvExtraProjectile(..)) => {} + SystemOrGame::Game(Game::SvChat(chat)) => { + if chat.client_id == -1 { + info!("[server]: {}", pretty::AlmostString::new(chat.message)); + } else { + info!( + "[team {}]: {}", + chat.team, + pretty::AlmostString::new(chat.message) + ) + } + } + SystemOrGame::Game(Game::SvBroadcast(broadcast)) => { + info!( + "broadcast: {}", + pretty::AlmostString::new(broadcast.message) + ); + } + _ => {} + } + + match msg { + SystemOrGame::System(ref msg) => match *msg { + System::MapChange(MapChange { crc, size, name }) => { + if let Some(_) = size.try_usize() { + if name.iter().any(|&b| b == b'/' || b == b'\\') { + error!("invalid map name"); + evloop.disconnect(pid, b"error"); + return; + } + match self.state { + ClientState::MapChange => {} + ClientState::ReadyToEnter if self.dummy_map => {} + _ => warn!("map change from state {:?}", self.state), + } + self.dummy_map = check_dummy_map(name, crc as u32, size); + self.current_votes.clear(); + self.num_snaps_since_reset = 0; + self.snaps.reset(); + info!("map change: {}", pretty::AlmostString::new(name)); + let name = String::from_utf8_lossy(name); + if let Cow::Owned(..) = name { + warn!("weird characters in map name"); + } + let mut start_download = false; + if need_file(crc, &name) { + if let Err(e) = self.open_download_file(crc, name.borrow()) { + error!("error opening file {:?}", e); + } else { + start_download = true; + } + } + if start_download { + info!("download starting"); + evloop.send_system(pid, RequestMapData { chunk: 0 }); + self.state = ClientState::MapData(crc, 0); + } else { + self.state = ClientState::ConReady; + evloop.send_system(pid, Ready); + + self.interface_send + .send(ClientMesgOut::MapChange { + name: name.to_string(), + crc, + }) + .unwrap(); + } + } else { + error!("invalid map size"); + evloop.disconnect(pid, b"error"); + return; + } + } + System::Snap(_) | System::SnapEmpty(_) | System::SnapSingle(_) => { + self.num_snaps_since_reset += 1; + let mut input = PlayerInput::default(); + { + let res = match *msg { + System::Snap(s) => self.snaps.snap(&mut Log, obj_size, s), + System::SnapEmpty(s) => self.snaps.snap_empty(&mut Log, obj_size, s), + System::SnapSingle(s) => self.snaps.snap_single(&mut Log, obj_size, s), + _ => unreachable!(), + }; + match res { + Ok(Some(snap)) => { + let snaps = snap + .items() + .filter_map(|item| { + SnapObj::decode_obj( + &mut WarnSnap(item), + item.type_id.into(), + &mut IntUnpacker::new(item.data), + ) + .map(|o| (item.id, o)) + .ok() + }) + .collect::<Vec<(u16, SnapObj)>>(); + + self.interface_send + .send(ClientMesgOut::Snaps(snaps.clone())) + .unwrap(); + + let client_id = snaps + .iter() + .filter_map(|(_id, i)| { + if let SnapObj::PlayerInfo(p) = i { + if p.local != 0 { + Some(p.client_id) + } else { + None + } + } else { + None + } + }) + .next() + .unwrap_or(1 << 15) + as u16; + + let mut target_position = None; + let mut my_position = (0, 0); + for (id, snap) in &snaps { + match snap { + SnapObj::Character(c) => { + let c = c.character_core; + if *id == client_id { + my_position = (c.x, c.y) + } else if c.hook_state != 0 { + target_position = Some((c.x, c.y)) + } + } + _ => {} + } + } + if let Some(target) = target_position { + if target.0 < my_position.0 { + input.direction = -1; + } + if target.0 > my_position.0 { + input.direction = 1; + } + if target.1 - 16 < my_position.1 { + input.jump = 1; + } + } + } + Ok(None) => { + self.num_snaps_since_reset -= 1; + } + Err(err) => warn!("snapshot error {:?}", err), + } + } + // DDNet needs the INPUT message as the first + // chunk of the packet. + evloop.force_flush(pid); + let tick = self.snaps.ack_tick().unwrap_or(-1); + evloop.send_system( + pid, + Input { + ack_snapshot: tick, + intended_tick: tick, + input_size: mem::size_of::<PlayerInput>().assert_i32(), + input, + }, + ); + } + _ => {} + }, + SystemOrGame::Game(ref msg) => match *msg { + _ => {} + }, + } + match self.state { + ClientState::Connection => unreachable!(), + ClientState::MapChange => {} // Handled above. + ClientState::MapData(cur_crc, cur_chunk) => match msg { + SystemOrGame::System(System::MapData(MapData { + last, + crc, + chunk, + data, + })) => { + if cur_crc == crc && cur_chunk == chunk { + let res = self.write_download_file(data); + if let Err(ref err) = res { + error!("error writing file {:?}", err); + } + if last != 0 || res.is_err() { + if !res.is_err() { + if let Err(err) = self.finish_download_file() { + error!("error finishing file {:?}", err); + } + if last != 1 { + warn!("weird map data packet"); + } + } + self.state = ClientState::ConReady; + evloop.send_system(pid, Ready); + + info!("download finished"); + } else { + let cur_chunk = cur_chunk.checked_add(1).unwrap(); + self.state = ClientState::MapData(cur_crc, cur_chunk); + evloop.send_system(pid, RequestMapData { chunk: cur_chunk }); + } + } else { + if cur_crc != crc || cur_chunk < chunk { + warn!("unsolicited map data crc={:08x} chunk={}", crc, chunk); + warn!("want crc={:08x} chunk={}", cur_crc, cur_chunk); + } + } + } + _ => {} + }, + ClientState::ConReady => match msg { + SystemOrGame::System(System::ConReady(..)) => { + evloop.send_game( + pid, + ClStartInfo { + name: self.config.nick.as_bytes(), + clan: self.config.clan.as_bytes(), + country: -1, + skin: b"limekittygirl", + use_custom_color: true, + color_body: 0xFF00FF, + color_feet: 0x550055, + }, + ); + self.state = ClientState::ReadyToEnter; + } + _ => {} + }, + ClientState::ReadyToEnter => match msg { + SystemOrGame::Game(Game::SvReadyToEnter(..)) => { + evloop.send_system(pid, EnterGame); + evloop.send_game(pid, ClSetTeam { team: Team::Red }); + } + _ => {} + }, + } + evloop.flush(pid); + } + fn on_connless_packet(&mut self, _: &mut L, chunk: ConnlessChunk) { + warn!( + "connless packet {} {:?}", + chunk.addr, + pretty::Bytes::new(chunk.data) + ); + } + fn on_connect(&mut self, _: &mut L, _: PeerId) { + unreachable!(); + } + fn on_ready(&mut self, evloop: &mut L, pid: PeerId) { + if pid != self.pid { + error!("not our pid: {} vs {}", pid, self.pid); + } + self.state = ClientState::MapChange; + evloop.send_system( + pid, + Info { + version: VERSION.as_bytes(), + password: Some(b""), + }, + ); + evloop.flush(pid); + } + fn on_disconnect(&mut self, _: &mut L, pid: PeerId, remote: bool, reason: &[u8]) { + if remote { + error!( + "disconnected pid={:?} error={}", + pid, + pretty::AlmostString::new(reason) + ); + } + } +} + +struct Download { + file: NamedTempFile, + crc: i32, + name: String, +} + +impl Client { + fn open_download_file(&mut self, crc: i32, name: &str) -> Result<(), io::Error> { + self.download = Some(Download { + file: NamedTempFileOptions::new() + .prefix(&format!("{}_{:08x}_", name, crc)) + .suffix(".map") + .create_in("/tmp/downloading")?, + crc, + name: name.to_string(), + }); + Ok(()) + } + fn write_download_file(&mut self, data: &[u8]) -> Result<(), io::Error> { + self.download.as_mut().unwrap().file.write_all(data) + } + fn finish_download_file(&mut self) -> Result<(), io::Error> { + let download = self.download.take().unwrap(); + let path = get_map_path(download.name.as_str(), download.crc); + download + .file + .persist(&path) + .map(|_| ()) + .map_err(|e| e.error)?; + Ok(()) + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs new file mode 100644 index 0000000..3c19df7 --- /dev/null +++ b/client/src/lib.rs @@ -0,0 +1,21 @@ +#![feature(exclusive_range_pattern)] + +pub mod client; +pub mod world; + +#[cfg(feature = "gamenet_ddnet_0_6")] +pub extern crate gamenet_ddnet as gamenet; +#[cfg(feature = "gamenet_0_5")] +pub extern crate gamenet_teeworlds_0_5 as gamenet; +#[cfg(feature = "gamenet_0_6")] +pub extern crate gamenet_teeworlds_0_6 as gamenet; +#[cfg(feature = "gamenet_0_7")] +pub extern crate gamenet_teeworlds_0_7 as gamenet; + + +use std::sync::atomic::AtomicBool; + +use lazy_static::lazy_static; +lazy_static! { + pub static ref SHOULD_EXIT: AtomicBool = AtomicBool::new(false); +} diff --git a/client/src/main.rs b/client/src/main.rs new file mode 100644 index 0000000..b79f338 --- /dev/null +++ b/client/src/main.rs @@ -0,0 +1,41 @@ +use log::{error, log, warn}; +use signal_hook::{ + consts::{SIGINT, SIGTERM}, + iterator::Signals, + low_level::exit, +}; +use std::{net::IpAddr, str::FromStr, sync::atomic::Ordering, thread, time::Duration}; +use twclient::{ + client::{Client, ClientConfig}, + SHOULD_EXIT, +}; + +fn main() { + env_logger::init(); + + let mut signals = Signals::new(&[SIGTERM, SIGINT]).unwrap(); + thread::spawn(move || { + for sig in signals.forever() { + warn!("received signal {:?}", sig); + SHOULD_EXIT.store(true, Ordering::Relaxed); + thread::sleep(Duration::from_secs(3)); + error!("exit timeout!"); + exit(1); + } + }); + + let config = ClientConfig { + nick: String::from("metamuffin"), + clan: String::from("rustacean"), + timeout: String::from("asgefdhjikhjfhjf"), + }; + let mut args = std::env::args().skip(1); + let ip = IpAddr::from_str(args.next().unwrap().as_str()).unwrap(); + let port = u16::from_str(args.next().unwrap().as_str()).unwrap(); + drop(ip); + drop(port); + drop(config); + drop(Client::new_evloop()); + todo!() + // Client::run_thing(ip, port, config) +} diff --git a/client/src/world/map.rs b/client/src/world/map.rs new file mode 100644 index 0000000..fb67918 --- /dev/null +++ b/client/src/world/map.rs @@ -0,0 +1,135 @@ +use ::map as mapfile; +use anyhow::Error; +use common::{num::Cast, vec}; +use log::{info, log}; +use ndarray::Array2; +use std::{ + collections::{hash_map, HashMap}, + fs::File, + mem, +}; + +pub use mapfile::format; +pub use mapfile::{ + format::Tile, + reader::{self, Color, LayerTilemapType}, +}; +pub const TILE_NUM: u32 = 16; + +pub struct Layer { + pub color: Color, + pub image: Option<usize>, + pub tiles: Array2<Tile>, + pub kind: LayerTilemapType, +} + +pub struct Map { + pub layers: Vec<Layer>, + pub tilesets: HashMap<Option<usize>, Array2<Color>>, +} + +impl Map { + pub fn empty() -> Self { + Self { + layers: Vec::new(), + tilesets: HashMap::new(), + } + } + + pub fn load(file: File) -> Result<Self, Error> { + info!("loading map"); + let datafile = datafile::Reader::new(file).unwrap(); + let mut map = mapfile::Reader::from_datafile(datafile); + + let mut layers = vec![]; + for group_idx in map.group_indices() { + let group = map.group(group_idx).unwrap(); + + if group.parallax_x != 100 + || group.parallax_y != 100 + || group.offset_x != 0 + || group.offset_y != 0 + || group.clipping.is_some() + { + continue; + } + + for layer_idx in group.layer_indices { + let layer = map.layer(layer_idx).unwrap(); + + let tilemap = if let reader::LayerType::Tilemap(t) = layer.t { + t + } else { + continue; + }; + let normal = if let Some(n) = tilemap.type_.to_normal() { + n + } else { + continue; + }; + let tiles = map.layer_tiles(tilemap.tiles(normal.data)).unwrap(); + layers.push(Layer { + color: normal.color, + image: normal.image, + kind: tilemap.type_, + tiles, + }); + } + } + + let mut tilesets = HashMap::new(); + for layer in &layers { + match tilesets.entry(layer.image) { + hash_map::Entry::Occupied(_) => {} + hash_map::Entry::Vacant(v) => { + let data = match layer.image { + None => Array2::from_elem( + (1, 1), + Color { + alpha: 255, + red: 255, + blue: 255, + green: 0, + }, + ), + Some(image_idx) => { + let image = map.image(image_idx).unwrap(); + let height = image.height.usize(); + let width = image.width.usize(); + match image.data { + Some(d) => { + let data = map.image_data(d).unwrap(); + if data.len() % mem::size_of::<Color>() != 0 { + panic!("image shape invalid"); + } + let data: Vec<Color> = unsafe { vec::transmute(data) }; + Array2::from_shape_vec((height, width), data).unwrap() + } + None => { + continue; + // let image_name = map.image_name(image.name)?; + // // WARN? Unknown external image + // // WARN! Wrong dimensions + // str::from_utf8(&image_name).ok() + // .and_then(sanitize) + // .map(&mut external_tileset_loader) + // .transpose()? + // .unwrap_or(None) + // .unwrap_or_else(|| Array2::from_elem((1, 1), Color::white())) + } + } + } + }; + v.insert(data); + } + } + } + + info!( + "{} layers + {} tilesets loaded", + layers.len(), + tilesets.len() + ); + Ok(Self { tilesets, layers }) + } +} diff --git a/client/src/world/mod.rs b/client/src/world/mod.rs new file mode 100644 index 0000000..ae985a6 --- /dev/null +++ b/client/src/world/mod.rs @@ -0,0 +1,132 @@ +use gamenet::{ + enums::{Emote, Team, Weapon}, + SnapObj, +}; + +use self::map::Map; +use crate::client::{helper::get_map_path, ClientMesgOut}; +use std::{collections::BTreeMap, fs::File}; + +pub mod map; + +pub use gamenet::enums; + +#[derive(Debug)] +pub struct Tee { + pub local: bool, + pub latency: i32, + pub score: i32, + + pub team: Team, + pub weapon: Weapon, + pub armor: i32, + pub ammo: i32, + pub emote: Emote, + pub attack_tick: i32, + + pub tick: i32, + pub angle: i32, + pub x: i32, + pub y: i32, + pub vel_x: i32, + pub vel_y: i32, + pub hook_x: i32, + pub hook_y: i32, + pub hook_dx: i32, + pub hook_dy: i32, + pub hook_player: i32, + pub hook_state: i32, +} + +impl Default for Tee { + fn default() -> Self { + Self { + x: Default::default(), + y: Default::default(), + local: false, + team: Team::Spectators, + latency: Default::default(), + score: Default::default(), + weapon: Weapon::Shotgun, + armor: Default::default(), + ammo: Default::default(), + attack_tick: Default::default(), + emote: Emote::Normal, + tick: Default::default(), + angle: Default::default(), + vel_x: Default::default(), + vel_y: Default::default(), + hook_x: Default::default(), + hook_y: Default::default(), + hook_dx: Default::default(), + hook_dy: Default::default(), + hook_player: Default::default(), + hook_state: Default::default(), + } + } +} + +pub struct World { + pub map: Map, + pub tees: BTreeMap<u16, Tee>, +} + +impl World { + pub fn new() -> Self { + Self { + map: Map::empty(), + tees: BTreeMap::new(), + } + } + + pub fn update(&mut self, m: &ClientMesgOut) { + match m { + ClientMesgOut::MapChange { name, crc } => { + let file = File::open(get_map_path(name.as_str(), *crc)).unwrap(); + self.map = Map::load(file).unwrap(); + } + ClientMesgOut::Snaps(s) => { + self.tees.clear(); + for (id, o) in s { + match o { + SnapObj::ClientInfo(_o) => { + // TODO + } + SnapObj::PlayerInfo(o) => { + let e = self.tees.entry(*id).or_default(); + e.local = o.local == 1; + e.team = o.team; + e.latency = o.latency; + e.score = o.score; + } + SnapObj::Character(c) => { + let e = self.tees.entry(*id).or_default(); + e.ammo = c.ammo_count; + e.weapon = c.weapon; + e.emote = c.emote; + e.attack_tick = c.attack_tick; + + e.x = c.character_core.x; + e.y = c.character_core.y; + e.vel_x = c.character_core.vel_x; + e.vel_y = c.character_core.vel_y; + + e.tick = c.character_core.tick; + e.hook_x = c.character_core.hook_x; + e.hook_y = c.character_core.hook_y; + e.hook_player = c.character_core.hooked_player; + e.hook_dx = c.character_core.hook_dx; + e.hook_dy = c.character_core.hook_dy; + e.hook_state = c.character_core.hook_state; + } + _ => (), + } + } + } + } + } + + pub fn local_tee(&self) -> Option<&Tee> { + self.tees.values().find(|e| e.local) + } +} |