chiark / gitweb /
hidden: Note everything as occulted, even the unoccultable
[otter.git] / src / hidden.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 crate::prelude::*;
6
7 #[path="vpid.rs"] mod vpid;
8 pub use vpid::{PerPlayerIdMap, NotchNumber, Notch, Notches, consistency_check};
9
10 use slotmap::secondary;
11
12 type ONI = OldNewIndex;
13
14 visible_slotmap_key!{ OccId(b'H') }
15
16 // ========== data structures ==========
17
18 #[derive(Copy,Clone,Debug)]
19 /// Proof token.
20 ///
21 /// Proof obligation when constructing.
22 pub struct ShowUnocculted(());
23
24 #[derive(Copy,Clone,Debug)]
25 pub struct OcculterRotationChecked(());
26
27 #[derive(Debug,Serialize,Deserialize)]
28 #[serde(transparent)]
29 pub struct IPieceTraitObj(Box<dyn PieceTrait>);
30
31 #[derive(Clone,Debug,Default,Serialize,Deserialize)]
32 pub struct GOccults {
33   occults: DenseSlotMap<OccId, Occultation>,
34 }
35
36 #[derive(Clone,Debug,Default,Serialize,Deserialize)]
37 // kept in synch with Occultation::pieces
38 pub struct PieceOccult {
39   active: Option<OccId>, // kept in synch with Occultation::occulter
40   passive: Option<Passive>, // kept in synch with Occultation::notches
41 }
42
43 #[derive(Clone,Copy,Debug,Serialize,Deserialize,Eq,PartialEq)]
44 struct Passive {
45   occid: OccId,
46   // If None, this piece does not participate in mixing (IOI::Distinct)
47   permute_notch: Option<Notch>,
48 }
49
50 #[derive(Clone,Debug,Serialize,Deserialize)]
51 pub struct Occultation {
52   region: Region, // automatically affect pieces here
53   occulter: PieceId, // kept in synch with PieceOccult::active
54   notches: Notches, // kept in synch with PieceOccult::passive
55   #[serde(default)]
56   unnotched: HashSet<PieceId>, // kept in synch with PieceOccult::passive
57   ppiece_use_size: Pos, // taken from first piece
58   #[serde(flatten)] views: OccultationViews,
59 }
60
61 #[derive(Clone,Debug,Serialize,Deserialize)]
62 pub struct OccultationViews {
63   pub views: Vec<OccultView>,
64   #[serde(default)] pub defview: OccultationKind,
65 }
66
67 #[derive(Clone,Debug,Serialize,Deserialize)]
68 pub struct OccultView {
69   #[serde(default)] pub occult: OccultationKind,
70   pub players: Vec<PlayerId>,
71 }
72
73 #[derive(Clone,Copy,Debug,Serialize,Deserialize)]
74 #[derive(Eq,PartialEq,Hash)]
75 pub enum OccultationKindGeneral<D> {
76   Visible,
77   Scrambled,
78   Displaced(D),
79   Invisible,
80 }
81 pub type OccultationKind = OccultationKindGeneral<(OccDisplacement,ZCoord)>;
82
83 #[derive(Clone,Debug)]
84 #[derive(Eq,PartialEq,Hash)]
85 pub enum OccultationKindAlwaysOk {
86   Visible,
87   // Scrambled is only allowed as the only view; enforced by our
88   // OccultationViewDef trait impls
89   Displaced((OccDisplacement,ZCoord)),
90   Invisible,
91 }
92 impl From<OccultationKindAlwaysOk> for OccultationKind {
93   fn from(i: OccultationKindAlwaysOk) -> OccultationKind {
94     match i {
95       OccKA::Visible      => OccKG::Visible,
96       OccKA::Displaced(d) => OccKG::Displaced(d),
97       OccKA::Invisible    => OccKG::Invisible,
98     }
99   }
100 }
101
102 #[derive(Clone,Debug,Serialize,Deserialize)]
103 #[derive(Eq,PartialEq,Hash)]
104 pub enum OccDisplacement {
105   Stack {
106     pos: Pos,
107   },
108   Rect {
109     rect: Rect,
110   },
111 }
112
113 impl PieceOccult {
114   pub fn is_active(&self) -> bool { self.active.is_some() }
115
116   #[throws(IE)]
117   fn active_occ<'r>(&'r self, goccults: &'r GOccults)
118                     -> Option<&'r Occultation> {
119     if let Some(occid) = self.active {
120       let occ = goccults.occults.get(occid).ok_or_else(
121         || internal_error_bydebug(&self))?;
122       Some(occ)
123     } else {
124       None
125     }
126   }
127
128   #[throws(IE)]
129   pub fn active_views<'r>(&'r self, goccults: &'r GOccults)
130                           -> Option<&'r OccultationViews> {
131     self.active_occ(goccults)?.map(
132       |occ| &occ.views
133     )
134   }
135
136   #[throws(IE)]
137   pub fn active_region<'r>(&'r self, goccults: &'r GOccults)
138                            -> Option<&'r Region> {
139     self.active_occ(goccults)?.map(
140       |occ| &occ.region
141     )
142   }
143
144   #[throws(IE)]
145   pub fn active_total_ppieces(&self, goccults: &GOccults)
146                               -> Option<usize> {
147     self.active_occ(goccults)?.map(|occ| {
148       let notches_len = usize::try_from(occ.notches.len()).unwrap();
149       notches_len + occ.unnotched.len()
150     })
151   }
152
153   pub fn passive_occid(&self) -> Option<OccId> { Some(self.passive?.occid) }
154   pub fn passive_delete_hook(&self, goccults: &mut GOccults,
155                              piece: PieceId) {
156     if_chain! {
157       if let Some(Passive { occid, permute_notch }) = self.passive;
158       if let Some(occ) = goccults.occults.get_mut(occid);
159       then {
160         if let Some(notch) = permute_notch {
161           occ.notches.remove(piece, notch)
162             .unwrap_or_else(|e| error!("removing occulted piece {:?}", e));
163         } else {
164           occ.unnotched.remove(&piece);
165         }
166       }
167     }
168   }
169 }
170
171 impl Default for OccultationKind {
172   fn default() -> Self { OccK::Visible }
173 }
174
175 impl Ord for OccultationKind {
176   fn cmp(&self, rhs: &Self) -> Ordering {
177     fn level(k: &OccK) -> u8 { use OccKG::*; match k {
178       Visible       => 0,
179       Scrambled     => 1,
180       Displaced{..} => 2,
181       Invisible     => 3,
182     } }
183     level(self).cmp(&level(rhs))
184   }
185 }
186 impl PartialOrd for OccultationKind {
187   fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
188     Some(self.cmp(rhs))
189   }
190 }
191
192 impl OccultationKind {
193   fn at_all_visible(&self) -> bool {
194     match self {
195       OccK::Visible |
196       OccK::Scrambled |
197       OccK::Displaced { .. }
198         => true,
199       OccK::Invisible
200         => false,
201     }
202   }
203 }
204
205 impl<P,Z> OccultationKindGeneral<(P, Z)> {
206   pub fn pri_occulted(self) -> Option<PriOccultedGeneral<P,Z>> {
207     Some(match self {
208       OccKG::Invisible          => return None,
209       OccKG::Visible            => PriOG::Visible(ShowUnocculted(())),
210       OccKG::Scrambled          => PriOG::Occulted,
211       OccKG::Displaced((pos,z)) => PriOG::Displaced(pos, z),
212     })
213   }
214
215 }
216
217 impl<T> OccultationKindGeneral<T> {
218   fn map_displaced<U,F>(&self, f: F) -> OccultationKindGeneral<U>
219     where F: FnOnce(&T) -> U,
220   {
221     use OccKG::*;
222     match self {
223       Visible   => Visible,
224       Scrambled => Scrambled,
225       Invisible => Invisible,
226       Displaced(t) => Displaced(f(t)),
227     }
228   }
229 }      
230
231 impl OccultationViews {
232   pub fn get_kind(&self, player: PlayerId) -> &OccultationKind {
233     let kind = self.views.iter().find_map(|view| {
234       if view.players.contains(&player) { return Some(&view.occult); }
235       None
236     }).unwrap_or(
237       &self.defview
238     );
239     kind
240   }
241 }
242
243 impl Occultation {
244   pub fn get_kind(&self, player: PlayerId) -> &OccultationKind {
245     self.views.get_kind(player)
246   }
247
248   pub fn in_region(&self, pos: Pos) -> bool {
249     self.region.contains(pos)
250   }
251
252   pub fn pieces(&self) -> impl Iterator<Item=PieceId> + '_ {
253     chain!(
254       self.notches.iter(),
255       self.unnotched.iter().cloned(),
256     )
257   }
258 }
259
260 impl GOccults {
261   #[throws(IE)]
262   fn by_id(&self, occid: OccId) -> &Occultation {
263     self.occults.get(occid).ok_or_else(
264       || internal_logic_error("piece missing"))?
265   }
266
267   #[throws(IE)]
268   pub fn get_kind(&self, occid: OccId, player: PlayerId) -> &OccultationKind {
269     let occ = self.by_id(occid)?;
270     let kind = occ.get_kind(player);
271     kind
272   }
273
274   #[throws(IE)]
275   pub fn pos_occulter(&self, goccults: &GOccults, pos: Pos)
276                       -> Option<PieceId> {
277     goccults.occults.iter().find_map(|(_occid, occ)| {
278       if occ.in_region(pos) {
279         Some(occ.occulter)
280       } else {
281         None
282       }
283     })
284   }
285
286   pub fn is_empty(&self) -> bool {
287     let GOccults { occults } = self;
288     occults.is_empty()
289   }
290 }
291
292 // ========== public entrypoints ==========
293
294 /// None => do not render at all
295 pub fn piece_pri(
296   _ioccults: &IOccults,
297   occults: &GOccults,
298   player: PlayerId, gpl: &mut GPlayer,
299   piece: PieceId, gpc: &GPiece, _ipc: &IPiece,
300 ) -> Option<PieceRenderInstructions>
301 {
302   let mut occk_dbg = None;
303   let occulted = if_chain! {
304     if let Some(Passive { occid, permute_notch }) = gpc.occult.passive;
305     if let Some(occ) = occults.occults.get(occid);
306     then { if_chain!{
307       if let Some(notch) = permute_notch;
308       if let Some(zg) = occ.notch_zg(notch);
309       then {
310         let occk = occ.views.get_kind(player)
311           .map_displaced(|(displace, z)| {
312             let notch: NotchNumber = notch.into();
313             let pos = displace.place(occ.ppiece_use_size, notch);
314             let z = z.plus_offset(notch)
315               .unwrap_or_else(|e| { // eek!
316                 error!("z coordinate overflow ({:?}), bodging! {:?} {:?}",
317                        e, piece, &z);
318                 z.clone()
319               });
320             (pos, ZLevel { z, zg })
321           });
322
323         occk_dbg = Some(occk.clone());
324         match occk.pri_occulted() {
325           Some(o) => o,
326           None => {
327             trace_dbg!("piece_pri", player, piece, occk_dbg, gpc);
328             return None;
329           }
330         }
331       }
332       else { // No notch, not mixing (so not displacing)
333         match occ.views.get_kind(player) {
334           OccKG::Invisible          => {
335             trace_dbg!("piece_pri", player, piece, gpc);
336             return None;
337           }
338           OccKG::Visible      => PriOG::Visible(ShowUnocculted(())),
339           OccKG::Scrambled    => PriOG::Occulted,
340           OccKG::Displaced(_) => PriOG::Occulted,
341         }
342       }
343     } }
344     else {
345       PriOG::Visible(ShowUnocculted(()))
346     }
347   };
348
349   let vpid = gpl.idmap.fwd_or_insert(piece);
350   trace_dbg!("piece_pri", player, piece, occk_dbg, vpid, occulted, gpc);
351   Some(PieceRenderInstructions { vpid, occulted })
352 }
353
354 impl OccDisplacement {
355   fn place(&self, ppiece_use_size: Pos, notch: NotchNumber) -> Pos {
356     use OccDisplacement as OD;
357     match self {
358       OD::Stack{pos} => *pos,
359       OD::Rect{rect} => (|| Some({
360         let notch: Coord = notch.try_into().ok()?;
361         let mut spare = ((rect.br() - rect.tl()).ok()?
362                          - ppiece_use_size).ok()?;
363         for s in &mut spare.coords { *s = max(*s,1) }
364         let fi = 0;
365         let gi = 1;
366         let f_stride = max(ppiece_use_size.coords[fi] / 4, 1);
367         let g_stride = max(ppiece_use_size.coords[gi] / 3, 1);
368         let f_count = max(spare.coords[fi] / f_stride, 1);
369         let g_count = max(spare.coords[gi] / g_stride, 1);
370         let mut f_num = notch % f_count;
371         let     g_num = notch / f_count;
372         if g_num % 2 != 0 { f_num = f_count - 1 - f_num }
373         let f_coord = rect.br().coords[fi] - ppiece_use_size.coords[fi] / 2 -
374             f_stride * f_num;
375         let g_coord = rect.tl().coords[gi] + ppiece_use_size.coords[gi] / 2 +
376           if g_num < g_count {
377             g_stride * g_num
378           } else if g_num < spare.coords[gi] {
379             g_num
380           } else {
381             spare.coords[gi] - 1
382           };
383         trace_dbg!("placement", spare,
384                    f_stride, f_count, f_num, f_coord,
385                    g_stride, g_count, g_num, g_coord);
386         let mut pos = PosC::zero();
387         pos.coords[fi] = f_coord;
388         pos.coords[gi] = g_coord;
389         pos
390       }))().unwrap_or_else(||{
391         rect.middle()
392       })
393     }
394   }
395 }
396
397 impl ShowUnocculted {
398   /// Override.  Proof obligation: this context does not require
399   /// honouring occultation.
400   pub const fn new_visible() -> ShowUnocculted {
401     ShowUnocculted(())
402   }
403 }
404
405 impl PieceRenderInstructions {
406   /// Override.  Proof obligation: this context does not require
407   /// honouring occultation.
408   pub fn new_visible(vpid: VisiblePieceId) -> PieceRenderInstructions {
409     PieceRenderInstructions {
410       vpid,
411       occulted: PriOcculted::Visible(ShowUnocculted(())),
412     }
413   }
414 }
415
416 impl IPieceTraitObj {
417   pub fn new(p: Box<dyn PieceTrait>) -> Self { Self(p) }
418
419   pub fn show(&self, _: ShowUnocculted) -> &dyn PieceTrait {
420     &*self.0
421   }
422
423   pub fn into_inner(self) -> Box<dyn PieceTrait> { self.0 }
424
425   pub fn direct_trait_access(&self) -> &dyn PieceTrait {
426     &*self.0
427   }
428 }
429
430 impl IPiece {
431   #[throws(IE)]
432   pub fn show_or_instead<'p>(&'p self, ioccults: &'p IOccults,
433                          y: Option<ShowUnocculted>)
434           -> Either<ShowUnocculted, /*occulted*/ &'p dyn InertPieceTrait> {
435     match y {
436       Some(y) => Left(y),
437       None => Right({
438         if_let!{ Some(occilk) = self.occilk.as_ref();
439                  else return Ok(Left(ShowUnocculted::new_visible())); }
440         let occ_data = ioccults.ilks.from_iilk(occilk)
441           .ok_or_else(|| internal_logic_error(format!(
442             "occulted ilk vanished {:?} {:?}", self, occilk)))?;
443         occ_data.p_occ.as_ref()
444       }),
445     }
446   }
447 }
448
449 impl GPiece {
450   pub fn fully_visible_to_everyone(&self) -> Option<ShowUnocculted> {
451     match self.occult.passive {
452       Some(_) => None,
453       None => Some(ShowUnocculted(())),
454     }
455   }
456
457   pub fn occulter_check_unrotated(&self, _:ShowUnocculted)
458       -> Result<OcculterRotationChecked, Inapplicable> {
459     if self.angle.is_rotated() { Err(Ia::OcculterAlreadyRotated) }
460     else { Ok(OcculterRotationChecked(())) }
461   }
462
463
464   pub fn fully_visible_to(&self, goccults: &GOccults, player: PlayerId)
465                           -> Option<ShowUnocculted>
466   {
467     const HIDE: Option<ShowUnocculted> = None;
468     const SHOW: Option<ShowUnocculted> = Some(ShowUnocculted(()));
469     if_let!{ Some(passive) = &self.occult.passive; else return SHOW };
470     want_let!{ Some(occ) = goccults.occults.get(passive.occid);
471                else ?passive.occid; return HIDE };
472     return match occ.views.get_kind(player) {
473       OccK::Visible => SHOW,
474       OccK::Scrambled |
475       OccK::Displaced(_) |
476       OccK::Invisible => HIDE,
477     }
478   }
479
480   pub fn involved_in_occultation(&self) -> bool {
481     self.occult.passive.is_some() ||
482     self.occult.active.is_some()
483   }
484
485   #[throws(Ia)]
486   pub fn forbid_involved_in_occultation(&self) {
487     if self.involved_in_occultation() { throw!(Ia::Occultation) }
488   }
489 }
490
491 pub fn vpiece_decode(
492   gs: &GameState,
493   player: PlayerId,
494   gpl: &GPlayer,
495   vis: VisiblePieceId
496 ) -> Option<PieceId> {
497   let piece: Option<PieceId> = gpl.idmap.rev(vis);
498   let piece: Option<PieceId> = if_chain! {
499     if let Some(p) = piece;
500     if let Some(gpc) = gs.pieces.get(p);
501     if let Some(Passive { occid, permute_notch:_ }) = gpc.occult.passive;
502     if let Some(occ) = gs.occults.occults.get(occid);
503     let kind = occ.views.get_kind(player);
504     if ! kind.at_all_visible();
505     then { None }
506     else { piece }
507   };
508   trace!("{} {:?} <= {}", player, piece, vis);
509   piece
510 }
511
512 #[throws(InternalError)]
513 pub fn recalculate_occultation_general<
514   RD: Debug,                                          // return data
515   LD: Debug,                                          // log data
516   VF: FnOnce() -> RD,                                 // ret_vanilla
517   LF: FnOnce(Option<Html>, Option<Html>, &Html) -> LD, // log_callback
518   RF: FnOnce(PieceUpdateOps_PerPlayer, LD) -> RD,     // ret_callback
519 >(
520   gen: &mut UniqueGenGen,
521   //
522   gplayers: &GPlayers, gpieces: &mut GPieces,
523   goccults: &mut GOccults, ipieces: &IPieces, ioccults: &IOccults,
524   //
525   to_recalculate: &mut ToRecalculate, piece: PieceId,
526   // if no change, we return ret_vanilla()
527   ret_vanilla: VF,
528   // otherwise we use log_invisible or log_callback(who_by,old,new,desc)
529   log_invisible: LD,
530   log_callback: LF,
531   // and then call ret_callback(<calculated>, <logmsgs>)
532   ret_callback: RF,
533 )
534   -> RD
535 {
536   #[derive(Debug,Copy,Clone)]
537   struct OldNewOcculteds<O> {
538     old: Option<(O, Option<Notch>)>,
539     new: Option<O>,
540   }
541   impl<O> OldNewOcculteds<O> {
542     fn main(&self) -> OldNew<Option<O>> where O:Copy {
543       [ self.old.map(|(o,_n)| o), self.new ].into()
544     }
545     fn map<P, F:FnMut(O) -> P>(self, mut f: F) -> OldNewOcculteds<P> {
546       OldNewOcculteds {
547         old: self.old.map(|(o,n)| (f(o), n)),
548         new: self.new.map(f),
549       }
550     }
551     fn as_refs(&self) -> OldNewOcculteds<&O> {
552       OldNewOcculteds {
553         old: self.old.as_ref().map(|(o,n)| (o, *n)),
554         new: self.new.as_ref()
555       }
556     }
557   }
558
559   let nopiece = || internal_logic_error("piece vanished");
560   let ipc = ipieces.get(piece).ok_or_else(nopiece)?;
561
562   // fallible part
563   let (puos, log, occulteds): (_, _, OldNewOcculteds<OccId>) = {
564     let gpc = gpieces.get(piece).ok_or_else(nopiece)?;
565
566     #[derive(Debug,Copy,Clone)]
567     struct Occulted<'o> { occid: OccId, occ: &'o Occultation }
568
569     let occulteds = OldNewOcculteds {
570       old:
571         gpc.occult.passive.map(|Passive { occid, permute_notch }| Ok::<_,IE>((
572           Occulted {
573             occid,
574             occ: goccults.occults.get(occid).ok_or_else(
575               || internal_logic_error("uccultation vanished"))?,
576           },
577           permute_notch,
578         ))).transpose()?,
579
580       new:
581         goccults.occults.iter().find_map(|(occid, occ)| {
582           if gpc.pinned {
583             // Prevent pinned pieces being occulted.  What scrambling
584             // them etc. would mean is not entirely clear.
585             return None
586           } else if gpc.occult.active.is_some() {
587             // prevent occulting pieces being occulted
588             // (also prevents reflexive occultation)
589             return None
590           } else if occ.in_region(gpc.pos) {
591             Some(Occulted { occid, occ })
592           } else {
593             None
594           }
595         }),
596     };
597     trace_dbg!("recalculating", piece, occulteds);
598
599     let occids = occulteds.main().map(|h| h.as_ref().map(|occ| occ.occid));
600     if occids.old() == occids.new() { return ret_vanilla(); }
601
602   /*
603     #[throws(IE)]
604     fn get_kind(gs: &GameState, occid: Option<OccultationId>, player: PlayerId)
605                 -> Option<(OccultationId, OccultationKind)> {
606     };*/
607
608     let mut situations: HashMap<
609         OldNew<&OccultationKind>,
610         Vec<PlayerId>,
611       > = default();
612     for (player, _gpl) in gplayers {
613       situations
614         .entry(
615           occulteds.main().map(|occulted| {
616             if let Some(occulted) = occulted {
617               occulted.occ.get_kind(player)
618             } else {
619               &OccK::Visible
620             }
621           }))
622         .or_default()
623         .push(player);
624     }
625
626     trace_dbg!("situations", &situations);
627
628     let mut puos = SecondarySlotMap::new();
629     let mut most_obscure = None;
630
631     for (kinds, players) in &situations {
632       // For each player, the message obscuration is the least obscure.
633       // Then the overall obscuration is that from most afflicted player.
634       most_obscure = cmp::max(
635         most_obscure,
636         kinds.iter().map(Deref::deref).min()
637       );
638
639       let puo = match (
640         kinds.old().at_all_visible(),
641         kinds.new().at_all_visible(),
642       ) {
643         (false, false) => None,
644         (false, true ) => Some(PUO::InsertQuiet(())),
645         (true,  false) => Some(PUO::Delete()),
646         (true,  true ) => Some(PUO::Modify(())),
647       };
648       trace_dbg!("situation", &kinds, &players, &most_obscure, &puo);
649
650       if let Some(puo) = puo {
651         for player in players {
652           puos.insert(*player, puo);
653         }
654       }
655     }
656     // this calculation does not seem to work?
657     // to repro: bob moves a card out of bob's hand
658     // alice sees "a card with a red-striped back"
659     trace_dbg!("most_obscure", most_obscure);
660
661     let describe_occulter = |oni| Ok::<_,IE>(if_chain! {
662       if let Some(h) = occulteds.as_refs().main()[oni];
663       let opiece = h.occ.occulter;
664       let bad = || internal_error_bydebug(&("missing", opiece, h.occid));
665       let oipc = ipieces.get(opiece).ok_or_else(bad)?;
666       let ogpc = gpieces.get(opiece).ok_or_else(bad)?;
667       let ounocc = ogpc.fully_visible_to_everyone()
668         .ok_or_else(||internal_error_bydebug(&(occulteds, &ogpc)))?;
669       then {
670         Some(oipc.show(ounocc).describe_html(ogpc, goccults)?)
671       } else {
672         None
673       }
674     });
675
676     let most_obscure = most_obscure.unwrap_or(&OccK::Visible); // no players!
677
678     let call_log_callback =
679       |show| Ok::<_,IE>(
680         log_callback(describe_occulter(ONI::Old)?,
681                      describe_occulter(ONI::New)?,
682                      show)
683       );
684
685     let log = match most_obscure.map_displaced(|_|((),())).pri_occulted() {
686       Some(prioc@ PriOG::Visible(_)) |
687       Some(prioc@ PriOG::Occulted) |
688       Some(prioc@ PriOG::Displaced(..)) => {
689         let show = prioc.describe(ioccults, goccults, gpc, ipc);
690         call_log_callback(&show)?
691       },
692       None => {
693         log_invisible
694       },
695     };
696
697     (puos, log, occulteds.map(|h| h.occid))
698   };
699   
700
701   trace_dbg!("committing", &puos, &log, &occulteds);
702
703   (||{
704     let occultation:
705        &mut dyn for<'g> FnMut(&'g mut GOccults, OccId) -> &mut Occultation
706       = &mut |goccults, occid|
707       // rust-lang/rust/issues/58525
708     {
709       to_recalculate.mark_dirty(occid);
710       goccults.occults.get_mut(occid).unwrap()
711     };
712     if let Some((occid, old_notch)) = occulteds.old {
713       let occ = occultation(goccults, occid);
714       if let Some(old_notch) = old_notch {
715         occ
716           .notches
717           .remove(piece, old_notch)
718           .unwrap();
719       } else {
720         occ
721           .unnotched
722           .remove(&piece);
723       }
724     };
725     let passive = if_chain!{
726       if let Some(occid) = occulteds.new;
727       let zg = gen.next();
728       let occ = occultation(goccults, occid);
729       let ilk = ipc.occilk.as_ref();
730       then {
731         let permute_notch = match ilk {
732           Some(IOI::Distinct(_)) | None => {
733             occ.unnotched.insert(piece);
734             None
735           },
736           Some(IOI::Mix(ilk)) => {
737             if_chain!{
738               if occ.notches.is_empty();
739               if let Some(ilk) = wants!( ioccults.ilks.get(ilk) );
740               if let Some(bbox) = want!( Ok = ilk.p_occ.bbox_approx() );
741               if let Some(size) = want!( Ok = bbox.br() - bbox.tl(), ?(bbox) );
742               then { occ.ppiece_use_size = size; }
743             };
744
745             let notch = occ.notches.insert(zg, piece);
746             Some(notch)
747           }
748         };
749         Some(Passive { occid, permute_notch })
750       }
751       else {
752         None
753       }
754     };
755     gpieces.byid_mut(piece).unwrap().occult.passive = passive;
756   })(); // <- no ?, infallible commitment
757
758   ret_callback(puos, log)
759 }
760
761 #[throws(InternalError)]
762 pub fn recalculate_occultation_piece(
763   gs: &mut GameState,
764   who_by: Html,
765   ipieces: &IPieces,
766   ioccults: &IOccults,
767   to_recalculate: &mut ToRecalculate,
768   piece: PieceId,
769   (vanilla_wrc, vanilla_op, vanilla_log): PUFOS,
770 )
771   -> PieceUpdate
772 {
773     recalculate_occultation_general(
774       &mut gs.gen.unique_gen(),
775       &gs.players, &mut gs.pieces, &mut gs.occults, ipieces, ioccults,
776       to_recalculate, piece,
777       || (vanilla_wrc, vanilla_op, vanilla_log).into(),
778       vec![],
779       |old, new, show| vec![ LogEntry { html: hformat!(
780         "{} {}",
781         &who_by,
782         match (old, new) {
783           (None, None) => hformat!("modified {} somehow", show),
784           (Some(old), None) => hformat!("produced {} from {}", show, old),
785           (None, Some(new)) => hformat!("placed {} into {}", show, new),
786           (Some(old), Some(new)) => hformat!("moved {} from {} to {}",
787                                             show, old, new),
788         }
789       )}],
790       |puos, log| PieceUpdate {
791         wrc: WRC::Unpredictable,
792         ops: PieceUpdateOps::PerPlayer(puos),
793         log
794       }
795     )?
796 }
797
798 #[throws(IE)]
799 fn recalculate_occultation_ofmany(
800   gen: &mut UniqueGenGen,
801   gplayers: &GPlayers,
802   gpieces: &mut GPieces,
803   goccults: &mut GOccults,
804   ipieces: &IPieces,
805   ioccults: &IOccults,
806   to_recalculate: &mut ToRecalculate,
807   ppiece: PieceId,
808   updates: &mut Vec<(PieceId, PieceUpdateOps)>,
809 ){
810   recalculate_occultation_general(
811     gen,
812     gplayers, gpieces, goccults, ipieces, ioccults,
813     to_recalculate, ppiece,
814     ||(),
815     (), |_,_,_|(),
816     |puo_pp, ()|{
817       updates.push((ppiece, PUOs::PerPlayer(puo_pp)));
818     },
819   )?;
820 }
821
822
823 mod recompute {
824   use super::*;
825
826   #[derive(Debug)]
827   pub struct ToRecalculate {
828     outdated: HashSet<OccId>,
829   }
830   #[must_use]
831   pub struct Implemented(UnpreparedUpdates);
832   impl Debug for Implemented {
833     #[throws(fmt::Error)]
834     fn fmt(&self, f: &mut Formatter) {
835       write!(f, "Implemented({})", self.0.len())?;
836     }
837   }
838
839   impl ToRecalculate {
840     /// F returning `Implemented` proves that it called `implement`
841     pub fn with<R, F>(f: F) -> (R, UnpreparedUpdates)
842     where F: FnOnce(Self) -> (R, Implemented)
843     {
844       let to_recalculate = ToRecalculate { outdated: default() };
845       let (r, Implemented(uu)) = f(to_recalculate);
846       (r, uu)
847     }
848     pub fn mark_dirty(&mut self, occid: OccId) { self.outdated.insert(occid); }
849
850     pub fn implement_auth<A>(self, igu: &mut Unauthorised<InstanceGuard,A>)
851                              -> Implemented {
852       // Caller must have had some kind of authorisation to do whatever
853       // it was they did.  It doesn't amke sense to demand any more.
854       self.implement(igu.by_mut(Authorisation::promise_any()))
855     }
856     pub fn implement(self, ig: &mut Instance) -> Implemented {
857       self.implement_inner(&mut ig.gs.players,
858                            &mut ig.gs.pieces,
859                            &mut ig.gs.occults,
860                            &ig.ipieces)
861     }
862     fn implement_inner(self,
863                      gplayers: &mut GPlayers,
864                      gpieces: &mut GPieces,
865                      goccults: &mut GOccults,
866                      ipieces: &IPieces) -> Implemented {
867       let mut unprepared = vec![];
868
869       for occid in self.outdated {
870         if let Some(occ) = goccults.occults.get_mut(occid) {
871           vpid::permute(occid, occ, gplayers, gpieces, ipieces);
872           if let Some(ipc) = ipieces.get(occ.occulter) {
873             let uu = ipc
874                 .direct_trait_access()
875                 .occultation_notify_hook(occ.occulter);
876             unprepared.extend(uu);
877           }
878         }
879       }
880
881       consistency_check(gplayers, gpieces, goccults);
882
883       Implemented(unprepared)
884     }
885   }
886 }
887
888 pub use recompute::ToRecalculate;
889
890 #[must_use]
891 pub struct NascentOccultation(Occultation);
892
893 #[derive(Debug,Clone)]
894 pub struct UniformOccultationView(
895   pub OccultationKind,
896 );
897 #[derive(Debug,Clone)]
898 pub struct OwnerOccultationView {
899   pub defview: OccultationKindAlwaysOk,
900   pub owner: PlayerId,
901   pub owner_view: OccultationKindAlwaysOk,
902 }
903
904 pub trait OccultationViewDef {
905   fn views(self) -> Result<OccultationViews, IE>;
906 }
907 impl OccultationViewDef for UniformOccultationView {
908   #[throws(IE)]
909   fn views(self) -> OccultationViews { OccultationViews {
910     defview: self.0,
911     views: vec![]
912   } }
913 }
914 impl OccultationViewDef for OwnerOccultationView {
915   #[throws(IE)]
916   fn views(self) -> OccultationViews { OccultationViews {
917     defview: self.defview.into(),
918     views: vec![OccultView {
919       players: vec![self.owner],
920       occult: self.owner_view.into(),
921     }]
922   } }
923 }
924
925 #[throws(APOE)]
926 pub fn create_occultation(
927   gen: &mut UniqueGenGen,
928   max_z: &mut ZLevel,
929   gplayers: &mut GPlayers,
930   gpieces: &mut GPieces,
931   goccults: &mut GOccults,
932   ipieces: &IPieces,
933   ioccults: &IOccults,
934   to_recalculate: &mut ToRecalculate,
935   _: OcculterRotationChecked,
936   region: Region,
937   occulter: PieceId,
938   views: OccultationViews,
939   // Caller must promise not to fail if we succeed, so that this
940   // update actually happens!
941   _puos_will_return: &PUOs_Simple_Modify,
942 ) -> Vec<(PieceId, PieceUpdateOps)> {
943   // We mustn't actually store this in gpieces until we commit.
944   let ogpc_z_new = piece_make_heavy(gpieces, occulter)?;
945
946   {
947     let ogpc = gpieces.get(occulter).ok_or_else(
948       ||internal_logic_error("create occultation with non-piece"))?;
949     if ogpc.occult.active.is_some() {
950       throw!(internal_logic_error("re-occulting!"))
951     }
952
953     if let Some(displ_z) = {
954       views.views
955         .iter().map(|ov| &ov.occult)
956         .chain(iter::once(&views.defview))
957         .filter_map(|ok| { use OccKG::*; match ok {
958           Visible | Scrambled | Invisible => None,
959           Displaced((_region, ref z)) => Some(z)
960         }})
961         .max()
962     } {
963       // We expect that ogpc.zlevel.z.increment() is shorter than
964       // the displ_z, but in case it isn't, we must look at both.
965       let max_z = &mut max_z.z;
966       (||{
967         max_z.update_max(&ogpc_z_new.clone_mut().increment()?);
968         max_z.update_max(&displ_z.plus_offset(! 0)?);
969         Ok::<_,IE>(())
970       })()?;
971     }
972   }
973
974   for occ in goccults.occults.values() {
975     if occ.region.overlaps(&region) { throw!(Ia::OverlappingOccultation) }
976   }
977
978   let mut recalc = vec![];
979   for (ppiece, pgpc) in gpieces.iter() {
980     if ! region.contains(pgpc.pos) { continue }
981     if pgpc.occult.passive.is_some() { throw!(internal_logic_error(
982       format!("piece {:?} in region, no occulters, but occulted", &pgpc)
983     )) }
984     recalc.push(ppiece);
985   }
986
987   let occultation = Occultation {
988     region,
989     occulter,
990     views,
991     ppiece_use_size: PosC::zero(),
992     notches: default(),
993     unnotched: default(),
994   };
995   debug!("creating occultation {:?}", &occultation);
996   trace_dbg!("recalc", &recalc);
997
998   // Everything from here on must be undone if we get an error
999   // but we hope not to get one...
1000
1001   let occid = goccults.occults.insert(occultation);
1002   let mut updates = vec![];
1003
1004   (|| (
1005     (||{
1006       let ogpc = gpieces.get_mut(occulter).ok_or_else(
1007         ||internal_logic_error("occulter vanished"))?;
1008       ogpc.occult.active = Some(occid);
1009       ogpc.zlevel.z = ogpc_z_new;
1010
1011       for &ppiece in &recalc {
1012         recalculate_occultation_ofmany(gen,
1013                                        gplayers, gpieces, goccults,
1014                                        ipieces, ioccults,
1015                                        to_recalculate,
1016                                        ppiece, &mut updates)?;
1017       }
1018
1019       Ok::<_,IE>(())
1020     })().map_err(|e| {
1021       for &ppiece in &recalc {
1022         let pgpc = gpieces.get_mut(ppiece).expect("had ppiece earlier");
1023         pgpc.occult.passive = None;
1024       }
1025       let ogpc = gpieces.get_mut(occulter).expect("had occulter earlier");
1026       ogpc.occult.active = None;
1027       goccults.occults.remove(occid).expect("inserted this earlier");
1028       e
1029     })
1030   ))()?;
1031
1032   trace_dbg!("created", &updates);
1033   updates
1034 }
1035
1036 #[throws(IE)]
1037 pub fn remove_occultation(
1038   gen: &mut UniqueGenGen,
1039   gplayers: &mut GPlayers,
1040   gpieces: &mut GPieces,
1041   goccults: &mut GOccults,
1042   ipieces: &IPieces,
1043   ioccults: &IOccults,
1044   to_recalculate: &mut ToRecalculate,
1045   occulter: PieceId,
1046 ) -> Vec<(PieceId, PieceUpdateOps)> {
1047   let mut aggerr = AggregatedIE::new();
1048
1049   let occid = if_chain! {
1050     if let Some(ogpc) = gpieces.get(occulter);
1051     // This can be None if the occulter is being deleted
1052
1053     if let Some(occid) = ogpc.occult.active.or_else(||{
1054       aggerr.record(internal_logic_error(
1055         "removing occultation by non-active piece"));
1056       None
1057     });
1058
1059     then { occid }
1060     else { aggerr.ok()?; panic!(); }
1061   };
1062
1063   let mut updates = vec![];
1064   let mut unbreak_pieces: Vec<PieceId> = vec![];
1065
1066   match (||{
1067     let occ = goccults.occults.get_mut(occid).ok_or_else(
1068       || internal_logic_error("removing nonexistent occultation"))?;
1069     debug!("removing occultation {:?}", &occ);
1070
1071     // We have to recalculate with the occultation still active, so
1072     // that the affected pieces can know what the old situation was.
1073     // So we set the region to empty, and do a recalculation of the
1074     // relevant pieces.  Only then can we get rid of the occultation.
1075     occ.region = Region::empty();
1076
1077     let pieces: Vec<_> = occ.pieces().collect();
1078       
1079     for &ppiece in pieces.iter() {
1080       recalculate_occultation_ofmany(gen,
1081                                      gplayers, gpieces, goccults,
1082                                      ipieces, ioccults,
1083                                      to_recalculate,
1084                                      ppiece, &mut updates)
1085         .unwrap_or_else(|e| {
1086           aggerr.record(e);
1087           if let Some(pgpc) = gpieces.get_mut(ppiece) {
1088             pgpc.occult.passive = None;
1089           }
1090         });
1091     }
1092
1093     // now there should be nothing
1094     let occ = goccults.occults.remove(occid).ok_or_else(
1095       || internal_logic_error("occultation vanished in recalc!"))?;
1096     
1097     unbreak_pieces.extend(occ.pieces());
1098
1099     Ok::<_,IE>(())
1100   })() {
1101     e@ Err(_) => {
1102       aggerr.handle(e);
1103       unbreak_pieces.extend(gpieces.keys());
1104     }
1105     Ok(()) => {},
1106   }
1107
1108   if ! unbreak_pieces.is_empty() {
1109     aggerr.record(internal_logic_error(format!(
1110       "occultation remove left pieces: {:?}", &unbreak_pieces)));
1111
1112     for ppiece in unbreak_pieces { if_chain! {
1113       if let Some(pgpc) = gpieces.get_mut(ppiece);
1114       if let Some(passive) = pgpc.occult.passive;
1115       if passive.occid == occid;
1116       then {
1117         pgpc.occult.passive = None;
1118         updates.push((ppiece, PieceUpdateOp::Modify(()).into()));
1119       }
1120     }}
1121   }
1122
1123   if let Some(ogpc) = gpieces.get_mut(occulter) {
1124     ogpc.occult.active = None;
1125   } else {
1126     aggerr.record(internal_logic_error("removing occultation of non-piece"));
1127   }
1128
1129   aggerr.ok()?;
1130
1131   updates
1132 }