/*
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}");
}
});
}
}