use anyhow::Result; use arrayvec::ArrayVec; use libtw2_event_loop::{ Addr, Application, Chunk, ConnlessChunk, Loop, PeerId, SocketLoop, Timeout, }; use libtw2_gamenet_ddnet::{ enums::{Team, VERSION}, msg::{ self, Game, System, SystemOrGame, game::{ClSetTeam, ClStartInfo}, system::{EnterGame, Info, MapChange, Ready, RequestMapData}, }, snap_obj::obj_size, }; use libtw2_packer::{IntUnpacker, Unpacker, with_packer}; use libtw2_snapshot::Manager as SnapManager; use log::{debug, info, warn}; use std::{ fmt::Debug, fs::File, io::Write, net::{IpAddr, Ipv4Addr}, path::PathBuf, }; use twsnap::{Snap, SnapObj}; pub struct Client {} impl Client { pub fn new() -> Result { let mut sloop = SocketLoop::client(); sloop.connect(Addr { ip: IpAddr::V4(Ipv4Addr::LOCALHOST), port: 8303, }); sloop.run(ClientNetwork { state: NetworkState::New, }); Ok(Self {}) } } pub struct ClientNetwork { state: NetworkState, } enum NetworkState { New, Downloading { chunk: i32, data: Vec, path: PathBuf, size: usize, }, Ready, Ingame { snap_manager: SnapManager, snap: twsnap::Snap, }, } impl Application for ClientNetwork { fn needs_tick(&mut self) -> Timeout { Timeout::inactive() } fn on_tick(&mut self, _loop_: &mut L) {} fn on_packet(&mut self, loop_: &mut L, chunk: Chunk) { let pid = chunk.pid; let msg; match msg::decode(&mut Warn(chunk.data), &mut Unpacker::new(chunk.data)) { Ok(m) => msg = m, Err(err) => { warn!("decode error {:?}:", err); return; } } debug!("<- {msg:?}"); match msg { SystemOrGame::System(System::MapChange(MapChange { name, crc, size })) => { let path = map_cache_path(name, crc); if path.exists() { loop_.sends(pid, Ready); self.state = NetworkState::Ready } else { loop_.sends(pid, RequestMapData { chunk: 0 }); self.state = NetworkState::Downloading { chunk: 0, data: Vec::new(), path, size: size as usize, } } } SystemOrGame::System(System::MapData(p)) => { if let NetworkState::Downloading { chunk, data, path, size, } = &mut self.state { data.extend(p.data); info!("download {} / {}", data.len(), size); assert_eq!(*chunk, p.chunk); *chunk += 1; if p.last != 0 { File::create(path).unwrap().write_all(&data).unwrap(); self.state = NetworkState::Ready; loop_.sends(pid, Ready); } else { loop_.sends(pid, RequestMapData { chunk: *chunk }); } } } SystemOrGame::System(System::ConReady(..)) => { if let NetworkState::Ready = &mut self.state { loop_.sendg(pid, ClStartInfo { name: b"testclient", clan: b"metamuffin", country: -1, skin: b"limekittygirl", use_custom_color: true, color_body: 0xFF00FF, color_feet: 0x550055, }); self.state = NetworkState::Ingame { snap: Snap::default(), snap_manager: SnapManager::new(), } } } SystemOrGame::System(System::SnapSingle(p)) => { if let NetworkState::Ingame { snap, snap_manager } = &mut self.state { if let Some(new_snap) = snap_manager .snap_single(&mut Warn(&[]), obj_size, p) .unwrap() { let objs = new_snap .items() .filter_map(|i| { let mut ints = IntUnpacker::new(i.data); match SnapObj::decode_obj(&mut Warn(&[]), i.type_id, &mut ints) { Ok(ob) => Some((ob, i.id)), Err(e) => { warn!("snap object decode error: {e:?} {:?}", i.type_id); None } } }) .collect::>(); snap.process_next(objs.iter()); debug!("{snap:#?}"); } } } SystemOrGame::Game(Game::SvReadyToEnter(..)) => { loop_.sends(pid, EnterGame); loop_.sendg(pid, ClSetTeam { team: Team::Red }); } _ => {} } } fn on_connless_packet(&mut self, _loop_: &mut L, _chunk: ConnlessChunk) {} fn on_connect(&mut self, _loop_: &mut L, _pid: PeerId) { todo!() } fn on_ready(&mut self, loop_: &mut L, pid: PeerId) { loop_.sends(pid, Info { version: VERSION.as_bytes(), password: Some(b""), }); loop_.flush(pid); } fn on_disconnect(&mut self, _loop_: &mut L, _pid: PeerId, _remote: bool, _reason: &[u8]) {} } // Adapted from from libtw2/downloader/src/main.rs by heinrich5991, MIT-or-Apache-2.0 trait LoopExt: Loop { fn sends<'a, S: Into> + Debug>(&mut self, pid: PeerId, msg: S) { debug!("-> System({msg:?})"); fn inner(msg: System, pid: PeerId, loop_: &mut L) { let mut buf: ArrayVec<[u8; 2048]> = ArrayVec::new(); with_packer(&mut buf, |p| msg.encode(p).unwrap()); loop_.send(Chunk { pid, vital: true, data: &buf, }) } inner(msg.into(), pid, self) } fn sendg<'a, G: Into> + Debug>(&mut self, pid: PeerId, msg: G) { debug!("-> System({msg:?})"); fn inner(msg: Game, pid: PeerId, loop_: &mut L) { let mut buf: ArrayVec<[u8; 2048]> = ArrayVec::new(); with_packer(&mut buf, |p| msg.encode(p).unwrap()); loop_.send(Chunk { pid, vital: true, data: &buf, }) } inner(msg.into(), pid, self) } } impl LoopExt for L {} struct Warn<'a>(#[allow(unused)] &'a [u8]); impl<'a, W: Debug> warn::Warn for Warn<'a> { fn warn(&mut self, w: W) { warn!("{:?}", w); } } fn map_cache_path(name: &[u8], crc: i32) -> PathBuf { xdg::BaseDirectories::with_prefix("twclient") .unwrap() .create_cache_directory("maps") .unwrap() .join(format!( "{}_{:08x}", String::from_utf8_lossy(name) .replace("/", "") .replace(".", ""), crc as u32 )) }