query: filter parameter

This commit is contained in:
Denis Drakhnia 2023-10-19 17:38:39 +03:00
parent 7e676620eb
commit 1254e41adf
8 changed files with 106 additions and 57 deletions

View File

@ -229,7 +229,7 @@ impl MasterServer {
match p {
game::Packet::QueryServers(p) => {
trace!("{}: recv {:?}", from, p);
if p.filter.clver < self.clver {
if p.filter.clver.map_or(false, |v| v < self.clver) {
let iter = std::iter::once(self.update_addr);
self.send_server_list(from, iter)?;
} else {

View File

@ -59,7 +59,10 @@ impl<'a> Iterator for ColorIter<'a> {
fn next(&mut self) -> Option<Self::Item> {
if !self.inner.is_empty() {
let i = self.inner[1..].find('^').map(|i| i + 1).unwrap_or(self.inner.len());
let i = self.inner[1..]
.find('^')
.map(|i| i + 1)
.unwrap_or(self.inner.len());
let (head, tail) = self.inner.split_at(i);
let (color, text) = trim_start_color(head);
self.inner = tail;

View File

@ -8,7 +8,7 @@ use std::slice;
use std::str;
use super::types::Str;
use super::{Error, color};
use super::{color, Error};
pub trait GetKeyValue<'a>: Sized {
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error>;

View File

@ -90,7 +90,7 @@ pub struct Version {
}
impl Version {
pub fn new(major: u8, minor: u8) -> Self {
pub const fn new(major: u8, minor: u8) -> Self {
Self { major, minor }
}
}
@ -136,11 +136,11 @@ impl PutKeyValue for Version {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Filter<'a> {
/// Servers running the specified modification (ex. cstrike)
pub gamedir: &'a [u8],
pub gamedir: Option<&'a [u8]>,
/// Servers running the specified map (ex. cs_italy)
pub map: &'a [u8],
pub map: Option<&'a [u8]>,
/// Client version.
pub clver: Version,
pub clver: Option<Version>,
pub flags: FilterFlags,
pub flags_mask: FilterFlags,
@ -154,13 +154,15 @@ impl Filter<'_> {
pub fn matches(&self, _addr: SocketAddrV4, info: &ServerInfo) -> bool {
!((info.flags & self.flags_mask) != self.flags
|| (!self.gamedir.is_empty() && self.gamedir != &*info.gamedir)
|| (!self.map.is_empty() && self.map != &*info.map))
|| self.gamedir.map_or(false, |s| s != &*info.gamedir)
|| self.map.map_or(false, |s| s != &*info.map))
}
}
impl<'a> Filter<'a> {
pub fn from_bytes(src: &'a [u8]) -> Result<Self, Error> {
impl<'a> TryFrom<&'a [u8]> for Filter<'a> {
type Error = Error;
fn try_from(src: &'a [u8]) -> Result<Self, Self::Error> {
let mut cur = Cursor::new(src);
let mut filter = Self::default();
@ -174,17 +176,18 @@ impl<'a> Filter<'a> {
match *key {
b"dedicated" => filter.insert_flag(FilterFlags::DEDICATED, cur.get_key_value()?),
b"secure" => filter.insert_flag(FilterFlags::SECURE, cur.get_key_value()?),
b"gamedir" => filter.gamedir = cur.get_key_value()?,
b"map" => filter.map = cur.get_key_value()?,
b"gamedir" => filter.gamedir = Some(cur.get_key_value()?),
b"map" => filter.map = Some(cur.get_key_value()?),
b"empty" => filter.insert_flag(FilterFlags::NOT_EMPTY, cur.get_key_value()?),
b"full" => filter.insert_flag(FilterFlags::FULL, cur.get_key_value()?),
b"password" => filter.insert_flag(FilterFlags::PASSWORD, cur.get_key_value()?),
b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, cur.get_key_value()?),
b"clver" => {
filter.clver = cur
.get_key_value::<&str>()?
.parse()
.map_err(|_| Error::InvalidPacket)?
filter.clver = Some(
cur.get_key_value::<&str>()?
.parse()
.map_err(|_| Error::InvalidPacket)?,
);
}
b"nat" => filter.insert_flag(FilterFlags::NAT, cur.get_key_value()?),
b"lan" => filter.insert_flag(FilterFlags::LAN, cur.get_key_value()?),
@ -214,18 +217,20 @@ impl fmt::Display for &Filter<'_> {
display_flag!("dedicated", FilterFlags::DEDICATED);
display_flag!("secure", FilterFlags::SECURE);
if !self.gamedir.is_empty() {
write!(fmt, "\\gamedir\\{}", Str(self.gamedir))?;
if let Some(s) = self.gamedir {
write!(fmt, "\\gamedir\\{}", Str(s))?;
}
display_flag!("secure", FilterFlags::SECURE);
if !self.map.is_empty() {
write!(fmt, "\\map\\{}", Str(self.map))?;
if let Some(s) = self.map {
write!(fmt, "\\map\\{}", Str(s))?;
}
display_flag!("empty", FilterFlags::NOT_EMPTY);
display_flag!("full", FilterFlags::FULL);
display_flag!("password", FilterFlags::PASSWORD);
display_flag!("noplayers", FilterFlags::NOPLAYERS);
write!(fmt, "\\clver\\{}", self.clver)?;
if let Some(v) = self.clver {
write!(fmt, "\\clver\\{}", v)?;
}
display_flag!("nat", FilterFlags::NAT);
display_flag!("lan", FilterFlags::LAN);
display_flag!("bots", FilterFlags::BOTS);
@ -253,7 +258,7 @@ mod tests {
.. Filter::default()
};
$(assert_eq!(
Filter::from_bytes($src),
Filter::try_from($src as &[u8]),
Ok(Filter {
$($field: $value,)*
..predefined
@ -266,17 +271,17 @@ mod tests {
tests! {
parse_gamedir {
b"\\gamedir\\valve" => {
gamedir: &b"valve"[..],
gamedir: Some(&b"valve"[..]),
}
}
parse_map {
b"\\map\\crossfire" => {
map: &b"crossfire"[..],
map: Some(&b"crossfire"[..]),
}
}
parse_clver {
b"\\clver\\0.20" => {
clver: Version::new(0, 20),
clver: Some(Version::new(0, 20)),
}
}
parse_dedicated(flags_mask: FilterFlags::DEDICATED) {
@ -349,9 +354,9 @@ mod tests {
\\password\\1\
\\secure\\1\
" => {
gamedir: &b"valve"[..],
map: &b"crossfire"[..],
clver: Version::new(0, 20),
gamedir: Some(&b"valve"[..]),
map: Some(&b"crossfire"[..]),
clver: Some(Version::new(0, 20)),
flags: FilterFlags::all(),
flags_mask: FilterFlags::all(),
}
@ -383,7 +388,7 @@ mod tests {
macro_rules! matches {
($servers:expr, $filter:expr$(, $expected:expr)*) => (
let servers = &$servers;
let filter = Filter::from_bytes($filter).unwrap();
let filter = Filter::try_from($filter as &[u8]).unwrap();
let iter = servers
.iter()
.enumerate()

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::fmt;
use std::net::SocketAddrV4;
use crate::cursor::{Cursor, CursorMut};
@ -9,18 +10,23 @@ use crate::server::Region;
use crate::Error;
#[derive(Clone, Debug, PartialEq)]
pub struct QueryServers<'a> {
pub struct QueryServers<T> {
pub region: Region,
pub last: SocketAddrV4,
pub filter: Filter<'a>,
pub filter: T,
}
impl<'a> QueryServers<'a> {
impl QueryServers<()> {
pub const HEADER: &'static [u8] = b"1";
}
impl<'a, T: 'a> QueryServers<T>
where
T: TryFrom<&'a [u8], Error = Error>,
{
pub fn decode(src: &'a [u8]) -> Result<Self, Error> {
let mut cur = Cursor::new(src);
cur.expect(Self::HEADER)?;
cur.expect(QueryServers::HEADER)?;
let region = cur.get_u8()?.try_into().map_err(|_| Error::InvalidPacket)?;
let last = cur.get_cstr_as_str()?;
let filter = cur.get_cstr()?;
@ -28,13 +34,18 @@ impl<'a> QueryServers<'a> {
Ok(Self {
region,
last: last.parse().map_err(|_| Error::InvalidPacket)?,
filter: Filter::from_bytes(&filter)?,
filter: T::try_from(*filter)?,
})
}
}
impl<'a, T: 'a> QueryServers<T>
where
for<'b> &'b T: fmt::Display,
{
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> {
Ok(CursorMut::new(buf)
.put_bytes(Self::HEADER)?
.put_bytes(QueryServers::HEADER)?
.put_u8(self.region as u8)?
.put_as_str(self.last)?
.put_u8(0)?
@ -76,7 +87,7 @@ impl GetServerInfo {
#[derive(Clone, Debug, PartialEq)]
pub enum Packet<'a> {
QueryServers(QueryServers<'a>),
QueryServers(QueryServers<Filter<'a>>),
GetServerInfo(GetServerInfo),
}
@ -106,9 +117,9 @@ mod tests {
region: Region::RestOfTheWorld,
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0),
filter: Filter {
gamedir: &b"valve"[..],
map: &b"crossfire"[..],
clver: Version::new(0, 20),
gamedir: Some(&b"valve"[..]),
map: Some(&b"crossfire"[..]),
clver: Some(Version::new(0, 20)),
flags: FilterFlags::all(),
flags_mask: FilterFlags::all(),
},

View File

@ -5,18 +5,22 @@ mod cursor;
mod server_info;
pub mod admin;
pub mod color;
pub mod filter;
pub mod game;
pub mod master;
pub mod server;
pub mod types;
pub mod color;
pub use server_info::ServerInfo;
use thiserror::Error;
pub const VERSION: u8 = 49;
use crate::filter::Version;
pub const PROTOCOL_VERSION: u8 = 49;
pub const CLIENT_VERSION: Version = Version::new(0, 20);
#[derive(Error, Debug, PartialEq, Eq)]
pub enum Error {

View File

@ -5,6 +5,8 @@ use std::process;
use getopts::Options;
use xash3d_protocol as proto;
const BIN_NAME: &str = env!("CARGO_BIN_NAME");
const PKG_NAME: &str = env!("CARGO_PKG_NAME");
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
@ -22,6 +24,7 @@ pub struct Cli {
pub json: bool,
pub debug: bool,
pub force_color: bool,
pub filter: String,
}
impl Default for Cli {
@ -34,10 +37,12 @@ impl Default for Cli {
args: Default::default(),
master_timeout: 2,
server_timeout: 2,
protocol: vec![xash3d_protocol::VERSION, xash3d_protocol::VERSION - 1],
protocol: vec![proto::PROTOCOL_VERSION, proto::PROTOCOL_VERSION - 1],
json: false,
debug: false,
force_color: false,
// if changed do not forget to update cli parsing
filter: format!("\\gamedir\\valve\\clver\\{}", proto::CLIENT_VERSION),
}
}
}
@ -94,6 +99,8 @@ pub fn parse() -> Cli {
opts.optflag("j", "json", "output JSON");
opts.optflag("d", "debug", "output debug");
opts.optflag("F", "force-color", "force colored output");
let help = format!("query filter [default: {:?}]", cli.filter);
opts.optopt("f", "filter", &help, "FILTER");
let matches = match opts.parse(&args[1..]) {
Ok(m) => m,
@ -124,7 +131,7 @@ pub fn parse() -> Cli {
}
}
match matches.opt_get("master") {
match matches.opt_get("master-timeout") {
Ok(Some(t)) => cli.master_timeout = t,
Ok(None) => {}
Err(_) => {
@ -161,6 +168,18 @@ pub fn parse() -> Cli {
}
}
if let Some(s) = matches.opt_str("filter") {
let mut filter = String::with_capacity(cli.filter.len() + s.len());
if !s.contains("\\gamedir") {
filter.push_str("\\gamedir\\valve");
}
if !s.contains("\\clver") {
filter.push_str("\\clver\\0.20");
}
filter.push_str(&s);
cli.filter = filter;
}
cli.json = matches.opt_present("json");
cli.debug = matches.opt_present("debug");
cli.force_color = matches.opt_present("force-color");

View File

@ -9,14 +9,14 @@ use std::fmt;
use std::io;
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
use std::process;
use std::sync::mpsc;
use std::sync::{mpsc, Arc};
use std::thread;
use std::time::{Duration, Instant};
use serde::Serialize;
use thiserror::Error;
use xash3d_protocol::types::Str;
use xash3d_protocol::{color, filter, game, master, server, Error as ProtocolError};
use xash3d_protocol::{color, game, master, server, Error as ProtocolError};
use crate::cli::Cli;
@ -135,6 +135,7 @@ struct InfoResult<'a> {
master_timeout: u32,
server_timeout: u32,
masters: &'a [Box<str>],
filter: &'a str,
servers: &'a [&'a ServerResult],
}
@ -142,6 +143,7 @@ struct InfoResult<'a> {
struct ListResult<'a> {
master_timeout: u32,
masters: &'a [Box<str>],
filter: &'a str,
servers: &'a [&'a str],
}
@ -199,7 +201,12 @@ fn cmp_address(a: &str, b: &str) -> cmp::Ordering {
}
}
fn query_servers(host: &str, timeout: Duration, tx: &mpsc::Sender<Message>) -> Result<(), Error> {
fn query_servers(
host: &str,
cli: &Cli,
timeout: Duration,
tx: &mpsc::Sender<Message>,
) -> Result<(), Error> {
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(host)?;
@ -207,12 +214,7 @@ fn query_servers(host: &str, timeout: Duration, tx: &mpsc::Sender<Message>) -> R
let p = game::QueryServers {
region: server::Region::RestOfTheWorld,
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0),
filter: filter::Filter {
gamedir: b"valve",
clver: filter::Version::new(0, 20),
// TODO: filter
..Default::default()
},
filter: cli.filter.as_str(),
};
let n = p.encode(&mut buf)?;
sock.send(&buf[..n])?;
@ -282,7 +284,7 @@ fn get_server_info(
Ok(ServerResult::protocol(addr))
}
fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
fn query_server_info(cli: &Arc<Cli>, servers: &[String]) -> Result<(), Error> {
let (tx, rx) = mpsc::channel();
let mut workers = 0;
@ -291,8 +293,9 @@ fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
let master = i.to_owned();
let tx = tx.clone();
let timeout = Duration::from_secs(cli.master_timeout as u64);
let cli = cli.clone();
thread::spawn(move || {
if let Err(e) = query_servers(&master, timeout, &tx) {
if let Err(e) = query_servers(&master, &cli, timeout, &tx) {
eprintln!("master({}) error: {}", master, e);
}
tx.send(Message::End).unwrap();
@ -341,6 +344,7 @@ fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
master_timeout: cli.master_timeout,
server_timeout: cli.server_timeout,
masters: &cli.masters,
filter: &cli.filter,
servers: &servers,
};
@ -408,7 +412,7 @@ fn query_server_info(cli: &Cli, servers: &[String]) -> Result<(), Error> {
Ok(())
}
fn list_servers(cli: &Cli) -> Result<(), Error> {
fn list_servers(cli: &Arc<Cli>) -> Result<(), Error> {
let (tx, rx) = mpsc::channel();
let mut workers = 0;
@ -416,8 +420,9 @@ fn list_servers(cli: &Cli) -> Result<(), Error> {
let master = i.to_owned();
let tx = tx.clone();
let timeout = Duration::from_secs(cli.master_timeout as u64);
let cli = cli.clone();
thread::spawn(move || {
if let Err(e) = query_servers(&master, timeout, &tx) {
if let Err(e) = query_servers(&master, &cli, timeout, &tx) {
eprintln!("master({}) error: {}", master, e);
}
tx.send(Message::End).unwrap();
@ -448,6 +453,7 @@ fn list_servers(cli: &Cli) -> Result<(), Error> {
let result = ListResult {
master_timeout: cli.master_timeout,
masters: &cli.masters,
filter: &cli.filter,
servers: &servers,
};
@ -468,6 +474,7 @@ fn list_servers(cli: &Cli) -> Result<(), Error> {
}
fn execute(cli: Cli) -> Result<(), Error> {
let cli = Arc::new(cli);
match cli.args.get(0).map(|s| s.as_str()).unwrap_or_default() {
"all" | "" => query_server_info(&cli, &[])?,
"info" => query_server_info(&cli, &cli.args[1..])?,