rust/hedgewars-checker/src/main.rs
author unC0Rr
Tue, 10 Sep 2024 13:56:51 +0200
branchtransitional_engine
changeset 16032 31cc1e450273
parent 15980 39ae4ed7de6e
permissions -rw-r--r--
Add maze land generator ported from pascal engine

use anyhow::{anyhow, bail, Result};
use argparse::{ArgumentParser, Store};
use base64::{engine::general_purpose, Engine};
use hedgewars_network_protocol::{
    messages::HwProtocolMessage as ClientMessage, messages::HwServerMessage::*, parser,
};
use ini::Ini;
use log::{debug, info, warn};
use netbuf::Buf;
use std::{io::Write, str::FromStr};
use tokio::time::MissedTickBehavior;
use tokio::{io, io::AsyncWriteExt, net::TcpStream, process::Command, sync::mpsc};

async fn check(executable: &str, data_prefix: &str, buffer: &[String]) -> Result<Vec<String>> {
    let mut replay = tempfile::NamedTempFile::new()?;

    for line in buffer.iter() {
        replay.write_all(&general_purpose::STANDARD.decode(line)?)?;
    }

    let temp_file_path = replay.path();

    let mut home_dir = dirs::home_dir().ok_or(anyhow!("Home path not detected"))?;
    home_dir.push(".hedgewars");

    debug!("Checking replay in {}", temp_file_path.to_string_lossy());

    let output = Command::new(executable)
        .arg("--user-prefix")
        .arg(&home_dir)
        .arg("--prefix")
        .arg(data_prefix)
        .arg("--nomusic")
        .arg("--nosound")
        .arg("--stats-only")
        .arg(temp_file_path)
        //.spawn()?
        //.wait_with_output()
        .output()
        .await?;

    debug!("Engine finished!");

    let mut result = Vec::new();

    let mut engine_lines = output
        .stderr
        .split(|b| *b == b'\n')
        .skip_while(|l| *l != b"WINNERS" && *l != b"DRAW");

    // debug!("Engine lines: {:?}", &engine_lines);

    loop {
        match engine_lines.next() {
            Some(b"DRAW") => result.push("DRAW".to_owned()),
            Some(b"WINNERS") => {
                result.push("WINNERS".to_owned());
                let winners = engine_lines.next().unwrap();
                let winners_num = u32::from_str(&String::from_utf8(winners.to_vec())?)?;
                result.push(String::from_utf8(winners.to_vec())?);

                for _i in 0..winners_num {
                    result.push(String::from_utf8(engine_lines.next().unwrap().to_vec())?);
                }
            }
            Some(b"GHOST_POINTS") => {
                result.push("GHOST_POINTS".to_owned());
                let points = engine_lines.next().unwrap();
                let points_num = u32::from_str(&String::from_utf8(points.to_vec())?)? * 2;
                result.push(String::from_utf8(points.to_vec())?);

                for _i in 0..points_num {
                    result.push(String::from_utf8(engine_lines.next().unwrap().to_vec())?);
                }
            }
            Some(b"ACHIEVEMENT") => {
                result.push("ACHIEVEMENT".to_owned());
                for _i in 0..4 {
                    result.push(String::from_utf8(engine_lines.next().unwrap().to_vec())?);
                }
            }
            _ => break,
        }
    }

    // println!("Engine lines: {:?}", &result);

    if !result.is_empty() {
        Ok(result)
    } else {
        bail!("no data from engine")
    }
}

async fn check_loop(
    executable: &str,
    data_prefix: &str,
    results_sender: mpsc::Sender<Result<Vec<String>>>,
    mut replay_receiver: mpsc::Receiver<Vec<String>>,
) -> Result<()> {
    while let Some(replay) = replay_receiver.recv().await {
        results_sender
            .send(check(executable, data_prefix, &replay).await)
            .await?;
    }

    Ok(())
}

