Skip to main content

otter/
fastsplit.rs

1//! Pieces that can split and merge in play
2//!
3//! *Not* done to enable sharing of the piece image between instances
4//! of "the same" piece.  Rather, to avoid having to rewrite the aux
5//! file when a piece is split or merged.
6//!
7//! The approach is that splittable/mergeable pieces trait objects
8//! are all wrapped up in fastsplit::Piece, and GPiece has an ID
9//! that can make more of them so they can be recovered on game load.
10
11use crate::prelude::*;
12
13visible_slotmap_key!{ FastSplitId(b'F') }
14
15impl FastSplitId {
16  pub fn new_placeholder() -> Option<FastSplitId> { Some(default()) }
17}
18
19#[derive(Default,Debug,Serialize,Deserialize)]
20pub struct IFastSplits {
21  table: DenseSlotMap<FastSplitId, Record>
22}
23
24use crate::*; // wat, https://github.com/rust-lang/rust/pull/52234
25
26#[derive(Serialize,Deserialize,Debug)]
27struct Piece {
28  // When we deserialize, we can effectiely clone the contents of the Arc.
29  // But it contains an OccultIlkOwningId which is not Clone (since
30  // it needs to manage the refcount in the occult ilks table).
31  //
32  // So instead, we serialize this to DummyPiece.  That means that
33  // when we serialize, we only visit each Arc once, via Record.  So
34  // when we deserialize, we preserve the original number of
35  // OccultIlkOwningIds.
36  //
37  // When loaded, an occultable Piece has in fact *two* OccultIlkOwningIds,
38  // one in the IPiece here inside this Arc (accessible from Piece
39  // or Record) and one in the outer IPiece which is in the IPieces table.
40  // Trying to optimise one of these away would be quite confusing.
41  #[serde(skip)]
42  ipc: Option<Arc<IPiece>>,
43}
44
45#[derive(Debug,Serialize,Deserialize)]
46pub struct Record {
47  ipc: Arc<IPiece>,
48}
49
50impl Piece {
51  fn inner(&self) -> &dyn PieceTrait {
52    self.ipc
53      .as_ref()
54      .expect("attempted to invoke unresolved fastsplit::Piece")
55      .p.direct_trait_access()
56  }
57}
58
59impl_via_ambassador!{
60  #[dyn_upcast]
61  impl OutlineTrait for Piece { inner() } 
62
63  #[dyn_upcast]
64  impl PieceBaseTrait for Piece { inner() }
65
66  #[typetag::serde(name="FastSplit")]
67  impl PieceTrait for Piece { inner() }
68}
69
70impl Record {
71  fn dummy() -> Record {
72    let p = Piece { ipc: None };
73    let p = IPieceTraitObj::new(Box::new(p) as _);
74    let ipc = IPiece { p, occilk: None, special: default() };
75    Record { ipc: Arc::new(ipc) }
76  }
77}
78
79impl InstanceGuard<'_> {
80  /// The new piece will be below the old one
81  #[throws(ApiPieceOpError)]
82  pub fn fastsplit_split<I>(
83    &mut self, player: PlayerId,
84    tpiece: PieceId, _: ShowUnocculted, tpc_new_z: ShouldSetZLevel,
85    implementation: I
86  ) -> UpdateFromOpComplex
87  where I: FnOnce(&IOccults, &GOccults, &GPlayer,
88                  &mut GPiece, &IPiece,
89                  &mut GPiece)
90                  -> Result<UpdateFromOpComplex, ApiPieceOpError>
91  {
92    // All this complicated machinery exists precisely to let us make
93    // pieces without having to save aux.
94    //
95    // The "save later" part of this ought to be unnecessarily, because
96    // we'll be running in piece API context which already does that.
97    // But it is clearer to call the appropriate function.
98    let modperm = self.modify_pieces_not_necessarily_saving_aux();
99
100    let ig = &mut **self;
101    let gpl = ig.gs.players.byid(player)?;
102    let tgpc = ig.gs.pieces.byid_mut(tpiece)?;
103    let tipc = ig.ipieces.get(tpiece).ok_or(Ia::PieceGone)?;
104
105    let fs_record = (||{
106      let fsid = tgpc.fastsplit?;
107      ig.ifastsplits.table.get(fsid)
108    })().ok_or_else(|| internal_logic_error("fastsplit on non-fastsplit"))?;
109
110    // The new piece (the change, which stays in place) inherits the
111    // old piece's Z (but we need to give it a new generation so that
112    // we don't risk having two pieces with the precise same Z).  The
113    // old piece becomes the amount taken and gets the specified Z.
114    let npc_z = ZLevel { z: tgpc.zlevel.z.clone(), zg: ig.gs.gen };
115    if ! (tpc_new_z.inspect() > &npc_z) {
116      throw!(Ia::BadPieceStateForOperation);
117    }
118
119    let mut ngpc = GPiece {
120      pos:           tgpc.pos,
121      face:          tgpc.face,
122      held:          None,
123      zlevel:        npc_z,
124      pinned:        tgpc.pinned,
125      occult:        default(),
126      angle:         tgpc.angle,
127      gen:           ig.gs.gen,
128      lastclient:    default(),
129      last_released: default(),
130      gen_before_lastclient: Generation(0),
131      xdata:         default(),
132      moveable:      tgpc.moveable,
133      rotateable:    tgpc.rotateable,
134      fastsplit:     tgpc.fastsplit,
135    };
136
137    let (t_pu, t_unprepared) = implementation(
138      &ig.ioccults, &ig.gs.occults, gpl,
139      tgpc, tipc,
140      &mut ngpc
141    )?;
142
143    let ipc_from_record = fs_record.ipc.clone();
144
145    // Committing.
146
147    // This is outside the infallible closure because borrowck
148    // can't see that we drop tgpc before doing stuff with ig.
149    tpc_new_z.implement(tgpc);
150    (||{
151      let nipc = IFastSplits::make_ipc(&mut ig.ioccults.ilks, ipc_from_record);
152      let npiece = ig.gs.pieces.as_mut(modperm).insert(ngpc);
153      ig.ipieces.as_mut(modperm).insert(npiece, nipc);
154
155      // "Recalculate" the occultation of the piece we're leaving behind.
156      let ((), r_unprepared) = ToRecalculate::with(|mut to_permute| {
157        let () = recalculate_occultation_general(
158          &mut ig.gs.gen.unique_gen(),
159          &mut ig.gs.players, &mut ig.gs.pieces,
160          &mut ig.gs.occults, &ig.ipieces, &ig.ioccults,
161          &mut to_permute, npiece,
162          // We are treat this as a new piece, but it's not, semantically.
163          // We don't log anything, since that new piece is not really
164          // supposed to be interesting and the piece implwementation
165          // is supposed to have logged whatever is needed.
166          || (),
167          (),
168          |_old,_new,_desc| (),
169          |_puopp, ()| (),
170        ).unwrap_or_else(|e| {
171          // not good
172          error!("failed to recalculate after piece split! {:?}", &e);
173        });
174        ((), to_permute.implement(ig))
175      });
176
177      let n_unprepared = vec![(
178        npiece,
179        PUOs::Simple(PUO::Insert(())),
180      )].into_unprepared(None);
181
182      let unprepared = chain!(
183        n_unprepared,
184        r_unprepared,
185        t_unprepared,
186      ).collect();
187
188      (t_pu, unprepared)
189    })()// <- no ?, infallible (to avoid having not completed implementation
190  }
191
192  #[throws(IE)]
193  pub fn fastsplit_delete(&mut self,
194                          piece: PieceId,
195                          #[allow(clippy::ptr_arg)]
196                          _proof_that_caller_handles_logging: &Vec<LogEntry>)
197                          -> (PieceUpdateOp<(),()>, UnpreparedUpdates)
198  {
199    let missing_e = || internal_error_bydebug(&piece);
200
201    // See reasoning in fastsplit_split
202    let modperm = self.modify_pieces_not_necessarily_saving_aux();
203
204    let gpc = self.gs.pieces.get(piece).ok_or_else(missing_e)?;
205    let ipc = self.ipieces.get(piece).ok_or_else(missing_e)?;
206
207    // If we are merging or deleting here, presumably someone has
208    // already checked that this is appropriate wrt occultations,
209    // since we don't just do that willy-nilly and occultation will
210    // have been checked when deciding *what* to merge with.
211    let _p: &Piece = ipc.p.direct_trait_access().downcast_piece()?;
212
213    let _fsid: &FastSplitId = gpc.fastsplit.as_ref()
214      .ok_or_else(|| internal_error_bydebug(gpc))?;
215    // We allow merging things with different FastSplitIds.  The
216    // FastSplitId identifies not the "kind of piece", but simply
217    // ancestry.  For example, all banknotes split from the same
218    // original note will have the same id, but otherwise, different
219    // banknotes even of the same currency have different ids.
220
221    match ToRecalculate::with(|mut to_permute| {
222      let r = self.delete_piece(modperm,&mut to_permute, piece,|_,_,_,_|());
223      (r, to_permute.implement(self))
224    }) {
225      (Err(e), uu_p) => {
226        PrepareUpdatesBuffer::only_unprepared(self, uu_p);
227        throw!(e);
228      },
229      (Ok(((), puo, uu_d)), uu_p) => {
230        (puo, chain!(uu_d, uu_p).collect())
231      },
232    }
233  }
234}
235
236impl IFastSplits {
237  /// During piece addition: make this into a new fastsplit piece family
238  ///
239  /// Do not just drop the result, because it contains an OwningOccultIlkId
240  pub fn new_original(&mut self, ilks: &mut OccultIlks, ipc: IPiece)
241                      -> (IPiece, FastSplitId) {
242    let ipc = Arc::new(ipc);
243    let record = Record { ipc: ipc.clone() };
244    let fsid = self.table.insert(record);
245    (Self::make_ipc(ilks, ipc), fsid)
246  }
247
248  /// During game load: recover a proper IPiece for a fastsplit piece
249  ///
250  /// Just dropping the result on error is ok, despite it containing
251  /// an OwningOccultIlkId, because in that case the whole game is toast.
252  #[throws(as Option)]
253  pub fn recover_ipc(&self, ilks: &mut OccultIlks, fsid: FastSplitId)
254                     -> IPiece {
255    let record = self.table.get(fsid)?;
256    Self::make_ipc(ilks, record.ipc.clone())
257  }
258
259  /// Do not just drop the result, because it contains an OwningOccultIlkId
260  fn make_ipc(ilks: &mut OccultIlks, ipc: Arc<IPiece>) -> IPiece {
261    let occilk = ipc.occilk.as_ref().map(|i| ilks.clone_iilk(i));
262    let special = ipc.special.clone();
263    let piece = Box::new(Piece { ipc: Some(ipc) });
264    IPiece {
265      p: IPieceTraitObj::new(piece as _),
266      occilk, special,
267    }
268  }
269
270  /// During save: free up any now-unused family records
271  pub fn cleanup(&mut self, ilks: &mut OccultIlks) {
272    self.table.retain(|_fsid, record| {
273      if Arc::strong_count(&record.ipc) != 1 { return true; }
274      let removed = mem::replace(record, Record::dummy());
275      if_let!{
276        Ok(ipc) = Arc::try_unwrap(removed.ipc);
277        Err(busy) => {
278          // someone upgraded a weak reference, since we have the only
279          // strong one?  but we don't use weak references.  odd.
280          *record = Record { ipc: busy };
281          return true;
282        }
283      };
284      if let Some(i) = ipc.occilk { ilks.dispose_iilk(i); }
285      false
286    })
287  }
288}
289
290#[ext(pub)]
291impl<'r> &'r dyn PieceTrait {
292  #[throws(PieceTraitDowncastFailed<'r>)]
293  fn downcast_piece_fastsplit<P: PieceTrait>(self) -> &'r P {
294    self.downcast_piece::<Piece>()?
295      .ipc.as_ref().ok_or_else(
296        || PieceTraitDowncastFailed { p: self, why: "unresolved fastsplit"})?
297      .p.direct_trait_access() // we're just digging down, this is fine
298      .downcast_piece::<P>()?
299  }
300}