chiark / gitweb /
7ace404226951cfbfbc5229ddbea5f898aa37268
[otter.git] / cli / forgame.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use super::*;
6 use super::usebundles::*;
7
8 // todo: list-players
9
10 //---------- reset ----------
11
12 mod reset_game {
13   use super::*;
14
15   #[derive(Default,Debug)]
16   struct Args {
17     table_file: Option<String>,
18     game_spec: String,
19     bundles: Vec<String>,
20     bundles_only: bool,
21   }
22
23   fn subargs(sa: &mut Args) -> ArgumentParser {
24     use argparse::*;
25     let mut ap = ArgumentParser::new();
26     ap.refer(&mut sa.table_file).metavar("TABLE-SPEC[-TOML")
27       .add_option(&["--reset-table"],StoreOption,
28                   "reset the players and access too");
29     ap.refer(&mut sa.game_spec).required()
30       .add_argument("GAME-SPEC",Store,
31                     "game spec, as found in server, \
32                      or local filename if it contains a '/')");
33     ap.refer(&mut sa.bundles).required()
34       .add_argument("BUNDLES",Collect,
35                     "Bundle files to use.  If any are specified, \
36                      all needed bundles must be specified, as any \
37                      not mentioned will be cleared from the server.");
38     let mut bundles_only = ap.refer(&mut sa.bundles_only);
39     bundles_only.add_option(&["--bundles-only"],StoreTrue,
40               "insist that server has only the specified BUNDLES \
41                (clearing out the server if no BUNDLES were specified)");
42     bundles_only.add_option(&["--bundles-at-least"],StoreFalse,
43               "don't care if the server has additional bundles uploaded \
44                earlier (default)");
45     ap
46   }
47
48   fn call(SCCA{ ma, args, mut out,.. }:SCCA) -> Result<(),AE> {
49     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
50     let instance_name = ma.instance();
51     let mut chan = ma.access_game()?;
52
53     let reset_insn =
54       if let Some(filename) = spec_arg_is_path(&args.game_spec) {
55         let spec_toml = read_spec_from_path(
56           filename, SpecRaw::<GameSpec>::new())?;
57
58         let spec_toml = bundles::spec_macroexpand(spec_toml, &mut |what,data|{
59           if ma.verbose >= 2 {
60             for (lno,l) in data.split('\n').enumerate() {
61               writeln!(out, "spec {} {} {}", what, lno+1, l)?;
62             }
63           }
64           Ok(())
65         })
66           .context("failed to template expand game spec")?;
67
68         MGI::ResetFromGameSpec { spec_toml }
69       } else {
70         MGI::ResetFromNamedSpec { spec: args.game_spec.clone() }
71       };
72
73     let mut insns = vec![];
74
75     if let Some(table_file) = args.table_file {
76       let table_spec = read_spec(&ma, &table_file, SpecParseToml::new())?;
77       let game = chan.game.clone();
78       chan.cmd(&MgmtCommand::CreateGame {
79         game,
80         insns: vec![],
81       }).map(|_|()).or_else(|e| {
82         if let Some(&MgmtError::AlreadyExists) = e.downcast_ref() {
83           return Ok(())
84         }
85         Err(e)
86       })?;
87
88       insns.extend(setup_table(&ma, &instance_name, &table_spec, true)?);
89     }
90
91     if args.bundles_only || args.bundles.len() != 0 {
92       let n_bundles = args.bundles.len();
93       let progress = ma.progressbar()?;
94       let mut progress = termprogress::Nest::with_total
95         (n_bundles as f32, progress);
96       let bundle_i_msg = |i| Some(format!("{}/{}", i, n_bundles));
97
98       let local = args.bundles.into_iter().enumerate().map(|(i,file)| {
99         progress.start_phase(0., bundle_i_msg(i), "preparing".into());
100         BundleForUpload::prepare(file, &mut progress)
101       }).collect::<Result<Vec<_>,_>>()?;
102
103       let resp = chan.cmd(&MgmtCommand::ListBundles { game: ma.instance() })?;
104       let remote = match resp {
105         MR::Bundles { bundles } => bundles,
106         x => throw!(anyhow!("unexpected response to ListBundles: {:?}",x)),
107       };
108       progress.clear();
109
110       let bundles_only = args.bundles_only;
111       match Itertools::zip_longest(
112         local.iter().rev(),
113         remote.iter().rev(),
114       ).map(|eob| {
115         use EitherOrBoth::*;
116         use bundles::State::*;
117         match eob {
118           Right((id, remote)) => if bundles_only {
119             Err(format!("server has additional bundle(s) eg {} {}",
120                         id, remote))
121           } else {
122             Ok(())
123           },
124           Left(local) => {
125             Err(format!("server is missing {} {} {}",
126                         local.kind, local.hash, local.file))
127           },
128           Both(_local, (id, Uploading)) => {
129             Err(format!("server has incomplete upload :{}", id))
130           },
131           Both(local, (id, Loaded(remote))) => {
132             if (local.size,  local.hash) !=
133                (remote.size, remote.hash) {
134                Err(format!("server's {} does not match {}", id, &local.file))
135             } else {
136                Ok(())
137             }
138           }
139         }
140       }).find_map(Result::err).map_or_else(|| Ok(()), Err) {
141         Ok(()) => {
142           if ma.verbose >= 0 {
143             eprintln!("Reusing server's existing bundles");
144           }
145         },
146         Err(why) => {
147           if ma.verbose >= 0 {
148             eprintln!("Re-uploading bundles: {}", why);
149           }
150           if bundles_only {
151             clear_game(&ma, &mut chan)?;
152           }
153           for (i, bundle) in local.into_iter().enumerate() {
154             progress.start_phase(PROGFRAC_UPLOAD,
155                                  bundle_i_msg(i),
156                                  "uploading".into());
157             bundle.upload(&ma, &mut chan, &mut progress)?;
158           }
159         },
160       }
161     }
162
163     insns.push(reset_insn);
164
165     chan.alter_game(insns, None)?;
166
167     if ma.verbose >= 0 {
168       eprintln!("reset successful.");
169     }
170     Ok(())
171   }
172
173   inventory_subcmd!{
174     "reset",
175     "Reset the state of the game table",
176   }
177 }
178
179 //---------- set-link ----------
180
181 mod set_link {
182
183   use super::*;
184
185   #[derive(Debug,Default)]
186   struct Args {
187     kind: Option<LinkKind>,
188     url: Option<String>,
189   }
190
191   fn subargs(sa: &mut Args) -> ArgumentParser {
192     use argparse::*;
193     let mut ap = ArgumentParser::new();
194     ap.refer(&mut sa.kind)
195       .add_argument("LINK-KIND",StoreOption,"link kind");
196     ap.refer(&mut sa.url)
197       .add_argument("URL",StoreOption,"url (or empty for none)");
198     ap
199   }
200
201   #[throws(AE)]
202   fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
203     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
204     let mut chan = ma.access_game()?;
205
206     match args.url {
207       None => {
208         let MgmtGameResponseGameInfo { links, .. } = chan.info()?;
209         for (tk, v) in links {
210           let v: Url = (&v).try_into().context("reparse sererr's UrlSpec")?;
211           match args.kind {
212             None => {
213               writeln!(out, "{:<10} {}", tk, &v)?;
214             }
215             Some(wk) => {
216               if wk == tk {
217                 writeln!(out, "{}", &v)?;
218               }
219             }
220           }
221         }
222       },
223
224       Some(url) => {
225         let kind = args.kind.unwrap();
226         chan.alter_game(vec![
227           if url == "" {
228             MGI::RemoveLink { kind }
229           } else {
230             MGI::SetLink { kind, url: UrlSpec(url) }
231           }
232         ], None)?;
233       },
234     }
235   }
236
237   inventory_subcmd!{
238     "set-link",
239     "Set one of the info links visible from within the game",
240   }
241 }
242
243 //---------- join-game ----------
244
245 mod join_game {
246   use super::*;
247
248   #[derive(Default,Debug)]
249   struct Args {
250     reset_access: bool,
251   }
252
253   fn subargs(sa: &mut Args) -> ArgumentParser {
254     use argparse::*;
255     let mut ap = ArgumentParser::new();
256     ap.refer(&mut sa.reset_access)
257       .add_option(&["--reset"],StoreTrue,
258                   "generate and deliver new player access token");
259     ap
260   }
261
262   fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
263     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
264     let mut chan = ma.access_game()?;
265
266     let mut insns = vec![];
267     match chan.has_player(&ma.account)? {
268       None => {
269         let nick = ma.nick.clone()
270           .unwrap_or_else(|| ma.account.default_nick());
271         let details = MgmtPlayerDetails { nick: Some(nick) };
272         insns.push(MGI::JoinGame { details });
273       }
274       Some((player, mpi)) => {
275         writeln!(out, "already in game, as player #{} {:?}",
276                  player.0.get_idx_version().0, &mpi.nick)?;
277         let MgmtPlayerInfo { nick, account:_ } = mpi;
278         if let Some(new_nick) = &ma.nick {
279           if &nick != new_nick {
280             writeln!(out, "changing nick to {:?}", &new_nick)?;
281             let details = MgmtPlayerDetails { nick: ma.nick.clone() };
282             insns.push(MGI::UpdatePlayer { player, details });
283           }
284         }
285         if args.reset_access {
286           writeln!(out, "resetting access token (invalidating other URLs)")?;
287           insns.push(MGI::ResetPlayerAccess(player));
288         } else {
289           writeln!(out, "redelivering existing access token")?;
290           insns.push(MGI::RedeliverPlayerAccess(player));
291         }
292       }
293     };
294
295     fn deliver(out: &mut CookedStdout, token: &AccessTokenReport) {
296       for l in &token.lines {
297         if l.contains(char::is_control) {
298           writeln!(out, "Server token info contains control chars! {:?}", &l)
299         } else {
300           writeln!(out, " {}", &l)
301         }.unwrap()
302       }
303     }
304
305     for resp in chan.alter_game(insns, None)? {
306       match resp {
307         MGR::JoinGame { nick, player, token } => {
308           writeln!(out, "joined game as player #{} {:?}",
309                    player.0.get_idx_version().0,
310                    &nick)?;
311           deliver(&mut out, &token);
312         }
313         MGR::PlayerAccessToken(token) => {
314           deliver(&mut out, &token);
315         }
316         MGR::Fine => {}
317         _ => throw!(anyhow!("unexpected response to instruction(s)")),
318       }
319     }
320
321     Ok(())
322   }
323
324   inventory_subcmd!{
325     "join-game",
326     "Join a game or reset access token (creating or updating account)",
327   }
328 }
329
330 //---------- leave-game ----------
331
332 mod leave_game {
333   use super::*;
334
335   type Args = NoArgs;
336
337   fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
338     let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
339     let mut chan = ma.access_game()?;
340
341     let player = match chan.has_player(&ma.account)? {
342       None => {
343         writeln!(out, "this account is not a player in that game")?;
344         exit(EXIT_NOTFOUND);
345       }
346       Some((player, _)) => player,
347     };
348
349     chan.alter_game(vec![MGI::LeaveGame(player)], None)?;
350
351     Ok(())
352   }
353
354   inventory_subcmd!{
355     "leave-game",
356     "Leave a game",
357   }
358 }
359
360 //---------- clear game ----------
361
362 #[throws(AE)]
363 fn clear_game(ma: &MainOpts, chan: &mut MgmtChannelForGame) {
364   chan.alter_game(vec![MGI::ClearGame{ }], None)
365     .context("clear table")?;
366   chan.cmd(&MC::ClearBundles { game: ma.instance() })
367     .context("clear bundles")?;
368 }
369
370 mod clear_game {
371   use super::*;
372
373   type Args = NoArgs;
374
375   #[throws(AE)]
376   fn call(SCCA{ ma, args,.. }:SCCA) {
377     let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
378     let mut chan = ma.access_game()?;
379     clear_game(&ma, &mut chan)?;
380   }
381
382   inventory_subcmd!{
383     "clear-game",
384     "clear the table and clear out all bundles",
385   }
386 }
387
388 //---------- delete-game ----------
389
390 mod delete_game {
391   use super::*;
392
393   type Args = NoArgs;
394
395   fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> {
396     let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
397     let mut chan = ma.access_game()?;
398     let game = chan.game.clone();
399     chan.cmd(&MC::DestroyGame { game })?;
400     Ok(())
401   }
402
403   inventory_subcmd!{
404     "delete-game",
405     "Delete a game (throwing all the players out of it)",
406   }
407 }