async fn connect_and_run(
    username: &str,
    password: &str,
    protocol_number: u16,
    replay_sender: mpsc::Sender<Vec<String>>,
    mut results_receiver: mpsc::Receiver<Result<Vec<String>>>,
) -> Result<()> {
    info!("Connecting...");

    let mut stream = TcpStream::connect("hedgewars.org:46631").await?;

    let mut buf = Buf::new();

    let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(30));
    interval.set_missed_tick_behavior(MissedTickBehavior::Delay);

    loop {
        let r = tokio::select! {
            _ = interval.tick() => {
                // Send Ping
                stream.write_all(ClientMessage::Ping.to_raw_protocol().as_bytes()).await?;
                None
            },
            _ = stream.readable() => None,
            r = results_receiver.recv() => r
        };

        //println!("Loop: {:?}", &r);

        if let Some(execution_result) = r {
            match execution_result {
                Ok(result) => {
                    info!("Checked");
                    debug!("Check result: [{:?}]", result);

                    stream
                        .write_all(
                            ClientMessage::CheckedOk(result)
                                .to_raw_protocol()
                                .as_bytes(),
                        )
                        .await?;
                    stream
                        .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
                        .await?;
                }
                Err(e) => {
                    info!("Check failed: {:?}", e);
                    stream
                        .write_all(
                            ClientMessage::CheckedFail("error".to_owned())
                                .to_raw_protocol()
                                .as_bytes(),
                        )
                        .await?;
                    stream
                        .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
                        .await?;
                }
            }
        } else {
            let mut msg = [0; 4096];
            // Try to read data, this may still fail with `WouldBlock`
            // if the readiness event is a false positive.
            match stream.try_read(&mut msg) {
                Ok(n) => {
                    //println!("{:?}", &msg);
                    buf.write_all(&msg[0..n])?;
                }
                Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {}
                Err(e) => {
                    return Err(e.into());
                }
            }
        }

        while let Ok((tail, msg)) = parser::server_message(buf.as_ref()) {
            let tail_len = tail.len();
            buf.consume(buf.len() - tail_len);

            // println!("Message from server: {:?}", &msg);

            match msg {
                Connected(_, _) => {
                    info!("Connected");
                    stream
                        .write_all(
                            ClientMessage::Checker(
                                protocol_number,
                                username.to_owned(),
                                password.to_owned(),
                            )
                            .to_raw_protocol()
                            .as_bytes(),
                        )
                        .await?;
                }
                Ping => {
                    stream
                        .write_all(ClientMessage::Pong.to_raw_protocol().as_bytes())
                        .await?;
                }
                Pong => {
                    // do nothing
                }
                LogonPassed => {
                    stream
                        .write_all(ClientMessage::CheckerReady.to_raw_protocol().as_bytes())
                        .await?;
                }
                Replay(lines) => {
                    info!("Got a replay");
                    replay_sender.send(lines).await?;
                }
                Bye(message) => {
                    warn!("Received BYE: {}", message);
                    return Ok(());
                }
                ChatMsg { nick, msg } => {
                    info!("Chat [{}]: {}", nick, msg);
                }
                RoomAdd(fields) => {
                    let mut l = fields.into_iter();
                    info!("Room added: {}", l.nth(1).unwrap());
                }
                RoomUpdated(name, fields) => {
                    let mut l = fields.into_iter();
                    let new_name = l.nth(1).unwrap();

                    if name != new_name {
                        info!("Room renamed: {}", new_name);
                    }
                }
                RoomRemove(_) => {
                    // ignore
                }
                Error(message) => {
                    warn!("Received ERROR: {}", message);
                    return Ok(());
                }
                something => {
                    warn!("Unexpected protocol command: {:?}", something)
                }
            }
        }
    }
}

async fn get_protocol_number(executable: &str) -> Result<u16> {
    let output = Command::new(executable).arg("--protocol").output().await?;

    Ok(u16::from_str(String::from_utf8(output.stdout)?.trim()).unwrap_or(55))
}

#[tokio::main]
async fn main() -> Result<()> {
    stderrlog::new()
        .verbosity(3)
        .timestamp(stderrlog::Timestamp::Second)
        .module(module_path!())
        .init()?;

    let mut frontend_settings = dirs::home_dir().ok_or(anyhow!("Home path not detected"))?;
    frontend_settings.push(".hedgewars/settings.ini");

    let i = Ini::load_from_file(frontend_settings.to_str().unwrap()).unwrap();
    let username = i
        .get_from(Some("net"), "nick")
        .ok_or(anyhow!("Nickname not found in frontend config"))?;
    let password = i
        .get_from(Some("net"), "passwordhash")
        .ok_or(anyhow!("Password not found in frontend config"))?;

    let mut exe = "/usr/local/bin/hwengine".to_string();
    let mut prefix = "/usr/local/share/hedgewars/Data".to_string();
    {
        let mut ap = ArgumentParser::new();
        ap.set_description("Game replay checker for hedgewars.");
        ap.refer(&mut exe)
            .add_option(&["--exe"], Store, "Path to hwengine executable");
        ap.refer(&mut prefix)
            .add_option(&["--prefix"], Store, "Path main Data dir");
        ap.parse_args_or_exit();
    }

    info!("Executable: {}", exe);
    info!("Data dir: {}", prefix);

    let protocol_number = get_protocol_number(exe.as_str()).await?;

    info!("Using protocol number {}", protocol_number);

    let (replay_sender, replay_receiver) = mpsc::channel(1);
    let (results_sender, results_receiver) = mpsc::channel(1);

    let (network_result, checker_result) = tokio::join!(
        connect_and_run(
            username,
            password,
            protocol_number,
            replay_sender,
            results_receiver
        ),
        check_loop(&exe, &prefix, results_sender, replay_receiver)
    );

    network_result?;
    checker_result
}