/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ #![feature(never_type)] use anyhow::Result; use clap::Parser; use http_body_util::Full; use hurrycurry_protocol::registry::Entry; use hyper::{ body::Bytes, header::HeaderValue, server::conn::http1, service::service_fn, Response, StatusCode, }; use hyper_util::rt::TokioIo; use log::warn; use mdns_sd::{ServiceDaemon, ServiceEvent}; use std::{cmp::Reverse, collections::HashMap, net::SocketAddr, sync::Arc}; use tokio::{net::TcpListener, spawn, sync::RwLock}; #[derive(Parser)] struct Args { /// Print version and exit #[arg(short, long)] version: bool, } fn main() -> Result<()> { let args = Args::parse(); if args.version { println!("{}", env!("CARGO_PKG_VERSION")); return Ok(()); } env_logger::init_from_env("LOG"); tokio::runtime::Builder::new_current_thread() .enable_all() .build()? .block_on(async_main())?; } async fn async_main() -> Result { let mdns = ServiceDaemon::new()?; let mdns_events = mdns.browse("_hurrycurry._tcp.local.")?; let entries = Arc::new(RwLock::new(HashMap::::new())); let entries2 = entries.clone(); spawn(async move { while let Ok(event) = mdns_events.recv_async().await { match event { ServiceEvent::ServiceResolved(service_info) => { entries2.write().await.insert( service_info.get_fullname().to_owned(), Entry { name: service_info .get_fullname() .strip_suffix("._hurrycurry._tcp.local.") .unwrap_or(service_info.get_fullname()) .to_owned(), address: service_info .get_addresses() .iter() .map(|a| { format!("ws://{}", SocketAddr::new(*a, service_info.get_port())) }) .collect(), players_online: service_info .get_property_val_str("players") .and_then(|p| p.parse().ok()) .unwrap_or(0), last_game: 0, version: service_info .get_property_val_str("protocol") .and_then(|v| { let (maj, min) = v.split_once(".")?; Some((maj.parse().ok()?, min.parse().ok()?)) }) .unwrap_or((0, 0)), }, ); } ServiceEvent::ServiceRemoved(_, fullname) => { entries2.write().await.remove(&fullname); } _ => (), } } }); let listener = TcpListener::bind("127.0.0.1:27033").await?; loop { let (stream, _) = listener.accept().await?; let entries = entries.clone(); spawn(async move { if let Err(e) = http1::Builder::new() .serve_connection( TokioIo::new(stream), service_fn(move |_req| { let entries = entries.clone(); async move { Ok::<_, !>(match _req.uri().path() { "/" => Response::new(Full::new(Bytes::from( "Hurry Curry! local discovery service", ))), "/v1/list" => { let mut ents = entries .read() .await .clone() .into_values() .collect::>(); ents.sort_by_key(|e| Reverse(e.players_online)); let mut res = Response::new(Full::new(Bytes::from( serde_json::to_string(&ents).unwrap(), ))); *res.status_mut() = StatusCode::OK; res.headers_mut().insert( "content-type", HeaderValue::from_static("application/json"), ); res } _ => { let mut res = Response::new(Full::new(Bytes::from("not found"))); *res.status_mut() = StatusCode::NOT_FOUND; res } }) } }), ) .await { warn!("conn failed: {e}"); } }); } }