From: Ian Jackson Date: Wed, 2 Jun 2021 22:59:39 +0000 (+0100) Subject: Break out functions into manipgame.rs X-Git-Tag: otter-0.7.0~84 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=commitdiff_plain;h=cfb30d6d6e277f96b6a11b2996802985f30f62e7;p=otter.git Break out functions into manipgame.rs Signed-off-by: Ian Jackson --- diff --git a/cli/manipgame.rs b/cli/manipgame.rs new file mode 100644 index 00000000..235136df --- /dev/null +++ b/cli/manipgame.rs @@ -0,0 +1,398 @@ +// Copyright 2020-2021 Ian Jackson and contributors to Otter +// SPDX-License-Identifier: AGPL-3.0-or-later +// There is NO WARRANTY. + +use super::*; + +//---------- list-games ---------- + +mod list_games { + use super::*; + + #[derive(Default,Debug)] + struct Args { + all: bool, + } + + fn subargs(sa: &mut Args) -> ArgumentParser { + use argparse::*; + let mut ap = ArgumentParser::new(); + ap.refer(&mut sa.all) + .add_option(&["--all"],StoreTrue, + "user superuser access to list *all* games"); + ap + } + + fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> { + let args = parse_args::(args, &subargs, &ok_id, None); + let mut conn = connect(&ma)?; + let mut games = match conn.cmd(&MC::ListGames { all: Some(args.all) })? { + MR::GamesList(g) => g, + x => throw!(anyhow!("unexpected response to ListGames: {:?}", &x)), + }; + games.sort(); + for g in games { + writeln!(out, "{}", &g)?; + } + Ok(()) + } + + inventory_subcmd!{ + "list-games", + "List games", + } +} + +// todo: list-players +// todo: delete-account + +//---------- reset-game ---------- + +mod reset_game { + use super::*; + + #[derive(Default,Debug)] + struct Args { + table_file: Option, + game_spec: String, + bundles: Vec, + bundles_only: bool, + } + + fn subargs(sa: &mut Args) -> ArgumentParser { + use argparse::*; + let mut ap = ArgumentParser::new(); + ap.refer(&mut sa.table_file).metavar("TABLE-SPEC[-TOML]") + .add_option(&["--reset-table"],StoreOption, + "reset the players and access too"); + ap.refer(&mut sa.game_spec).required() + .add_argument("GAME-SPEC",Store, + "game spec, as found in server, \ + or local filename if it contains a '/')"); + ap.refer(&mut sa.bundles).required() + .add_argument("BUNDLES",Collect, + "Bundle files to use. If any are specified, \ + all needed bundles must be specified, as any \ + not mentioned will be cleared from the server."); + let mut bundles_only = ap.refer(&mut sa.bundles_only); + bundles_only.add_option(&["--bundles-only"],StoreTrue, + "insist that server has only the specified BUNDLES \ + (clearing out the server if no BUNDLES were specified)"); + bundles_only.add_option(&["--bundles-at-least"],StoreFalse, + "don't care if the server has additional bundles uploaded \ + earlier (default)"); + ap + } + + fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> { + let args = parse_args::(args, &subargs, &ok_id, None); + let instance_name = ma.instance(); + let mut chan = ma.access_game()?; + + let reset_insn = + if let Some(filename) = spec_arg_is_path(&args.game_spec) { + let spec_toml = read_spec_from_path( + filename, SpecRaw::::new())?; + MGI::ResetFromGameSpec { spec_toml } + } else { + MGI::ResetFromNamedSpec { spec: args.game_spec.clone() } + }; + + let mut insns = vec![]; + + if let Some(table_file) = args.table_file { + let table_spec = read_spec(&ma, &table_file, SpecParseToml::new())?; + let game = chan.game.clone(); + chan.cmd(&MgmtCommand::CreateGame { + game, + insns: vec![], + }).map(|_|()).or_else(|e| { + if let Some(&MgmtError::AlreadyExists) = e.downcast_ref() { + return Ok(()) + } + Err(e) + })?; + + insns.extend(setup_table(&ma, &instance_name, &table_spec)?); + } + + if args.bundles_only || args.bundles.len() != 0 { + let local = args.bundles.into_iter().map(|file| { + BundleForUpload::prepare(file) + }).collect::,_>>()?; + + let resp = chan.cmd(&MgmtCommand::ListBundles { game: ma.instance() })?; + let remote = match resp { + MR::Bundles { bundles } => bundles, + x => throw!(anyhow!("unexpected response to ListBundles: {:?}",x)), + }; + + let bundles_only = args.bundles_only; + match Itertools::zip_longest( + local.iter().rev(), + remote.iter().rev(), + ).map(|eob| { + use EitherOrBoth::*; + use bundles::State::*; + match eob { + Right((id, remote)) => if bundles_only { + Err(format!("server has additional bundle(s) eg {} {}", + id, remote)) + } else { + Ok(()) + }, + Left(local) => { + Err(format!("server is missing {} {} {}", + local.kind, local.hash, local.file)) + }, + Both(_local, (id, Uploading)) => { + Err(format!("server has incomplete upload :{}", id)) + }, + Both(local, (id, Loaded(remote))) => { + if (local.size, local.hash) != + (remote.size, remote.hash) { + Err(format!("server's {} does not match {}", id, &local.file)) + } else { + Ok(()) + } + } + } + }).find_map(Result::err).map_or_else(|| Ok(()), Err) { + Ok(()) => { + if ma.verbose >= 0 { + eprintln!("Reusing server's existing bundles"); + } + }, + Err(why) => { + if ma.verbose >= 0 { + eprintln!("Re-uploading bundles: {}", why); + } + if bundles_only { + clear_game(&ma, &mut chan)?; + } + let progress = ma.progressbar()?; + let mut progress = termprogress::Nest::new(local.len(), progress); + for bundle in local { + bundle.upload(&ma, &mut chan, &mut progress)?; + } + }, + } + } + + insns.push(reset_insn); + + chan.alter_game(insns, None)?; + + if ma.verbose >= 0 { + eprintln!("reset successful."); + } + Ok(()) + } + + inventory_subcmd!{ + "reset", + "Reset the state of the game table", + } +} + +//---------- set-link ---------- + +mod set_link { + + use super::*; + + #[derive(Debug,Default)] + struct Args { + kind: Option, + url: Option, + } + + fn subargs(sa: &mut Args) -> ArgumentParser { + use argparse::*; + let mut ap = ArgumentParser::new(); + ap.refer(&mut sa.kind) + .add_argument("LINK-KIND",StoreOption,"link kind"); + ap.refer(&mut sa.url) + .add_argument("URL",StoreOption,"url (or empty for none)"); + ap + } + + #[throws(AE)] + fn call(SCCA{ mut out, ma, args,.. }:SCCA) { + let args = parse_args::(args, &subargs, &ok_id, None); + let mut chan = ma.access_game()?; + + match args.url { + None => { + let MgmtGameResponseGameInfo { links, .. } = chan.info()?; + for (tk, v) in links { + let v: Url = (&v).try_into().context("reparse sererr's UrlSpec")?; + match args.kind { + None => { + writeln!(out, "{:<10} {}", tk, &v)?; + } + Some(wk) => { + if wk == tk { + writeln!(out, "{}", &v)?; + } + } + } + } + }, + + Some(url) => { + let kind = args.kind.unwrap(); + chan.alter_game(vec![ + if url == "" { + MGI::RemoveLink { kind } + } else { + MGI::SetLink { kind, url: UrlSpec(url) } + } + ], None)?; + }, + } + } + + inventory_subcmd!{ + "set-link", + "Set one of the info links visible from within the game", + } +} + +//---------- join-game ---------- + +mod join_game { + use super::*; + + #[derive(Default,Debug)] + struct Args { + reset_access: bool, + } + + fn subargs(sa: &mut Args) -> ArgumentParser { + use argparse::*; + let mut ap = ArgumentParser::new(); + ap.refer(&mut sa.reset_access) + .add_option(&["--reset"],StoreTrue, + "generate and deliver new player access token"); + ap + } + + fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> { + let args = parse_args::(args, &subargs, &ok_id, None); + let mut chan = ma.access_game()?; + + let mut insns = vec![]; + match chan.has_player(&ma.account)? { + None => { + let nick = ma.nick.clone() + .unwrap_or_else(|| ma.account.default_nick()); + let details = MgmtPlayerDetails { nick: Some(nick) }; + insns.push(MGI::JoinGame { details }); + } + Some((player, mpi)) => { + writeln!(out, "already in game, as player #{} {:?}", + player.0.get_idx_version().0, &mpi.nick)?; + let MgmtPlayerInfo { nick, account:_ } = mpi; + if let Some(new_nick) = &ma.nick { + if &nick != new_nick { + writeln!(out, "changing nick to {:?}", &new_nick)?; + let details = MgmtPlayerDetails { nick: ma.nick.clone() }; + insns.push(MGI::UpdatePlayer { player, details }); + } + } + if args.reset_access { + writeln!(out, "resetting access token (invalidating other URLs)")?; + insns.push(MGI::ResetPlayerAccess(player)); + } else { + writeln!(out, "redelivering existing access token")?; + insns.push(MGI::RedeliverPlayerAccess(player)); + } + } + }; + + fn deliver(out: &mut CookedStdout, token: &AccessTokenReport) { + for l in &token.lines { + if l.contains(char::is_control) { + writeln!(out, "Server token info contains control chars! {:?}", &l) + } else { + writeln!(out, " {}", &l) + }.unwrap() + } + } + + for resp in chan.alter_game(insns, None)? { + match resp { + MGR::JoinGame { nick, player, token } => { + writeln!(out, "joined game as player #{} {:?}", + player.0.get_idx_version().0, + &nick)?; + deliver(&mut out, &token); + } + MGR::PlayerAccessToken(token) => { + deliver(&mut out, &token); + } + MGR::Fine => {} + _ => throw!(anyhow!("unexpected response to instruction(s)")), + } + } + + Ok(()) + } + + inventory_subcmd!{ + "join-game", + "Join a game or reset access token (creating or updating account)", + } +} + +//---------- leave-game ---------- + +mod leave_game { + use super::*; + + type Args = NoArgs; + + fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> { + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; + + let player = match chan.has_player(&ma.account)? { + None => { + writeln!(out, "this account is not a player in that game")?; + exit(EXIT_NOTFOUND); + } + Some((player, _)) => player, + }; + + chan.alter_game(vec![MGI::LeaveGame(player)], None)?; + + Ok(()) + } + + inventory_subcmd!{ + "leave-game", + "Leave a game", + } +} + +//---------- delete-game ---------- + +mod delete_game { + use super::*; + + type Args = NoArgs; + + fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> { + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; + let game = chan.game.clone(); + chan.cmd(&MC::DestroyGame { game })?; + Ok(()) + } + + inventory_subcmd!{ + "delete-game", + "Delete a game (throwing all the players out of it)", + } +} diff --git a/cli/otter.rs b/cli/otter.rs index bce86444..d7b01e0f 100644 --- a/cli/otter.rs +++ b/cli/otter.rs @@ -29,6 +29,8 @@ pub use argparse::action::ParseResult::Parsed; pub mod clisupport; use clisupport::*; +mod manipgame; + #[derive(Debug)] enum ServerLocation { Socket(String), @@ -318,399 +320,6 @@ fn main() { .unwrap_or_else(|e| e.end_process(12)); } -//---------- list-games ---------- - -mod list_games { - use super::*; - - #[derive(Default,Debug)] - struct Args { - all: bool, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.all) - .add_option(&["--all"],StoreTrue, - "user superuser access to list *all* games"); - ap - } - - fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> { - let args = parse_args::(args, &subargs, &ok_id, None); - let mut conn = connect(&ma)?; - let mut games = match conn.cmd(&MC::ListGames { all: Some(args.all) })? { - MR::GamesList(g) => g, - x => throw!(anyhow!("unexpected response to ListGames: {:?}", &x)), - }; - games.sort(); - for g in games { - writeln!(out, "{}", &g)?; - } - Ok(()) - } - - inventory_subcmd!{ - "list-games", - "List games", - } -} - -// todo: list-players -// todo: delete-account - -//---------- reset-game ---------- - -mod reset_game { - use super::*; - - #[derive(Default,Debug)] - struct Args { - table_file: Option, - game_spec: String, - bundles: Vec, - bundles_only: bool, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_file).metavar("TABLE-SPEC[-TOML]") - .add_option(&["--reset-table"],StoreOption, - "reset the players and access too"); - ap.refer(&mut sa.game_spec).required() - .add_argument("GAME-SPEC",Store, - "game spec, as found in server, \ - or local filename if it contains a '/')"); - ap.refer(&mut sa.bundles).required() - .add_argument("BUNDLES",Collect, - "Bundle files to use. If any are specified, \ - all needed bundles must be specified, as any \ - not mentioned will be cleared from the server."); - let mut bundles_only = ap.refer(&mut sa.bundles_only); - bundles_only.add_option(&["--bundles-only"],StoreTrue, - "insist that server has only the specified BUNDLES \ - (clearing out the server if no BUNDLES were specified)"); - bundles_only.add_option(&["--bundles-at-least"],StoreFalse, - "don't care if the server has additional bundles uploaded \ - earlier (default)"); - ap - } - - fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> { - let args = parse_args::(args, &subargs, &ok_id, None); - let instance_name = ma.instance(); - let mut chan = ma.access_game()?; - - let reset_insn = - if let Some(filename) = spec_arg_is_path(&args.game_spec) { - let spec_toml = read_spec_from_path( - filename, SpecRaw::::new())?; - MGI::ResetFromGameSpec { spec_toml } - } else { - MGI::ResetFromNamedSpec { spec: args.game_spec.clone() } - }; - - let mut insns = vec![]; - - if let Some(table_file) = args.table_file { - let table_spec = read_spec(&ma, &table_file, SpecParseToml::new())?; - let game = chan.game.clone(); - chan.cmd(&MgmtCommand::CreateGame { - game, - insns: vec![], - }).map(|_|()).or_else(|e| { - if let Some(&MgmtError::AlreadyExists) = e.downcast_ref() { - return Ok(()) - } - Err(e) - })?; - - insns.extend(setup_table(&ma, &instance_name, &table_spec)?); - } - - if args.bundles_only || args.bundles.len() != 0 { - let local = args.bundles.into_iter().map(|file| { - BundleForUpload::prepare(file) - }).collect::,_>>()?; - - let resp = chan.cmd(&MgmtCommand::ListBundles { game: ma.instance() })?; - let remote = match resp { - MR::Bundles { bundles } => bundles, - x => throw!(anyhow!("unexpected response to ListBundles: {:?}",x)), - }; - - let bundles_only = args.bundles_only; - match Itertools::zip_longest( - local.iter().rev(), - remote.iter().rev(), - ).map(|eob| { - use EitherOrBoth::*; - use bundles::State::*; - match eob { - Right((id, remote)) => if bundles_only { - Err(format!("server has additional bundle(s) eg {} {}", - id, remote)) - } else { - Ok(()) - }, - Left(local) => { - Err(format!("server is missing {} {} {}", - local.kind, local.hash, local.file)) - }, - Both(_local, (id, Uploading)) => { - Err(format!("server has incomplete upload :{}", id)) - }, - Both(local, (id, Loaded(remote))) => { - if (local.size, local.hash) != - (remote.size, remote.hash) { - Err(format!("server's {} does not match {}", id, &local.file)) - } else { - Ok(()) - } - } - } - }).find_map(Result::err).map_or_else(|| Ok(()), Err) { - Ok(()) => { - if ma.verbose >= 0 { - eprintln!("Reusing server's existing bundles"); - } - }, - Err(why) => { - if ma.verbose >= 0 { - eprintln!("Re-uploading bundles: {}", why); - } - if bundles_only { - clear_game(&ma, &mut chan)?; - } - let progress = ma.progressbar()?; - let mut progress = termprogress::Nest::new(local.len(), progress); - for bundle in local { - bundle.upload(&ma, &mut chan, &mut progress)?; - } - }, - } - } - - insns.push(reset_insn); - - chan.alter_game(insns, None)?; - - if ma.verbose >= 0 { - eprintln!("reset successful."); - } - Ok(()) - } - - inventory_subcmd!{ - "reset", - "Reset the state of the game table", - } -} - -//---------- set-link ---------- - -mod set_link { - - use super::*; - - #[derive(Debug,Default)] - struct Args { - kind: Option, - url: Option, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.kind) - .add_argument("LINK-KIND",StoreOption,"link kind"); - ap.refer(&mut sa.url) - .add_argument("URL",StoreOption,"url (or empty for none)"); - ap - } - - #[throws(AE)] - fn call(SCCA{ mut out, ma, args,.. }:SCCA) { - let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = ma.access_game()?; - - match args.url { - None => { - let MgmtGameResponseGameInfo { links, .. } = chan.info()?; - for (tk, v) in links { - let v: Url = (&v).try_into().context("reparse sererr's UrlSpec")?; - match args.kind { - None => { - writeln!(out, "{:<10} {}", tk, &v)?; - } - Some(wk) => { - if wk == tk { - writeln!(out, "{}", &v)?; - } - } - } - } - }, - - Some(url) => { - let kind = args.kind.unwrap(); - chan.alter_game(vec![ - if url == "" { - MGI::RemoveLink { kind } - } else { - MGI::SetLink { kind, url: UrlSpec(url) } - } - ], None)?; - }, - } - } - - inventory_subcmd!{ - "set-link", - "Set one of the info links visible from within the game", - } -} - -//---------- join-game ---------- - -mod join_game { - use super::*; - - #[derive(Default,Debug)] - struct Args { - reset_access: bool, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.reset_access) - .add_option(&["--reset"],StoreTrue, - "generate and deliver new player access token"); - ap - } - - fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> { - let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = ma.access_game()?; - - let mut insns = vec![]; - match chan.has_player(&ma.account)? { - None => { - let nick = ma.nick.clone() - .unwrap_or_else(|| ma.account.default_nick()); - let details = MgmtPlayerDetails { nick: Some(nick) }; - insns.push(MGI::JoinGame { details }); - } - Some((player, mpi)) => { - writeln!(out, "already in game, as player #{} {:?}", - player.0.get_idx_version().0, &mpi.nick)?; - let MgmtPlayerInfo { nick, account:_ } = mpi; - if let Some(new_nick) = &ma.nick { - if &nick != new_nick { - writeln!(out, "changing nick to {:?}", &new_nick)?; - let details = MgmtPlayerDetails { nick: ma.nick.clone() }; - insns.push(MGI::UpdatePlayer { player, details }); - } - } - if args.reset_access { - writeln!(out, "resetting access token (invalidating other URLs)")?; - insns.push(MGI::ResetPlayerAccess(player)); - } else { - writeln!(out, "redelivering existing access token")?; - insns.push(MGI::RedeliverPlayerAccess(player)); - } - } - }; - - fn deliver(out: &mut CookedStdout, token: &AccessTokenReport) { - for l in &token.lines { - if l.contains(char::is_control) { - writeln!(out, "Server token info contains control chars! {:?}", &l) - } else { - writeln!(out, " {}", &l) - }.unwrap() - } - } - - for resp in chan.alter_game(insns, None)? { - match resp { - MGR::JoinGame { nick, player, token } => { - writeln!(out, "joined game as player #{} {:?}", - player.0.get_idx_version().0, - &nick)?; - deliver(&mut out, &token); - } - MGR::PlayerAccessToken(token) => { - deliver(&mut out, &token); - } - MGR::Fine => {} - _ => throw!(anyhow!("unexpected response to instruction(s)")), - } - } - - Ok(()) - } - - inventory_subcmd!{ - "join-game", - "Join a game or reset access token (creating or updating account)", - } -} - -//---------- leave-game ---------- - -mod leave_game { - use super::*; - - type Args = NoArgs; - - fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> { - let _args = parse_args::(args, &noargs, &ok_id, None); - let mut chan = ma.access_game()?; - - let player = match chan.has_player(&ma.account)? { - None => { - writeln!(out, "this account is not a player in that game")?; - exit(EXIT_NOTFOUND); - } - Some((player, _)) => player, - }; - - chan.alter_game(vec![MGI::LeaveGame(player)], None)?; - - Ok(()) - } - - inventory_subcmd!{ - "leave-game", - "Leave a game", - } -} - -//---------- delete-game ---------- - -mod delete_game { - use super::*; - - type Args = NoArgs; - - fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> { - let _args = parse_args::(args, &noargs, &ok_id, None); - let mut chan = ma.access_game()?; - let game = chan.game.clone(); - chan.cmd(&MC::DestroyGame { game })?; - Ok(()) - } - - inventory_subcmd!{ - "delete-game", - "Delete a game (throwing all the players out of it)", - } -} - //---------- library-list ---------- #[derive(Debug,Default)]