1use quake_serverinfo::Settings;
2use quake_text::bytestr::to_unicode;
3
4use crate::client::QuakeClient;
5use crate::server::QuakeServer;
6use crate::tokenize;
7
8use crate::hostport::Hostport;
9#[cfg(feature = "json")]
10use serde::{Deserialize, Serialize};
11
12#[derive(Clone, Debug, Default, Eq, PartialEq)]
13#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
14pub struct QtvServer {
15 pub settings: QtvSettings,
16 pub clients: Vec<QtvClient>,
17}
18
19impl From<&QuakeServer> for QtvServer {
20 fn from(server: &QuakeServer) -> Self {
21 let settings = QtvSettings::from(&server.settings);
22 let clients = server.clients.iter().map(QtvClient::from).collect();
23 Self { settings, clients }
24 }
25}
26
27#[derive(Clone, Debug, Default, Eq, PartialEq)]
28#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
29pub struct QtvSettings {
30 pub hostname: String,
31 pub maxclients: u32,
32 pub version: String,
33}
34
35impl From<&Settings> for QtvSettings {
36 fn from(settings: &Settings) -> Self {
37 Self {
38 hostname: settings.hostname.clone().unwrap_or_default(),
39 maxclients: settings.maxclients.unwrap_or_default() as u32,
40 version: settings.version.clone().unwrap_or_default(),
41 }
42 }
43}
44
45#[derive(Clone, Debug, Default, Eq, PartialEq)]
46#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
47pub struct QtvClient {
48 pub id: u32,
49 pub time: u32,
50 pub name: String,
51}
52
53impl From<&QuakeClient> for QtvClient {
54 fn from(client: &QuakeClient) -> Self {
55 Self {
56 id: client.id,
57 time: client.time,
58 name: client.name.clone(),
59 }
60 }
61}
62
63#[derive(Clone, Debug, Default, Eq, PartialEq)]
64#[cfg_attr(feature = "json", derive(Serialize, Deserialize))]
65pub struct QtvStream {
66 pub id: u32,
67 pub name: String,
68 pub number: u32,
69 pub address: Hostport,
70 pub client_count: u32,
71 pub client_names: Vec<String>,
72}
73
74impl QtvStream {
75 pub fn url(&self) -> String {
76 format!("{}@{}", self.number, self.address)
77 }
78}
79
80impl TryFrom<&[u8]> for QtvStream {
81 type Error = anyhow::Error;
82
83 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
84 let parts: Vec<String> = tokenize::tokenize(to_unicode(bytes).as_str());
85 let id: u32 = parts[1].parse()?;
86 let name = parts[2].to_string();
87 let url = parts[3].to_string();
88 let (number, address) = match url.split_once('@') {
89 Some((number_str, hostport)) => {
90 let number = number_str.parse::<u32>().unwrap_or_default();
91 (number, hostport.to_string())
92 }
93 None => (0, url.clone()),
94 };
95 let client_count: u32 = parts[4].parse()?;
96 let address = Hostport::try_from(address.as_str())?;
97
98 Ok(Self {
99 id,
100 name,
101 number,
102 address,
103 client_count,
104 client_names: vec![],
105 })
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use anyhow::Result;
113 use pretty_assertions::assert_eq;
114 use std::time::Duration;
115
116 #[tokio::test]
117 async fn test_from_gameserver() -> Result<()> {
118 let server =
119 QuakeServer::try_from_address("quake.se:28000", Duration::from_secs_f32(0.5)).await?;
120 assert_eq!(
121 QtvServer::from(&server).settings.hostname,
122 "QUAKE.SE KTX Qtv"
123 );
124 Ok(())
125 }
126
127 #[test]
128 fn test_qtv_stream_methods() {
129 let stream = QtvStream {
130 number: 2,
131 address: Hostport {
132 host: "dm6.uk".to_string(),
133 port: 28000,
134 },
135 ..Default::default()
136 };
137 assert_eq!(stream.url(), "2@dm6.uk:28000".to_string());
138 }
139
140 #[test]
141 fn test_qtv_stream_from_bytes() -> Result<()> {
142 assert_eq!(
143 QtvStream::try_from(br#"nqtv 1 "dm6.uk Qtv (7)" "7@dm6.uk:28000" 4"#.as_ref())?,
144 QtvStream {
145 id: 1,
146 name: "dm6.uk Qtv (7)".to_string(),
147 number: 7,
148 address: Hostport {
149 host: "dm6.uk".to_string(),
150 port: 28000,
151 },
152 client_count: 4,
153 client_names: vec![],
154 }
155 );
156 Ok(())
157 }
158
159 #[test]
160 fn test_from_quakeclient() {
161 assert_eq!(
162 QtvClient::from(&QuakeClient {
163 id: 7,
164 name: "XantoM".to_string(),
165 team: "f0m".to_string(),
166 frags: 12,
167 ping: 25,
168 time: 15,
169 top_color: 4,
170 bottom_color: 2,
171 skin: "XantoM".to_string(),
172 auth_cc: "xtm".to_string(),
173 is_spectator: false,
174 is_bot: false,
175 }),
176 QtvClient {
177 id: 7,
178 name: "XantoM".to_string(),
179 time: 15,
180 }
181 );
182 }
183}