gcmh 1.2.1

A CLI tool to improve your custom maps on generals.io.
Documentation
use crate::{
    constants::{CONCURRENT_PLAYS, GAME_SPEED, GAME_URL, SURRENDER_TURN, WS_URL},
    upvote::get,
    utils::{join_private, random_user_id, set_custom_map, set_force_start, set_pb, set_speed},
    GameUpdate,
};
use anyhow::Result;
use log::{debug, info};
use rust_socketio::{ClientBuilder, Payload};
use serde_json::json;
use std::{
    sync::{
        atomic::{AtomicBool, Ordering},
        mpsc::{self, Sender},
        Arc,
    },
    thread,
    time::Duration,
};

fn bot(room: &str, map: &str, should_report: bool, tx: Sender<()>) -> Result<()> {
    let room = room.to_owned();
    let room_clone = room.to_owned();
    let room_clone_2 = room.to_owned();

    let map = map.to_owned();

    let should_vote = Arc::new(AtomicBool::new(true));
    let should_vote_clone = should_vote.clone();
    let should_vote_clone_2 = should_vote.clone();

    ClientBuilder::new(WS_URL)
        .on("open", move |_, socket| {
            debug!("bot connected");
            join_private(&socket, &random_user_id(), &room).ok();
        })
        .on("close", move |_, _| {
            debug!("bot disconnected");
            should_vote.store(true, Ordering::Relaxed);
        })
        .on("queue_update", move |_, socket| {
            if should_vote_clone
                .compare_exchange(true, false, Ordering::Relaxed, Ordering::Relaxed)
                .is_ok()
            {
                set_custom_map(&socket, &room_clone, &map)
                    .and_then(|_| set_speed(&socket, &room_clone, GAME_SPEED))
                    .and_then(|_| set_force_start(&socket, &room_clone))
                    .ok();
            }
        })
        .on("game_update", move |payload, socket| {
            if let Payload::String(s) = payload {
                let GameUpdate { turn } = serde_json::from_str(&s).unwrap();
                if turn >= SURRENDER_TURN {
                    socket.emit("surrender", json!(())).ok();
                }
            }
        })
        .on("game_over", move |_, socket| {
            should_vote_clone_2.store(true, Ordering::Relaxed);

            if should_report && tx.send(()).is_err() {
                socket.disconnect().unwrap();
            }

            join_private(&socket, &random_user_id(), &room_clone_2).ok();
        })
        .connect()?;

    Ok(())
}

pub fn play(map: &str, count: u64) -> Result<()> {
    get(map)?;

    let (tx, rx) = mpsc::channel();

    info!("playing map `{map}` with speed {GAME_SPEED} and turns {SURRENDER_TURN}...");

    for _ in 0..CONCURRENT_PLAYS {
        let map = map.to_owned();
        let tx = tx.clone();

        thread::spawn(move || {
            let room = random_user_id();
            debug!("starting a game at {GAME_URL}/{room}");
            bot(&room, &map, true, tx.clone()).unwrap();
            bot(&room, &map, false, tx).unwrap();
        });
    }

    let pb = set_pb(count);

    let mut cnt = 0;

    loop {
        if rx.try_recv().is_ok() {
            cnt += 1;
        }

        pb.set_position(cnt);

        if cnt >= count {
            debug!("shutting down...");
            break;
        }

        thread::sleep(Duration::from_millis(161));
    }

    info!("successfully played map for {count} times. Fetching new data...");

    get(map)?;

    Ok(())
}