From 3f51173df2214caa0fa63b7b8e6b24a553ab7fdd Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Fri, 2 Apr 2021 23:35:38 +0100 Subject: [PATCH] organise: Introduce new functionality It still has some bugs. Signed-off-by: Ian Jackson --- src/error.rs | 3 + src/hand.rs | 16 +++- src/hidden.rs | 12 +++ src/lib.rs | 1 + src/organise.rs | 197 ++++++++++++++++++++++++++++++++++++++++++++++++ src/prelude.rs | 1 + 6 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/organise.rs diff --git a/src/error.rs b/src/error.rs index 1d1da32b..59ce5061 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,8 @@ pub enum InternalError { PartialPlayerData, #[error("Coordinate overflow")] CoordinateOverflow(#[from] CoordinateOverflow), + #[error("Organised placement not possible")] + OrganisedPlacementFailure, #[error("Z Coordinate overflow (game is too crufty?)")] ZCoordinateOverflow(#[from] zcoord::Overflow), #[error("Multiple errors occurred where only one could be reported")] @@ -177,6 +179,7 @@ pub enum PieceOpError { Conflict, PosOffTable, PieceGone, + Occultation, } display_as_debug!{PieceOpError} diff --git a/src/hand.rs b/src/hand.rs index 82777221..7aff7793 100644 --- a/src/hand.rs +++ b/src/hand.rs @@ -206,13 +206,25 @@ impl PieceTrait for Hand { desc: self.behaviour.claim_desc().into(), wrc: WRC::Unpredictable, }} - }) + }); + + organise::add_ui_operations(upd, &self.shape.outline.rect(gpc.pos)?)?; } #[throws(ApiPieceOpError)] - fn ui_operation(&self, a: ApiPieceOpArgs<'_>, + fn ui_operation(&self, mut a: ApiPieceOpArgs<'_>, opname: &str, wrc: WhatResponseToClientOp) -> UpdateFromOpComplex { + if let Some(r) = { + let gpc = a.gs.pieces.byid_mut(a.piece)?; + let rect = self.shape.outline.rect(gpc.pos) + .map_err(|CoordinateOverflow| + internal_error_bydebug(&(&gpc.pos, &self.shape)))?; + organise::ui_operation(&mut a, opname, wrc, &rect)? + } { + return r; + } + let ApiPieceOpArgs { gs,player,piece,ipieces,ioccults,to_recalculate,.. } = a; let gen = &mut gs.gen; let gplayers = &mut gs.players; diff --git a/src/hidden.rs b/src/hidden.rs index cf709cd7..5cd7f4a9 100644 --- a/src/hidden.rs +++ b/src/hidden.rs @@ -250,6 +250,18 @@ impl GameOccults { let kind = occ.get_kind(player); kind } + + #[throws(IE)] + pub fn pos_occulter(&self, goccults: &GameOccults, pos: Pos) + -> Option { + goccults.occults.iter().find_map(|(_occid, occ)| { + if occ.in_region(pos) { + Some(occ.occulter) + } else { + None + } + }) + } } // ========== public entrypoints ========== diff --git a/src/lib.rs b/src/lib.rs index 5ddbdc10..f2109a9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ pub mod keydata; pub mod mgmtchannel; pub mod nwtemplates; pub mod occultilks; +pub mod organise; pub mod pcrender; pub mod pieces; pub mod shapelib; diff --git a/src/organise.rs b/src/organise.rs new file mode 100644 index 00000000..369be675 --- /dev/null +++ b/src/organise.rs @@ -0,0 +1,197 @@ +// Copyright 2020-2021 Ian Jackson and contributors to Otter +// SPDX-License-Identifier: AGPL-3.0-or-later +// There is NO WARRANTY. + +use crate::prelude::*; + +const MARGIN_INSIDE: Coord = 1; +const HANG_INSIDE: Coord = 2; + +#[throws(InternalError)] +pub fn add_ui_operations(upd: &mut Vec, + region: &Rect) { + if (region.br() - region.tl())?.coords.iter().any( + |&c| c < HANG_INSIDE*2 + ) { + // too small! + return; + } + + upd.push(UoDescription { + kind: UoKind::Piece, + def_key: 'o', + opname: "organise".to_string(), + desc: Html::lit("Organise").into(), + wrc: WRC::Predictable, + }); +} + +define_index_type!{ struct InHand = usize; } + +#[derive(Copy,Clone,Debug,Ord,PartialOrd,Eq,PartialEq)] +#[derive(EnumIter)] +enum Attempt { + Nonoverlap, + Inside, + Abut, + Hanging, +} +use Attempt as A; + +impl Attempt { + #[throws(CoordinateOverflow)] + fn tl(self, bbox: &Rect) -> Pos { + let cnr = bbox.tl(); + match self { + A::Nonoverlap | + A::Inside => (cnr - PosC::both(MARGIN_INSIDE))?, + A::Abut => cnr, + A::Hanging => PosC::both(-HANG_INSIDE), + } + } + + #[throws(CoordinateOverflow)] + fn br_real(self, bbox: &Rect) -> Pos { + let cnr = bbox.br(); + match self { + A::Nonoverlap | + A::Inside => (cnr + PosC::both(MARGIN_INSIDE))?, + A::Abut => cnr, + A::Hanging => PosC::both(HANG_INSIDE), + } + } + + #[throws(CoordinateOverflow)] + fn br_tile(self, bbox: &Rect) -> Pos { + let cnr = bbox.br(); + match self { + A::Nonoverlap => (cnr + PosC::both(MARGIN_INSIDE))?, + A::Inside | + A::Abut => (bbox.tl() - bbox.tl().map(|v| v/3))?, + A::Hanging => PosC::both(HANG_INSIDE), + } + } +} + +#[throws(InternalError)] +fn try_layout(region: &Rect, + pieces: &IndexVec, + att: Attempt) + -> Option> { + let mut out = default(); + if pieces.is_empty() { return Some(out) } + + let mut cur = region.tl(); + let mut n_y = region.tl().y(); + // Invariant: + // Everything below n_y is overwriteable + // Everything below and to the right of cur is overwriteable + + for (_piece, bbox) in pieces { + let place = 'placed: loop { + for _ in 0..3 { + let place = (cur - att.tl(&bbox)?)?; + let br_real = (place + att.br_real(&bbox)?)?; + if br_real.x() > region.br().x() { + cur = PosC::new( + region.tl().x(), + n_y, + ); + } else if br_real.y() > region.br().y() { + if ! matches!(att, A::Hanging) { return None } + cur = PosC::new( + region.tl().x(), + region.br().y().checked_sub(HANG_INSIDE) + .ok_or(CoordinateOverflow)?, + ); + n_y = cur.y(); + continue; + } else { + break 'placed place; + } + } + throw!(IE::OrganisedPlacementFailure); + }; + let br_tile = (place + att.br_tile(&bbox)?)?; + cur.coords[0] = br_tile.x(); + n_y = max(n_y, br_tile.y()); + out.push(place); + } + Some(out) +} + + +#[throws(ApiPieceOpError)] +pub fn ui_operation(a: &mut ApiPieceOpArgs<'_>, opname: &str, + wrc: WhatResponseToClientOp, region: &Rect) + -> Option { + let _do_sort = match opname { + "organise" => (), + _ => return None, + }; + let ApiPieceOpArgs { ref mut gs, player,ipieces,ioccults,.. } = *a; + let apiece = a.piece; + let agpc = gs.pieces.byid(apiece)?; + let aipc = ipieces.get(apiece).ok_or(internal_error_bydebug(&apiece))?; + let gpl = gs.players.byid(player)?; + let log = log_did_to_piece(ioccults, &gs.occults, gpl,agpc,aipc, + "oranised")?; + + let pieces = gs.pieces.iter().filter_map(|(piece, gpc)| if_chain!{ + if region.contains(gpc.pos); + if gpc.held.is_none(); + if ! gpc.pinned; + if let PieceMoveable::Yes = gpc.moveable(); + if let Some(ipc) = wants!( ipieces.get(piece), ?piece ); + if let Some(vis) = gpc.fully_visible_to(&gs.occults, player); + if let Some(bbox) = want!( Ok = ipc.show(vis).bbox_approx(), ?piece ); + then { + Some((piece, bbox)) + } + else { + None + } + }).collect::>(); + + let layout = 'laid_out: loop { + for att in Attempt::iter() { + if let Some(layout) = try_layout(region, &pieces, att)? { + break 'laid_out layout; + } + } + throw!(internal_error_bydebug(region)); + }; + + for &pos in &layout { + // Some sanity checks + if pos.clamped(gs.table_size).is_err() { + throw!(APOE::ReportViaUpdate(POE::PosOffTable)) + } + match gs.occults.pos_occulter(&gs.occults, pos)? { + None => {}, + Some(occulter) if occulter == apiece => {}, + Some(_) => throw!(APOE::ReportViaUpdate(POE::Occultation)), + }; + } + + // point of no return + (||{ + let updates = { + let mut updates = Vec::with_capacity(pieces.len()); + + for ((piece, _bbox), pos) in izip!(pieces, layout) { + want_let!{ Some(gpc) = gs.pieces.get_mut(piece); else continue; } + gpc.pos = pos; + updates.push((piece, PUOs::Simple(PUO::Move(pos)))); + } + + updates + }; + + Some((PieceUpdate { + wrc, + log, + ops: PUOs::PerPlayer(default()), + }, updates.into_unprepared_nc())) + })() // <- no ?, shows it's infallible +} diff --git a/src/prelude.rs b/src/prelude.rs index caeeaf04..933de8a1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -134,6 +134,7 @@ pub use crate::keydata::*; pub use crate::mgmtchannel::*; pub use crate::nwtemplates; pub use crate::occultilks::*; +pub use crate::organise; pub use crate::pcrender::*; pub use crate::pieces::*; pub use crate::shapelib; -- 2.30.2