<commit_before>//! Implements a large hash-table that stores results of previously
//! performed searches ("transposition table").
//!
//! Chess programs, during their brute-force search, encounter the
//! same positions again and again, but from different sequences of
//! moves, which is called a "transposition". When the search
//! encounters a transposition, it is beneficial to "remember" what
//! was determined last time the position was examined, rather than
//! redoing the entire search again. For this reason, chess programs
//! have a transposition table, which is a large hash table storing
//! information about positions previously searched, how deeply they
//! were searched, and what we concluded about them.

use std;
use std::cell::{UnsafeCell, Cell};
use std::mem::transmute;
use basetypes::Value;
use chess_move::MoveDigest;

/// `BOUND_EXACT`, `BOUND_LOWER`, `BOUND_UPPER`, or `BOUND_NONE`.
///
/// For the majority of chess positions our evaluations will be more
/// or less inaccurate, and there is nothing we can do about it. But
/// sometimes we know that a given evaluation is probably inaccurate,
/// and we know the sign of the error. `BoundType` defines the
/// direction of such **known inaccuracies**.
///
/// * `BOUND_EXACT` means that the evaluation is exact (as far as we know).
///
/// * `BOUND_LOWER` means that the real value is greater or equal to
///    the evaluation (as far as we know).
///
/// * `BOUND_UPPER` means that the real value is lesser or equal to
///   the evaluation (as far as we know).
///
/// * `BOUND_NONE` means that the real value might be lesser, equal,
///   or grater than the evaluation.
pub type BoundType = u8;

pub const BOUND_NONE: BoundType = 0;
pub const BOUND_UPPER: BoundType = 0b10;
pub const BOUND_LOWER: BoundType = 0b01;
pub const BOUND_EXACT: BoundType = BOUND_UPPER | BOUND_LOWER;

/// Contains information about a particular position.
#[derive(Copy, Clone)]
pub struct TtEntry {
    move16: MoveDigest,
    value: Value,
    eval_value: Value,
    depth: u8,

    // The transposition table maintains a generation number for each
    // entry, which is used to implement an efficient replacement
    // strategy. This field stores the entry's generation (the highest
    // 6 bits) and the bound type (the lowest 2 bits).
    gen_bound: u8,
}

impl TtEntry {
    /// Creates a new instance.
    ///
    /// * `value` -- the value assigned to the position;
    ///
    /// * `bound` -- the accuracy of the assigned `value`;
    ///
    /// * `depth` -- the depth of search;
    ///
    /// * `move16` -- best or refutation move, or `0` if no move is
    ///   available;
    ///
    /// * `eval_value` -- the calculated static evaluation for the
    ///   position.
    pub fn new(value: Value,
               bound: BoundType,
               depth: u8,
               move16: MoveDigest,
               eval_value: Value)
               -> TtEntry {
        assert!(bound <= 0b11);
        assert!(depth < 127);
        TtEntry {
            move16: move16,
            value: value,
            eval_value: eval_value,
            depth: depth,
            gen_bound: bound,
        }
    }

    #[inline(always)]
    pub fn value(&self) -> Value {
        self.value
    }

    #[inline(always)]
    pub fn bound(&self) -> BoundType {
        self.gen_bound & 0b11
    }

    #[inline(always)]
    pub fn depth(&self) -> u8 {
        self.depth
    }

    #[inline(always)]
    pub fn move16(&self) -> MoveDigest {
        self.move16
    }

    #[inline(always)]
    pub fn eval_value(&self) -> Value {
        self.eval_value
    }

    /// Returns the contained data as one `u64` value.
    #[inline(always)]
    fn as_u64(&self) -> u64 {
        unsafe { transmute(*self) }
    }
}

/// A transposition table.
///
/// `Tt` methods (except `resize`) do not require a mutable reference
/// to do their work. This allows one transposition table instance to
/// be shared safely between many threads.
pub struct Tt {
    /// The current generation number. The lowest 2 bits will always
    /// be zeros.
    generation: Cell<u8>,

    /// The number of clusters in the table.
    cluster_count: usize,

    /// The transposition table consists of a vector of clusters. Each
    /// cluster stores 4 records.
    table: UnsafeCell<Vec<[Record; 4]>>,
}

impl Tt {
    /// Creates a new transposition table.
    ///
    /// The newly created table has the minimum possible size. Before
    /// using the new table for anything, `resize()` should be called
    /// on it, specifying the desired size.
    pub fn new() -> Tt {
        Tt {
            generation: Cell::new(0),
            cluster_count: 1,
            table: UnsafeCell::new(vec![Default::default()]),
        }
    }

    /// Resizes the transpositon table. All entries in the table will
    /// be lost.
    ///
    /// `size_mb` is the desired new size in Mbytes. If `size_mb` is
    /// not in the form "2**n", the new size of the transposition
    /// table will be as close as possible, but less than `size_mb`.
    pub fn resize(&mut self, size_mb: usize) {
        let requested_cluster_count = (size_mb * 1024 * 1024) / std::mem::size_of::<[Record; 4]>();

        // Calculate the new cluster count. (To do this, first we make
        // sure that `requested_cluster_count` is exceeded. Then we
        // make one step back.)
        let mut new_cluster_count = 1;
        while new_cluster_count <= requested_cluster_count && new_cluster_count != 0 {
            new_cluster_count <<= 1;
        }
        if new_cluster_count > 1 {
            new_cluster_count >>= 1;
        } else {
            new_cluster_count = 1;
        }
        assert!(new_cluster_count > 0);

        // Allocate the new vector of clusters.
        self.generation.set(0);
        self.cluster_count = new_cluster_count;
        self.table = UnsafeCell::new(vec![Default::default(); new_cluster_count]);
    }

    /// Returns the size of the transposition table in Mbytes.
    pub fn size(&self) -> usize {
        unsafe { &*self.table.get() }.len() * std::mem::size_of::<[Record; 4]>() / 1024 / 1024
    }

    /// Signals that a new search is about to begin.
    pub fn new_search(&self) {
        self.generation.set(self.generation.get().wrapping_add(0b100));
        assert_eq!(self.generation.get() & 0b11, 0);
    }

    /// Stores data by a specific key.
    ///
    /// After being stored, the data might be retrieved by
    /// `probe(key)`. This is not guaranteed though, because the entry
    /// might have been overwritten in the meantime.
    pub fn store(&self, key: u64, mut data: TtEntry) {
        // `store` and `probe` jointly implement a clever lock-less
        // hashing strategy. Rather than storing two disjoint items,
        // the key is stored XOR-ed with data, while data is stored
        // additionally as usual.

        // Set the entry's generation.
        data.gen_bound |= self.generation.get();

        // Choose a slot to which to write the data. (Each cluster has
        // 4 slots.)
        let mut cluster = unsafe { self.cluster_mut(key) };
        let mut replace_index = 0;
        let mut replace_score = 0xff;
        for (i, record) in cluster.iter_mut().enumerate() {
            // Check if this is an empty slot, or an old record for
            // the same key. If this this is the case we will use this
            // slot for the new record.
            if record.key == 0 || record.key ^ record.data.as_u64() == key {
                if data.move16 == 0 {
                    data.move16 = record.data.move16; // Preserve any existing move.
                }
                replace_index = i;
                break;
            }

            // Calculate the score for this record. If we can not find
            // an empty slot or an old record, the replaced record
            // will be the record with the lowest score.
            let record_score = self.calc_score(record);
            if record_score < replace_score {
                replace_index = i;
                replace_score = record_score;
            }
        }

        // Write the data to the chosen slot.
        unsafe {
            cluster.get_unchecked_mut(replace_index).key = key ^ data.as_u64();
            cluster.get_unchecked_mut(replace_index).data = data;
        }
    }

    /// Probes for data by a specific key.
    ///
    /// **Note:** This method may write to the transposition table
    /// (for example, to update the generation of the entry).
    #[inline]
    pub fn probe(&self, key: u64) -> Option<TtEntry> {
        let cluster = unsafe { self.cluster_mut(key) };
        for record in cluster.iter_mut() {
            if record.key ^ record.data.as_u64() == key {
                // If `key` and `data` were written simultaneously by
                // different search instances with different keys,
                // this will yield in a mismatch of the above
                // comparison (except for the rare and inherent key
                // collisions).
                record.set_generation(self.generation.get());
                return Some(record.data);
            }
        }
        None
    }

    /// Peeks for data by a specific key.
    ///
    /// This method does the same as `probe`, except it will never
    /// write to the transposition table. This is useful, for example,
    /// when we want to extract the primary variation from the the
    /// transposition table, without affecting any entries in the
    /// table.
    #[inline]
    pub fn peek(&self, key: u64) -> Option<TtEntry> {
        let cluster = unsafe { self.cluster_mut(key) };
        for record in cluster.iter_mut() {
            if record.key ^ record.data.as_u64() == key {
                return Some(record.data);
            }
        }
        None
    }

    /// Removes all entries in the table.
    pub fn clear(&self) {
        let table = unsafe { self.table.get().as_mut().unwrap() };
        for cluster in table {
            for record in cluster.iter_mut() {
                *record = Default::default();
            }
        }
        self.generation.set(0);
    }

    /// A helper method for `store`. It implements the record
    /// replacement strategy.
    #[inline(always)]
    fn calc_score(&self, record: &Record) -> u8 {
        // Here we try to return higher values for the records that
        // are move likely to save CPU work in the future:

        // Positions from the current generation are always scored
        // higher than positions from older generations.
        (if record.generation() == self.generation.get() {
            128
        } else {
            0
        })

        // Positions with higher search depths are scored higher.
        + record.data.depth()

        // Positions with exact evaluations are given slight advantage.
        + (if record.data.bound() == BOUND_EXACT {
            1
        } else {
            0
        })
    }

    /// A helper method for `probe` and `store`. It returns the
    /// cluster for a given key.
    #[inline]
    unsafe fn cluster_mut(&self, key: u64) -> &mut [Record; 4] {
        let cluster_index = (key & (self.cluster_count - 1) as u64) as usize;
        self.table.get().as_mut().unwrap().get_unchecked_mut(cluster_index)
    }
}

unsafe impl Sync for Tt {}

/// Represents a record in the transposition table.
///
/// It consists of 16 bytes, and is laid out the following way:
///
/// * key        64 bit
/// * move16     16 bit
/// * value      16 bit
/// * eval value 16 bit
/// * depth       8 bit
/// * generation  6 bit
/// * bound type  2 bit
#[derive(Copy, Clone)]
struct Record {
    key: u64,
    data: TtEntry,
}

impl Default for Record {
    fn default() -> Record {
        Record {
            key: 0,
            data: unsafe { transmute(0u64) },
        }
    }
}

impl Record {
    #[inline(always)]
    fn generation(&self) -> u8 {
        self.data.gen_bound & 0b11111100
    }

    #[inline(always)]
    fn set_generation(&mut self, generation: u8) {
        assert_eq!(generation & 0b11, 0);

        // Since the `key` is saved XOR-ed with the data, when we
        // change the data, we have to change the stored `key` as
        // well.
        let old_data_as_u64 = self.data.as_u64();
        self.data.gen_bound = generation | self.data.bound();
        self.key ^= old_data_as_u64 ^ self.data.as_u64();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::Record;
    use std;

    #[test]
    fn test_cluster_size() {
        assert_eq!(std::mem::size_of::<[Record; 4]>(), 64);
        assert_eq!(std::mem::size_of::<Record>(), 16);
    }

    #[test]
    fn test_tt_resize() {
        let mut tt = Tt::new();
        assert_eq!(unsafe { &*tt.table.get() }.capacity(), 1);
        tt.resize(1);
        assert_eq!(tt.size(), 1);
        tt.clear();
    }

    #[test]
    fn test_store_and_probe() {
        let tt = Tt::new();
        assert!(tt.probe(1).is_none());
        let data = TtEntry::new(0, 0, 100, 666, 0);
        assert_eq!(data.depth(), 100);
        assert_eq!(data.move16(), 666);
        tt.store(1, data);
        assert_eq!(tt.probe(1).unwrap().depth(), 100);
        tt.store(1, TtEntry::new(0, 0, 100, 666, 0));
        assert_eq!(tt.probe(1).unwrap().depth(), 100);
        assert_eq!(tt.probe(1).unwrap().move16(), 666);
        for i in 2..100 {
            tt.store(i, TtEntry::new(i as i16, 0, i as u8, i as u16, i as i16));
        }
        assert_eq!(tt.probe(1).unwrap().depth(), 100);
        assert_eq!(tt.probe(99).unwrap().depth(), 99);
        assert_eq!(tt.probe(98).unwrap().depth(), 98);
        assert_eq!(tt.probe(97).unwrap().depth(), 97);
        tt.clear();
        assert!(tt.probe(1).is_none());
        tt.store(1, data);
        tt.new_search();
        tt.probe(1);
        assert!(tt.probe(1).is_some());
    }

    #[test]
    fn test_new_search() {
        let tt = Tt::new();
        assert_eq!(tt.generation.get(), 0 << 2);
        tt.new_search();
        assert_eq!(tt.generation.get(), 1 << 2);
        for _ in 0..64 {
            tt.new_search();
        }
        assert_eq!(tt.generation.get(), 1 << 2);
    }
}
<commit_msg>fix Tt::new_search()<commit_after>//! Implements a large hash-table that stores results of previously
//! performed searches ("transposition table").
//!
//! Chess programs, during their brute-force search, encounter the
//! same positions again and again, but from different sequences of
//! moves, which is called a "transposition". When the search
//! encounters a transposition, it is beneficial to "remember" what
//! was determined last time the position was examined, rather than
//! redoing the entire search again. For this reason, chess programs
//! have a transposition table, which is a large hash table storing
//! information about positions previously searched, how deeply they
//! were searched, and what we concluded about them.

use std;
use std::cell::{UnsafeCell, Cell};
use std::cmp::min;
use std::mem::transmute;
use basetypes::Value;
use chess_move::MoveDigest;

/// `BOUND_EXACT`, `BOUND_LOWER`, `BOUND_UPPER`, or `BOUND_NONE`.
///
/// For the majority of chess positions our evaluations will be more
/// or less inaccurate, and there is nothing we can do about it. But
/// sometimes we know that a given evaluation is probably inaccurate,
/// and we know the sign of the error. `BoundType` defines the
/// direction of such **known inaccuracies**.
///
/// * `BOUND_EXACT` means that the evaluation is exact (as far as we know).
///
/// * `BOUND_LOWER` means that the real value is greater or equal to
///    the evaluation (as far as we know).
///
/// * `BOUND_UPPER` means that the real value is lesser or equal to
///   the evaluation (as far as we know).
///
/// * `BOUND_NONE` means that the real value might be lesser, equal,
///   or grater than the evaluation.
pub type BoundType = u8;

pub const BOUND_NONE: BoundType = 0;
pub const BOUND_UPPER: BoundType = 0b10;
pub const BOUND_LOWER: BoundType = 0b01;
pub const BOUND_EXACT: BoundType = BOUND_UPPER | BOUND_LOWER;

/// Contains information about a particular position.
#[derive(Copy, Clone)]
pub struct TtEntry {
    move16: MoveDigest,
    value: Value,
    eval_value: Value,
    depth: u8,

    // The transposition table maintains a generation number for each
    // entry, which is used to implement an efficient replacement
    // strategy. This field stores the entry's generation (the highest
    // 6 bits) and the bound type (the lowest 2 bits).
    gen_bound: u8,
}

impl TtEntry {
    /// Creates a new instance.
    ///
    /// * `value` -- the value assigned to the position;
    ///
    /// * `bound` -- the accuracy of the assigned `value`;
    ///
    /// * `depth` -- the depth of search;
    ///
    /// * `move16` -- best or refutation move, or `0` if no move is
    ///   available;
    ///
    /// * `eval_value` -- the calculated static evaluation for the
    ///   position.
    pub fn new(value: Value,
               bound: BoundType,
               depth: u8,
               move16: MoveDigest,
               eval_value: Value)
               -> TtEntry {
        assert!(bound <= 0b11);
        assert!(depth < 127);
        TtEntry {
            move16: move16,
            value: value,
            eval_value: eval_value,
            depth: depth,
            gen_bound: bound,
        }
    }

    #[inline(always)]
    pub fn value(&self) -> Value {
        self.value
    }

    #[inline(always)]
    pub fn bound(&self) -> BoundType {
        self.gen_bound & 0b11
    }

    #[inline(always)]
    pub fn depth(&self) -> u8 {
        self.depth
    }

    #[inline(always)]
    pub fn move16(&self) -> MoveDigest {
        self.move16
    }

    #[inline(always)]
    pub fn eval_value(&self) -> Value {
        self.eval_value
    }

    /// Returns the contained data as one `u64` value.
    #[inline(always)]
    fn as_u64(&self) -> u64 {
        unsafe { transmute(*self) }
    }
}

/// A transposition table.
///
/// `Tt` methods (except `resize`) do not require a mutable reference
/// to do their work. This allows one transposition table instance to
/// be shared safely between many threads.
pub struct Tt {
    /// The current generation number. The lowest 2 bits will always
    /// be zeros.
    generation: Cell<u8>,

    /// The number of clusters in the table.
    cluster_count: usize,

    /// The transposition table consists of a vector of clusters. Each
    /// cluster stores 4 records.
    table: UnsafeCell<Vec<[Record; 4]>>,
}

impl Tt {
    /// Creates a new transposition table.
    ///
    /// The newly created table has the minimum possible size. Before
    /// using the new table for anything, `resize()` should be called
    /// on it, specifying the desired size.
    pub fn new() -> Tt {
        Tt {
            generation: Cell::new(0),
            cluster_count: 1,
            table: UnsafeCell::new(vec![Default::default()]),
        }
    }

    /// Resizes the transposition table. All entries in the table will
    /// be lost.
    ///
    /// `size_mb` is the desired new size in Mbytes. If `size_mb` is
    /// not in the form "2**n", the new size of the transposition
    /// table will be as close as possible, but less than `size_mb`.
    pub fn resize(&mut self, size_mb: usize) {
        let requested_cluster_count = (size_mb * 1024 * 1024) / std::mem::size_of::<[Record; 4]>();

        // Calculate the new cluster count. (To do this, first we make
        // sure that `requested_cluster_count` is exceeded. Then we
        // make one step back.)
        let mut new_cluster_count = 1;
        while new_cluster_count <= requested_cluster_count && new_cluster_count != 0 {
            new_cluster_count <<= 1;
        }
        if new_cluster_count > 1 {
            new_cluster_count >>= 1;
        } else {
            new_cluster_count = 1;
        }
        assert!(new_cluster_count > 0);

        // Allocate the new vector of clusters.
        self.generation.set(0);
        self.cluster_count = new_cluster_count;
        self.table = UnsafeCell::new(vec![Default::default(); new_cluster_count]);
    }

    /// Returns the size of the transposition table in Mbytes.
    pub fn size(&self) -> usize {
        unsafe { &*self.table.get() }.len() * std::mem::size_of::<[Record; 4]>() / 1024 / 1024
    }

    /// Signals that a new search is about to begin.
    pub fn new_search(&self) {
        const N: usize = 128;

        loop {
            // Increment `self.generation` (with wrapping).
            self.generation.set(self.generation.get().wrapping_add(0b100));
            assert_eq!(self.generation.get() & 0b11, 0);

            // Count how many staled records from this generation
            // there are among the first `N` clusters.
            let mut staled = 0;
            let mut cluster_iter = unsafe { self.table.get().as_ref().unwrap().iter() };
            for _ in 0..min(N, self.cluster_count) {
                for record in cluster_iter.next().unwrap() {
                    if record.key != 0 && record.generation() == self.generation.get() {
                        staled += 1;
                    }
                }
            }

            if staled < N {
                // We exit the loop only if the staled records from
                // this generation are not too many. (We may get too
                // many staled records if, for example, a very long
                // search was executed long time ago.)
                break;
            }
        }
    }

    /// Stores data by a specific key.
    ///
    /// After being stored, the data might be retrieved by
    /// `probe(key)`. This is not guaranteed though, because the entry
    /// might have been overwritten in the meantime.
    pub fn store(&self, key: u64, mut data: TtEntry) {
        // `store` and `probe` jointly implement a clever lock-less
        // hashing strategy. Rather than storing two disjoint items,
        // the key is stored XOR-ed with data, while data is stored
        // additionally as usual.

        // Set the entry's generation.
        data.gen_bound |= self.generation.get();

        // Choose a slot to which to write the data. (Each cluster has
        // 4 slots.)
        let mut cluster = unsafe { self.cluster_mut(key) };
        let mut replace_index = 0;
        let mut replace_score = 0xff;
        for (i, record) in cluster.iter_mut().enumerate() {
            // Check if this is an empty slot, or an old record for
            // the same key. If this this is the case we will use this
            // slot for the new record.
            if record.key == 0 || record.key ^ record.data.as_u64() == key {
                if data.move16 == 0 {
                    data.move16 = record.data.move16; // Preserve any existing move.
                }
                replace_index = i;
                break;
            }

            // Calculate the score for this record. If we can not find
            // an empty slot or an old record, the replaced record
            // will be the record with the lowest score.
            let record_score = self.calc_score(record);
            if record_score < replace_score {
                replace_index = i;
                replace_score = record_score;
            }
        }

        // Write the data to the chosen slot.
        unsafe {
            cluster.get_unchecked_mut(replace_index).key = key ^ data.as_u64();
            cluster.get_unchecked_mut(replace_index).data = data;
        }
    }

    /// Probes for data by a specific key.
    ///
    /// **Note:** This method may write to the transposition table
    /// (for example, to update the generation of the entry).
    #[inline]
    pub fn probe(&self, key: u64) -> Option<TtEntry> {
        let cluster = unsafe { self.cluster_mut(key) };
        for record in cluster.iter_mut() {
            if record.key ^ record.data.as_u64() == key {
                // If `key` and `data` were written simultaneously by
                // different search instances with different keys,
                // this will yield in a mismatch of the above
                // comparison (except for the rare and inherent key
                // collisions).
                record.set_generation(self.generation.get());
                return Some(record.data);
            }
        }
        None
    }

    /// Peeks for data by a specific key.
    ///
    /// This method does the same as `probe`, except it will never
    /// write to the transposition table. This is useful, for example,
    /// when we want to extract the primary variation from the the
    /// transposition table, without affecting any entries in the
    /// table.
    #[inline]
    pub fn peek(&self, key: u64) -> Option<TtEntry> {
        let cluster = unsafe { self.cluster_mut(key) };
        for record in cluster.iter_mut() {
            if record.key ^ record.data.as_u64() == key {
                return Some(record.data);
            }
        }
        None
    }

    /// Removes all entries in the table.
    pub fn clear(&self) {
        let table = unsafe { self.table.get().as_mut().unwrap() };
        for cluster in table {
            for record in cluster.iter_mut() {
                *record = Default::default();
            }
        }
        self.generation.set(0);
    }

    /// A helper method for `store`. It implements the record
    /// replacement strategy.
    #[inline(always)]
    fn calc_score(&self, record: &Record) -> u8 {
        // Here we try to return higher values for the records that
        // are move likely to save CPU work in the future:

        // Positions from the current generation are always scored
        // higher than positions from older generations.
        (if record.generation() == self.generation.get() {
            128
        } else {
            0
        })

        // Positions with higher search depths are scored higher.
        + record.data.depth()

        // Positions with exact evaluations are given slight advantage.
        + (if record.data.bound() == BOUND_EXACT {
            1
        } else {
            0
        })
    }

    /// A helper method for `probe` and `store`. It returns the
    /// cluster for a given key.
    #[inline]
    unsafe fn cluster_mut(&self, key: u64) -> &mut [Record; 4] {
        let cluster_index = (key & (self.cluster_count - 1) as u64) as usize;
        self.table.get().as_mut().unwrap().get_unchecked_mut(cluster_index)
    }
}

unsafe impl Sync for Tt {}

/// Represents a record in the transposition table.
///
/// It consists of 16 bytes, and is laid out the following way:
///
/// * key        64 bit
/// * move16     16 bit
/// * value      16 bit
/// * eval value 16 bit
/// * depth       8 bit
/// * generation  6 bit
/// * bound type  2 bit
#[derive(Copy, Clone)]
struct Record {
    key: u64,
    data: TtEntry,
}

impl Default for Record {
    fn default() -> Record {
        Record {
            key: 0,
            data: unsafe { transmute(0u64) },
        }
    }
}

impl Record {
    #[inline(always)]
    fn generation(&self) -> u8 {
        self.data.gen_bound & 0b11111100
    }

    #[inline(always)]
    fn set_generation(&mut self, generation: u8) {
        assert_eq!(generation & 0b11, 0);

        // Since the `key` is saved XOR-ed with the data, when we
        // change the data, we have to change the stored `key` as
        // well.
        let old_data_as_u64 = self.data.as_u64();
        self.data.gen_bound = generation | self.data.bound();
        self.key ^= old_data_as_u64 ^ self.data.as_u64();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use super::Record;
    use std;

    #[test]
    fn test_cluster_size() {
        assert_eq!(std::mem::size_of::<[Record; 4]>(), 64);
        assert_eq!(std::mem::size_of::<Record>(), 16);
    }

    #[test]
    fn test_tt_resize() {
        let mut tt = Tt::new();
        assert_eq!(unsafe { &*tt.table.get() }.capacity(), 1);
        tt.resize(1);
        assert_eq!(tt.size(), 1);
        tt.clear();
    }

    #[test]
    fn test_store_and_probe() {
        let tt = Tt::new();
        assert!(tt.probe(1).is_none());
        let data = TtEntry::new(0, 0, 100, 666, 0);
        assert_eq!(data.depth(), 100);
        assert_eq!(data.move16(), 666);
        tt.store(1, data);
        assert_eq!(tt.probe(1).unwrap().depth(), 100);
        tt.store(1, TtEntry::new(0, 0, 100, 666, 0));
        assert_eq!(tt.probe(1).unwrap().depth(), 100);
        assert_eq!(tt.probe(1).unwrap().move16(), 666);
        for i in 2..100 {
            tt.store(i, TtEntry::new(i as i16, 0, i as u8, i as u16, i as i16));
        }
        assert_eq!(tt.probe(1).unwrap().depth(), 100);
        assert_eq!(tt.probe(99).unwrap().depth(), 99);
        assert_eq!(tt.probe(98).unwrap().depth(), 98);
        assert_eq!(tt.probe(97).unwrap().depth(), 97);
        tt.clear();
        assert!(tt.probe(1).is_none());
        tt.store(1, data);
        tt.new_search();
        tt.probe(1);
        assert!(tt.probe(1).is_some());
    }

    #[test]
    fn test_new_search() {
        let tt = Tt::new();
        assert_eq!(tt.generation.get(), 0 << 2);
        tt.new_search();
        assert_eq!(tt.generation.get(), 1 << 2);
        for _ in 0..64 {
            tt.new_search();
        }
        assert_eq!(tt.generation.get(), 1 << 2);
    }
}
<|endoftext|>


<commit_before>use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::copy;
use std::path::Path;
use std::str::FromStr;
use std::rc::Rc;

use iup;
use iup::prelude::*;
use iup::control::{Button, Label, List, Text};
use iup::dialog::{FileDlg};
use iup::element::{Handle};
use iup::led;

use time;
use xml;

use parser::{read_path, write_path};
use property::{Property, PropertyMap};

// Since we need to share mutable state with 'static ui callbacks,
// we clone a refcounted cell for moving into each callback.
type PropertyMapRc = Rc<RefCell<PropertyMap>>;

// LED dialog specification.
static DIALOG: &'static str = include_str!("../resources/ui.led");

// Get an element from handle
fn from_handle<E>(handle: iup::element::Handle) -> E where E: Element {
    E::from_handle(handle).unwrap()
}

// Get an element from named handle
fn from_name<E>(name: &str) -> E where E: Element {
    E::from_handle(E::from_name(name).unwrap()).unwrap()
}

// Data-bind an element to a numeric property value.
//
// Value of the element is set to the current value of the property, and
// changes to element value are written back to the property map.
//
// @param props {PropertyMapRc} a cloned refcounted property map.
//
fn bind<T, E>(elem: &mut E, props: PropertyMapRc, key: &'static str)
    where Property: From<T>,
          T: FromStr + Default,
          E: Element + ValueChangedCb {

    // Remove previous bindings, if any.
    elem.remove_valuechanged_cb();

    if let Some(ref prop) = props.borrow().get(key) {
        elem.set_attrib("VALUE", prop.to_string());
    }
    elem.set_valuechanged_cb(move |(elem,): (E,)| {
        if let Some(ref value) = elem.attrib("VALUE") {
            if let Ok(v) = T::from_str(value) {
                props.borrow_mut().insert(key.to_string(), Property::from(v));
            } else {
                props.borrow_mut().insert(key.to_string(), Property::from(T::default()));
            }
        }
    });
}

// Data-bind an element to a list property value, at given index.
//
// Value of the element is set to the current value of the property at index,
// and changes to element value are written back to the property map.
//
// @param props {PropertyMapRc} a cloned refcounted property map.
//
fn bind_list<E>(elem: &mut E, props: PropertyMapRc, key: &'static str, index: usize)
    where E: Element + ValueChangedCb {

    // Remove previous bindings, if any.
    elem.remove_valuechanged_cb();

    if let Some(&Property::List(ref v)) = props.borrow().get(key) {
        elem.set_attrib("VALUE", v[index].to_string());
    }
    elem.set_valuechanged_cb(move |(elem,): (E,)| {
        if let Some(ref value) = elem.attrib("VALUE") {
            if let Some(&mut Property::List(ref mut v)) = props.borrow_mut().get_mut(key) {
                v[index] = value.to_string();
            }
        }
    });
}

macro_rules! bind_stat {
    ($i:ident, $p:expr, $e:expr) => {
        bind::<f32,_>(&mut from_name::<Text>(stringify!($i)), $p.clone(), $e);
    }
}

macro_rules! bind_skill {
    ($i:ident, $p:expr, $n:expr) => {
        bind_list::<_>(&mut from_handle::<Text>($i), $p.clone(), "SkillPoints", $n);
    }
}

// Data-bind all elements relevant to a party member.
//
// @param props {PropertyMapRc} a cloned refcounted property map.
//
fn bind_member(props: PropertyMapRc) {
    bind_stat!(text_int, props, "Int");
    bind_stat!(text_dex, props, "Dex");
    bind_stat!(text_str, props, "Str");
    bind_stat!(text_occ, props, "Occ");
    bind_stat!(text_per, props, "Per");

    bind_stat!(text_wpn_sword,  props, "WpnSword");
    bind_stat!(text_wpn_short,  props, "WpnShortSword");
    bind_stat!(text_wpn_blunt,  props, "WpnSceptor");
    bind_stat!(text_wpn_cleave, props, "WpnAxe");
    bind_stat!(text_wpn_whip,   props, "WpnWhip");
    bind_stat!(text_wpn_bow,    props, "WpnBow");
    bind_stat!(text_wpn_xbow,   props, "WpnXbow");
    bind_stat!(text_wpn_elixir, props, "WpnElixir");

    if let Some(apt_grid) = Handle::from_named("apt_grid") {
        for i in 1..7 {
            if let Some(child) = apt_grid.child((i - 1) * 2 + 1) {
                bind_skill!(child, props, i);
            }
        }
    }
    if let Some(skill_grid) = Handle::from_named("skill_grid") {
        for i in 7..115 {
            if let Some(child) = skill_grid.child((i - 7) * 2 + 1) {
                bind_skill!(child, props, i);
            }
        }
    }
}

/// Ui entry point.
///
/// Starts by showing a direction selection dialog; after the user selects a directory,
/// the game is loaded from that directory and values bound to the ui elements.
///
pub fn ui_loop() -> Result<(), String> {
    match iup::with_iup(|| {
        // See also led::load(path) to load from a file
        led::load_buffer(DIALOG).unwrap();

        // Select saved game location
        let mut dlg_open = from_name::<FileDlg>("dlg_open");
        let dir = match dlg_open.popup(DialogPos::CenterParent, DialogPos::CenterParent) {
            Ok(..) => match dlg_open.attrib("STATUS") {
                Some(ref s) if s == "0" => {
                    dlg_open.attrib("VALUE").unwrap()
                },
                _ => return Err("File selection cancelled.".to_string())
            },
            _ => return Err("File selection failed.".to_string())
        };

        // Read game and party member files
        let game = Rc::new(RefCell::new({
            let path = Path::new(&dir).join("Game.txt");
            match read_path(path.as_path()) {
                Ok(v) => v,
                Err(e) => {
                    return Err(format!("Cannot read {:?}: {}", path, e))
                }
            }
        }));
        let party = Rc::new(RefCell::new({
            let mut members: Vec<PropertyMapRc> = Vec::new();
            if let Some(&Property::String(ref ids)) = game.borrow().get("PartyIDs") {
                for id in ids.split(",") {
                    let path = Path::new(&dir).join("Party".to_string() + id + ".txt");
                    match read_path(path.as_path()) {
                        Ok(v) => members.push(Rc::new(RefCell::new(v))),
                        Err(e) => {
                            return Err(format!("Cannot read {:?}: {}", path, e))
                        }
                    };
                }
            }
            members
        }));

        let mut text_emeralds = from_name::<Text>("text_emeralds");
        bind::<u32,_>(&mut text_emeralds, game.clone(), "Emeralds");

        let mut list_party = from_name::<List>("list_party");
        let mut list_party_items: Vec<String> = Vec::new();
        for member in party.borrow().iter().skip(1) {
            if let Some(&Property::String(ref name)) = member.borrow().get("Name") {
                if let Some(&Property::Float(level)) = member.borrow().get("Level") {
                    list_party_items.push(format!("{} ({})", name, level));
                }
            }
        }

        let skills = load_skills();
        if let Some(mut apt_grid) = Handle::from_named("apt_grid") {
            while let Some(mut child) = apt_grid.child(0) {
                child.detach().destroy();
            }
            for i in 1..7 {
                let mut label = Label::new()
                    .set_attrib("SIZE", "92x12".to_string())
                    .set_attrib("TITLE", "(empty)".to_string());
                let mut text = Text::new_spin()
                    .set_attrib("SIZE", "36x12".to_string())
                    .set_attrib("SPINMAX", "99".to_string())
                    .set_attrib("MASKINT", "0:99".to_string())
                    .set_attrib("ALIGNMENT", "ARIGHT".to_string());
                if let Some(ref skill) = skills.get(&i) {
                    label.set_attrib("TITLE", skill.name.to_string());
                } else {
                    label.set_attrib("ACTIVE", "NO");
                    text.set_attrib("ACTIVE", "NO");
                }
                apt_grid.append(label).unwrap();
                apt_grid.append(text).unwrap();
            }
        }
        if let Some(mut skill_grid) = Handle::from_named("skill_grid") {
            while let Some(mut child) = skill_grid.child(0) {
                child.detach().destroy();
            }
            for i in 7..115 {
                let mut label = Label::new()
                    .set_attrib("SIZE", "92x12".to_string())
                    .set_attrib("TITLE", "(empty)".to_string());
                let mut text = Text::new_spin()
                    .set_attrib("SIZE", "36x12".to_string())
                    .set_attrib("SPINMAX", "99".to_string())
                    .set_attrib("MASKINT", "0:99".to_string())
                    .set_attrib("ALIGNMENT", "ARIGHT".to_string());
                if let Some(ref skill) = skills.get(&i) {
                    label.set_attrib("TITLE", skill.name.to_string());
                } else {
                    label.set_attrib("ACTIVE", "NO");
                    text.set_attrib("ACTIVE", "NO");
                }
                skill_grid.append(label).unwrap();
                skill_grid.append(text).unwrap();
            }
        }

        let party_clone = party.clone();
        list_party.set_items(list_party_items);
        list_party.set_action(move |(_, _, i, _)| {
            let member = party_clone.borrow()[i as usize].clone();
            bind_member(member);
        });
        bind_member(party.borrow()[1].clone());

        // Write game and party member files on save
        let mut button_save = from_name::<Button>("button_save");
        {
            let game_clone = game.clone();
            let party_clone = party.clone();
            button_save.set_action(move |_| {
                let timestamp = time::strftime("%FT%H.%M.%SZ.txt", &time::now_utc()).unwrap();
                let path = Path::new(&dir).join("Game.txt");
                copy(path.as_path(), path.with_extension(&timestamp)).unwrap();
                write_path(path.as_path(), &game_clone.borrow()).unwrap();
                for member in party_clone.borrow().iter().skip(1) {

                    // Validate
                    let mut combat_skills: Vec<String> = Vec::new();
                    let mut spell_skills: Vec<String> = Vec::new();
                    if let Some(&Property::List(ref v)) = member.borrow().get("SkillPoints") {
                        let spell_grade = {
                            if let Some(&Property::Float(int_mod)) = member.borrow().get("Int") {
                                if let Some(&Property::Float(occ_mod)) = member.borrow().get("Occ") {
                                    int_mod + occ_mod + 20f32
                                } else {
                                    0f32
                                }
                            } else {
                                0f32
                            }
                        };
                        for (i, ref val) in v.iter().enumerate() {
                            match val.parse::<u32>() {
                                // Combat skills (1-3)
                                Ok(n) if i >= 7 && i <= 61 && n > 0 => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        combat_skills.push(skill.internal.to_string())
                                    }
                                }
                                // Spell skills (1)
                                Ok(n) if i >= 62 && i <= 72 && (n > 0 || spell_grade >= 21f32) => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        spell_skills.push(skill.internal.to_string())
                                    }
                                }
                                // Spell skills (2)
                                Ok(n) if i >= 73 && i <= 86 && (n > 0 || spell_grade >= 26f32) => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        spell_skills.push(skill.internal.to_string())
                                    }
                                }
                                // Spell skills (3)
                                Ok(n) if i >= 87 && i <= 114 && (n > 0 || spell_grade >= 32f32) => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        spell_skills.push(skill.internal.to_string())
                                    }
                                }
                                _ => {}
                            }
                        }
                    }
                    if let Some(&mut Property::List(ref mut v)) = member.borrow_mut().get_mut("CombatSelects") {
                        v.retain(|ref x| combat_skills.contains(&x));
                    }
                    if let Some(&mut Property::List(ref mut v)) = member.borrow_mut().get_mut("SpellFavorites") {
                        v.retain(|ref x| spell_skills.contains(&x));
                    }
                    member.borrow_mut().insert("CombatSkills".to_string(), Property::List(combat_skills));
                    member.borrow_mut().insert("SpellSkills".to_string(), Property::List(spell_skills));

                    if let Some(&Property::String(ref id)) = member.borrow().get("PartyID") {
                        let path = Path::new(&dir).join("Party".to_string() + id + ".txt");
                        copy(path.as_path(), path.with_extension(&timestamp)).unwrap();
                        write_path(path.as_path(), &member.borrow()).unwrap();
                    }
                }
            });
        }
        let mut button_close = from_name::<Button>("button_close");
        button_close.set_action(|_| {
            CallbackReturn::Close
        });

        let mut dlg = from_name::<Dialog>("dlg");
        dlg.show()

    }) {
        Err(iup::InitError::UserError(s)) => Err(s),
        Err(e) => Err(format!("{:?}", e)),
        _ => Ok(())
    }
}

struct Skill {
    name: String,
    internal: String,
}

fn load_skills() -> HashMap<usize, Skill> {
    let mut skills: HashMap<usize, Skill> = HashMap::with_capacity(115);
    if let Ok(elem) = include_str!("../resources/skills.xml").parse::<xml::Element>() {
        for child in elem.get_children("skill", None) {
            let name = child.get_children("name", None).nth(0).map(|ref e| e.content_str());
            let desc = child.get_children("description", None).nth(0).map(|ref e| e.content_str());
            match name {
                Some(ref name) if !name.is_empty() => {
                    match desc {
                        Some(ref desc) if !desc.is_empty() => {
                            let id = child.attributes
                                .get(&("number".to_string(), None)).unwrap()
                                .parse::<usize>()
                                .unwrap();
                            let internal = child.attributes
                                .get(&("spritename".to_string(), None)).unwrap_or(name);
                            skills.insert(id, Skill {
                                name: name.to_owned(),
                                internal: internal.to_owned(),
                            });
                        }
                        _ => {}
                    }
                }
                _ => {}
            }
        }
    }
    skills
}
<commit_msg>Fix crash when skill list is too short (expand it)<commit_after>use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::copy;
use std::path::Path;
use std::str::FromStr;
use std::rc::Rc;

use iup;
use iup::prelude::*;
use iup::control::{Button, Label, List, Text};
use iup::dialog::{FileDlg};
use iup::element::{Handle};
use iup::led;

use time;
use xml;

use parser::{read_path, write_path};
use property::{Property, PropertyMap};

// Since we need to share mutable state with 'static ui callbacks,
// we clone a refcounted cell for moving into each callback.
type PropertyMapRc = Rc<RefCell<PropertyMap>>;

// LED dialog specification.
static DIALOG: &'static str = include_str!("../resources/ui.led");

// Get an element from handle
fn from_handle<E>(handle: iup::element::Handle) -> E where E: Element {
    E::from_handle(handle).unwrap()
}

// Get an element from named handle
fn from_name<E>(name: &str) -> E where E: Element {
    E::from_handle(E::from_name(name).unwrap()).unwrap()
}

// Data-bind an element to a numeric property value.
//
// Value of the element is set to the current value of the property, and
// changes to element value are written back to the property map.
//
// @param props {PropertyMapRc} a cloned refcounted property map.
//
fn bind<T, E>(elem: &mut E, props: PropertyMapRc, key: &'static str)
    where Property: From<T>,
          T: FromStr + Default,
          E: Element + ValueChangedCb {

    // Remove previous bindings, if any.
    elem.remove_valuechanged_cb();

    if let Some(ref prop) = props.borrow().get(key) {
        elem.set_attrib("VALUE", prop.to_string());
    }
    elem.set_valuechanged_cb(move |(elem,): (E,)| {
        if let Some(ref value) = elem.attrib("VALUE") {
            if let Ok(v) = T::from_str(value) {
                props.borrow_mut().insert(key.to_string(), Property::from(v));
            } else {
                props.borrow_mut().insert(key.to_string(), Property::from(T::default()));
            }
        }
    });
}

// Data-bind an element to a list property value, at given index.
//
// Value of the element is set to the current value of the property at index,
// and changes to element value are written back to the property map.
//
// @param props {PropertyMapRc} a cloned refcounted property map.
//
fn bind_list<E>(elem: &mut E, props: PropertyMapRc, key: &'static str, index: usize)
    where E: Element + ValueChangedCb {

    // Remove previous bindings, if any.
    elem.remove_valuechanged_cb();

    if let Some(&Property::List(ref v)) = props.borrow().get(key) {
        elem.set_attrib("VALUE", v[index].to_string());
    }
    elem.set_valuechanged_cb(move |(elem,): (E,)| {
        if let Some(ref value) = elem.attrib("VALUE") {
            if let Some(&mut Property::List(ref mut v)) = props.borrow_mut().get_mut(key) {
                v[index] = value.to_string();
            }
        }
    });
}

macro_rules! bind_stat {
    ($i:ident, $p:expr, $e:expr) => {
        bind::<f32,_>(&mut from_name::<Text>(stringify!($i)), $p.clone(), $e);
    }
}

macro_rules! bind_skill {
    ($i:ident, $p:expr, $n:expr) => {
        bind_list::<_>(&mut from_handle::<Text>($i), $p.clone(), "SkillPoints", $n);
    }
}

// Data-bind all elements relevant to a party member.
//
// @param props {PropertyMapRc} a cloned refcounted property map.
//
fn bind_member(props: PropertyMapRc) {
    bind_stat!(text_int, props, "Int");
    bind_stat!(text_dex, props, "Dex");
    bind_stat!(text_str, props, "Str");
    bind_stat!(text_occ, props, "Occ");
    bind_stat!(text_per, props, "Per");

    bind_stat!(text_wpn_sword,  props, "WpnSword");
    bind_stat!(text_wpn_short,  props, "WpnShortSword");
    bind_stat!(text_wpn_blunt,  props, "WpnSceptor");
    bind_stat!(text_wpn_cleave, props, "WpnAxe");
    bind_stat!(text_wpn_whip,   props, "WpnWhip");
    bind_stat!(text_wpn_bow,    props, "WpnBow");
    bind_stat!(text_wpn_xbow,   props, "WpnXbow");
    bind_stat!(text_wpn_elixir, props, "WpnElixir");

    if let Some(&mut Property::List(ref mut v)) = props.borrow_mut().get_mut("SkillPoints") {
        while v.len() < 115 {
            v.push("0".to_string())
        }
    }
    if let Some(apt_grid) = Handle::from_named("apt_grid") {
        for i in 1..7 {
            if let Some(child) = apt_grid.child((i - 1) * 2 + 1) {
                bind_skill!(child, props, i);
            }
        }
    }
    if let Some(skill_grid) = Handle::from_named("skill_grid") {
        for i in 7..115 {
            if let Some(child) = skill_grid.child((i - 7) * 2 + 1) {
                bind_skill!(child, props, i);
            }
        }
    }
}

/// Ui entry point.
///
/// Starts by showing a direction selection dialog; after the user selects a directory,
/// the game is loaded from that directory and values bound to the ui elements.
///
pub fn ui_loop() -> Result<(), String> {
    match iup::with_iup(|| {
        // See also led::load(path) to load from a file
        led::load_buffer(DIALOG).unwrap();

        // Select saved game location
        let mut dlg_open = from_name::<FileDlg>("dlg_open");
        let dir = match dlg_open.popup(DialogPos::CenterParent, DialogPos::CenterParent) {
            Ok(..) => match dlg_open.attrib("STATUS") {
                Some(ref s) if s == "0" => {
                    dlg_open.attrib("VALUE").unwrap()
                },
                _ => return Err("File selection cancelled.".to_string())
            },
            _ => return Err("File selection failed.".to_string())
        };

        // Read game and party member files
        let game = Rc::new(RefCell::new({
            let path = Path::new(&dir).join("Game.txt");
            match read_path(path.as_path()) {
                Ok(v) => v,
                Err(e) => {
                    return Err(format!("Cannot read {:?}: {}", path, e))
                }
            }
        }));
        let party = Rc::new(RefCell::new({
            let mut members: Vec<PropertyMapRc> = Vec::new();
            if let Some(&Property::String(ref ids)) = game.borrow().get("PartyIDs") {
                for id in ids.split(",") {
                    let path = Path::new(&dir).join("Party".to_string() + id + ".txt");
                    match read_path(path.as_path()) {
                        Ok(v) => members.push(Rc::new(RefCell::new(v))),
                        Err(e) => {
                            return Err(format!("Cannot read {:?}: {}", path, e))
                        }
                    };
                }
            }
            members
        }));

        let mut text_emeralds = from_name::<Text>("text_emeralds");
        bind::<u32,_>(&mut text_emeralds, game.clone(), "Emeralds");

        let mut list_party = from_name::<List>("list_party");
        let mut list_party_items: Vec<String> = Vec::new();
        for member in party.borrow().iter().skip(1) {
            if let Some(&Property::String(ref name)) = member.borrow().get("Name") {
                if let Some(&Property::Float(level)) = member.borrow().get("Level") {
                    list_party_items.push(format!("{} ({})", name, level));
                }
            }
        }

        let skills = load_skills();
        if let Some(mut apt_grid) = Handle::from_named("apt_grid") {
            while let Some(mut child) = apt_grid.child(0) {
                child.detach().destroy();
            }
            for i in 1..7 {
                let mut label = Label::new()
                    .set_attrib("SIZE", "92x12".to_string())
                    .set_attrib("TITLE", "(empty)".to_string());
                let mut text = Text::new_spin()
                    .set_attrib("SIZE", "36x12".to_string())
                    .set_attrib("SPINMAX", "99".to_string())
                    .set_attrib("MASKINT", "0:99".to_string())
                    .set_attrib("ALIGNMENT", "ARIGHT".to_string());
                if let Some(ref skill) = skills.get(&i) {
                    label.set_attrib("TITLE", skill.name.to_string());
                } else {
                    label.set_attrib("ACTIVE", "NO");
                    text.set_attrib("ACTIVE", "NO");
                }
                apt_grid.append(label).unwrap();
                apt_grid.append(text).unwrap();
            }
        }
        if let Some(mut skill_grid) = Handle::from_named("skill_grid") {
            while let Some(mut child) = skill_grid.child(0) {
                child.detach().destroy();
            }
            for i in 7..115 {
                let mut label = Label::new()
                    .set_attrib("SIZE", "92x12".to_string())
                    .set_attrib("TITLE", "(empty)".to_string());
                let mut text = Text::new_spin()
                    .set_attrib("SIZE", "36x12".to_string())
                    .set_attrib("SPINMAX", "99".to_string())
                    .set_attrib("MASKINT", "0:99".to_string())
                    .set_attrib("ALIGNMENT", "ARIGHT".to_string());
                if let Some(ref skill) = skills.get(&i) {
                    label.set_attrib("TITLE", skill.name.to_string());
                } else {
                    label.set_attrib("ACTIVE", "NO");
                    text.set_attrib("ACTIVE", "NO");
                }
                skill_grid.append(label).unwrap();
                skill_grid.append(text).unwrap();
            }
        }

        let party_clone = party.clone();
        list_party.set_items(list_party_items);
        list_party.set_action(move |(_, _, i, _)| {
            let member = party_clone.borrow()[i as usize].clone();
            bind_member(member);
        });
        bind_member(party.borrow()[1].clone());

        // Write game and party member files on save
        let mut button_save = from_name::<Button>("button_save");
        {
            let game_clone = game.clone();
            let party_clone = party.clone();
            button_save.set_action(move |_| {
                let timestamp = time::strftime("%FT%H.%M.%SZ.txt", &time::now_utc()).unwrap();
                let path = Path::new(&dir).join("Game.txt");
                copy(path.as_path(), path.with_extension(&timestamp)).unwrap();
                write_path(path.as_path(), &game_clone.borrow()).unwrap();
                for member in party_clone.borrow().iter().skip(1) {

                    // Validate
                    let mut combat_skills: Vec<String> = Vec::new();
                    let mut spell_skills: Vec<String> = Vec::new();
                    if let Some(&Property::List(ref v)) = member.borrow().get("SkillPoints") {
                        let spell_grade = {
                            if let Some(&Property::Float(int_mod)) = member.borrow().get("Int") {
                                if let Some(&Property::Float(occ_mod)) = member.borrow().get("Occ") {
                                    int_mod + occ_mod + 20f32
                                } else {
                                    0f32
                                }
                            } else {
                                0f32
                            }
                        };
                        for (i, ref val) in v.iter().enumerate() {
                            match val.parse::<u32>() {
                                // Combat skills (1-3)
                                Ok(n) if i >= 7 && i <= 61 && n > 0 => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        combat_skills.push(skill.internal.to_string())
                                    }
                                }
                                // Spell skills (1)
                                Ok(n) if i >= 62 && i <= 72 && (n > 0 || spell_grade >= 21f32) => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        spell_skills.push(skill.internal.to_string())
                                    }
                                }
                                // Spell skills (2)
                                Ok(n) if i >= 73 && i <= 86 && (n > 0 || spell_grade >= 26f32) => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        spell_skills.push(skill.internal.to_string())
                                    }
                                }
                                // Spell skills (3)
                                Ok(n) if i >= 87 && i <= 114 && (n > 0 || spell_grade >= 32f32) => {
                                    if let Some(ref skill) = skills.get(&i) {
                                        spell_skills.push(skill.internal.to_string())
                                    }
                                }
                                _ => {}
                            }
                        }
                    }
                    if let Some(&mut Property::List(ref mut v)) = member.borrow_mut().get_mut("CombatSelects") {
                        v.retain(|ref x| combat_skills.contains(&x));
                    }
                    if let Some(&mut Property::List(ref mut v)) = member.borrow_mut().get_mut("SpellFavorites") {
                        v.retain(|ref x| spell_skills.contains(&x));
                    }
                    member.borrow_mut().insert("CombatSkills".to_string(), Property::List(combat_skills));
                    member.borrow_mut().insert("SpellSkills".to_string(), Property::List(spell_skills));

                    if let Some(&Property::String(ref id)) = member.borrow().get("PartyID") {
                        let path = Path::new(&dir).join("Party".to_string() + id + ".txt");
                        copy(path.as_path(), path.with_extension(&timestamp)).unwrap();
                        write_path(path.as_path(), &member.borrow()).unwrap();
                    }
                }
            });
        }
        let mut button_close = from_name::<Button>("button_close");
        button_close.set_action(|_| {
            CallbackReturn::Close
        });

        let mut dlg = from_name::<Dialog>("dlg");
        dlg.show()

    }) {
        Err(iup::InitError::UserError(s)) => Err(s),
        Err(e) => Err(format!("{:?}", e)),
        _ => Ok(())
    }
}

struct Skill {
    name: String,
    internal: String,
}

fn load_skills() -> HashMap<usize, Skill> {
    let mut skills: HashMap<usize, Skill> = HashMap::with_capacity(115);
    if let Ok(elem) = include_str!("../resources/skills.xml").parse::<xml::Element>() {
        for child in elem.get_children("skill", None) {
            let name = child.get_children("name", None).nth(0).map(|ref e| e.content_str());
            let desc = child.get_children("description", None).nth(0).map(|ref e| e.content_str());
            match name {
                Some(ref name) if !name.is_empty() => {
                    match desc {
                        Some(ref desc) if !desc.is_empty() => {
                            let id = child.attributes
                                .get(&("number".to_string(), None)).unwrap()
                                .parse::<usize>()
                                .unwrap();
                            let internal = child.attributes
                                .get(&("spritename".to_string(), None)).unwrap_or(name);
                            skills.insert(id, Skill {
                                name: name.to_owned(),
                                internal: internal.to_owned(),
                            });
                        }
                        _ => {}
                    }
                }
                _ => {}
            }
        }
    }
    skills
}
<|endoftext|>


<commit_before>use std::cell::RefCell;
use std::str::FromStr;
use std::rc::Rc;

use iup;
use iup::prelude::*;
use iup::control::{Frame, List, Text};
use iup::led;

use prop::{Property, PropertyMap, PropertyMapRef};

// LED dialog specification.
static DIALOG: &'static str = r#"
    text_emeralds = text[SPIN=YES, SPINMAX=99999, MASKINT=0:99999, ALIGNMENT=ARIGHT](_)
    text_int = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_dex = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_str = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_occ = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_per = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)

    list_party = list[DROPDOWN=YES, VALUE=1, VISIBLE_ITEMS=6](_)

    dlg = dialog(
        vbox[CGAP=2, CMARGIN=4x2, ALIGNMENT=ARIGHT](
            gridbox[NUMDIV=2, SIZECOL=1, CGAPCOL=4, CGAPLIN=2, NORMALIZESIZE=HORIZONTAL](
                label("Emeralds"), text_emeralds,
                label("Party"), list_party
            ),
            member = frame[TITLE=""](
                gridbox[NUMDIV=2, SIZECOL=1, CGAPCOL=4, CGAPLIN=2, NORMALIZESIZE=HORIZONTAL](
                    label("Intelligence (+10)"), text_int,
                    label("Dexterity (+10)"),    text_dex,
                    label("Strength (+10)"),     text_str,
                    label("Occult (+10)"),       text_occ,
                    label("Perception (+10)"),   text_per
                )
            )
        )
    )
"#;

// Get an element from named handle (defined in the LED input)
fn from_name<E>(name: &str) -> E where E: Element {
    E::from_handle(E::from_name(name).unwrap()).unwrap()
}

// Data-bind an element to a numeric property value.
//
// Value of the element is set to the current value of the property, and
// changes to element value are written back to the property map.
//
// @param props {PropertyMapRef} a cloned refcounted property map.
//
fn bind<T, E>(elem: &mut E, props: PropertyMapRef, key: &'static str)
    where Property: From<T>,
          T: FromStr + Default,
          E: Element + ValueChangedCb {

    // Remove previous bindings, if any.
    elem.remove_valuechanged_cb();

    if let Some(ref prop) = props.borrow().get(key) {
        elem.set_attrib("VALUE", prop.to_string());
    }
    elem.set_valuechanged_cb(move |(elem,): (E,)| {
        if let Some(ref value) = elem.attrib("VALUE") {
            if let Ok(v) = T::from_str(value) {
                props.borrow_mut().insert(key.to_string(), Property::from(v));
            } else {
                props.borrow_mut().insert(key.to_string(), Property::from(T::default()));
            }
        }
    });
}

// Data-bind all elements relevant to a party member.
//
// @param props {PropertyMapRef} a cloned refcounted property map.
//
fn bind_member(props: PropertyMapRef) {
    if let Some(&Property::String(ref name)) = props.borrow().get("Name") {
        if let Some(&Property::Float(level)) = props.borrow().get("Level") {
            let title = format!("{} (Level {})", name, level);
            from_name::<Frame>("member").set_attrib("TITLE", title);
        }
    }
    let mut text_int = from_name::<Text>("text_int");
    bind::<f32,_>(&mut text_int, props.clone(), "Int");

    let mut text_dex = from_name::<Text>("text_dex");
    bind::<f32,_>(&mut text_dex, props.clone(), "Dex");

    let mut text_str = from_name::<Text>("text_str");
    bind::<f32,_>(&mut text_str, props.clone(), "Str");

    let mut text_occ = from_name::<Text>("text_occ");
    bind::<f32,_>(&mut text_occ, props.clone(), "Occ");

    let mut text_per = from_name::<Text>("text_per");
    bind::<f32,_>(&mut text_per, props.clone(), "Per");
}

pub fn run_ui_loop(game: PropertyMapRef, party: Rc<RefCell<Vec<PropertyMapRef>>>) {
    iup::with_iup(|| {
        // See also led::load(path) to load from a file
        led::load_buffer(DIALOG).unwrap();

        let mut text_emeralds = from_name::<Text>("text_emeralds");
        bind::<u32,_>(&mut text_emeralds, game, "Emeralds");

        let mut list_party = from_name::<List>("list_party");
        let mut list_party_items: Vec<String> = Vec::new();
        for member in party.borrow().iter().skip(1) {
            if let Some(&Property::String(ref v)) = member.borrow().get("Name") {
                list_party_items.push(v.to_string());
            }
        }
        let party_clone = party.clone();
        list_party.set_items(list_party_items);
        list_party.set_action(move |(_, _, i, _)| {
            let member = party_clone.borrow()[i as usize].clone();
            bind_member(member);
        });
        bind_member(party.borrow()[1].clone());

        let mut dlg = from_name::<Dialog>("dlg");
        dlg.show()
    }).unwrap();
}
<commit_msg>Macro!<commit_after>use std::cell::RefCell;
use std::str::FromStr;
use std::rc::Rc;

use iup;
use iup::prelude::*;
use iup::control::{Frame, List, Text};
use iup::led;

use prop::{Property, PropertyMap, PropertyMapRef};

// LED dialog specification.
static DIALOG: &'static str = r#"
    text_emeralds = text[SPIN=YES, SPINMAX=99999, MASKINT=0:99999, ALIGNMENT=ARIGHT](_)
    text_int = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_dex = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_str = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_occ = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)
    text_per = text[SPIN=YES, SPINMIN=-9, SPINMAX=99, MASKINT=-9:99, ALIGNMENT=ARIGHT](_)

    list_party = list[DROPDOWN=YES, VALUE=1, VISIBLE_ITEMS=6](_)

    dlg = dialog(
        vbox[CGAP=2, CMARGIN=4x2, ALIGNMENT=ARIGHT](
            gridbox[NUMDIV=2, SIZECOL=1, CGAPCOL=4, CGAPLIN=2, NORMALIZESIZE=HORIZONTAL](
                label("Emeralds"), text_emeralds,
                label("Party"), list_party
            ),
            member = frame[TITLE=""](
                gridbox[NUMDIV=2, SIZECOL=1, CGAPCOL=4, CGAPLIN=2, NORMALIZESIZE=HORIZONTAL](
                    label("Intelligence (+10)"), text_int,
                    label("Dexterity (+10)"),    text_dex,
                    label("Strength (+10)"),     text_str,
                    label("Occult (+10)"),       text_occ,
                    label("Perception (+10)"),   text_per
                )
            )
        )
    )
"#;

// Get an element from named handle (defined in the LED input)
fn from_name<E>(name: &str) -> E where E: Element {
    E::from_handle(E::from_name(name).unwrap()).unwrap()
}

// Data-bind an element to a numeric property value.
//
// Value of the element is set to the current value of the property, and
// changes to element value are written back to the property map.
//
// @param props {PropertyMapRef} a cloned refcounted property map.
//
fn bind<T, E>(elem: &mut E, props: PropertyMapRef, key: &'static str)
    where Property: From<T>,
          T: FromStr + Default,
          E: Element + ValueChangedCb {

    // Remove previous bindings, if any.
    elem.remove_valuechanged_cb();

    if let Some(ref prop) = props.borrow().get(key) {
        elem.set_attrib("VALUE", prop.to_string());
    }
    elem.set_valuechanged_cb(move |(elem,): (E,)| {
        if let Some(ref value) = elem.attrib("VALUE") {
            if let Ok(v) = T::from_str(value) {
                props.borrow_mut().insert(key.to_string(), Property::from(v));
            } else {
                props.borrow_mut().insert(key.to_string(), Property::from(T::default()));
            }
        }
    });
}

macro_rules! bind_stat {
    ($i:ident, $p:expr, $e:expr) => {
        bind::<f32,_>(&mut from_name::<Text>(stringify!($i)), $p.clone(), $e);
    }
}

// Data-bind all elements relevant to a party member.
//
// @param props {PropertyMapRef} a cloned refcounted property map.
//
fn bind_member(props: PropertyMapRef) {
    if let Some(&Property::String(ref name)) = props.borrow().get("Name") {
        if let Some(&Property::Float(level)) = props.borrow().get("Level") {
            let title = format!("{} (Level {})", name, level);
            from_name::<Frame>("member").set_attrib("TITLE", title);
        }
    }
    bind_stat!(text_int, props, "Int");
    bind_stat!(text_dex, props, "Dex");
    bind_stat!(text_str, props, "Str");
    bind_stat!(text_occ, props, "Occ");
    bind_stat!(text_per, props, "Per");
}

pub fn run_ui_loop(game: PropertyMapRef, party: Rc<RefCell<Vec<PropertyMapRef>>>) {
    iup::with_iup(|| {
        // See also led::load(path) to load from a file
        led::load_buffer(DIALOG).unwrap();

        let mut text_emeralds = from_name::<Text>("text_emeralds");
        bind::<u32,_>(&mut text_emeralds, game, "Emeralds");

        let mut list_party = from_name::<List>("list_party");
        let mut list_party_items: Vec<String> = Vec::new();
        for member in party.borrow().iter().skip(1) {
            if let Some(&Property::String(ref v)) = member.borrow().get("Name") {
                list_party_items.push(v.to_string());
            }
        }
        let party_clone = party.clone();
        list_party.set_items(list_party_items);
        list_party.set_action(move |(_, _, i, _)| {
            let member = party_clone.borrow()[i as usize].clone();
            bind_member(member);
        });
        bind_member(party.borrow()[1].clone());

        let mut dlg = from_name::<Dialog>("dlg");
        dlg.show()
    }).unwrap();
}
<|endoftext|>


<commit_before>//! Type definitions for Stencila Cloud API types
//!
//! To avoid drift, prefer to only only add properties that are needed here to these structs.

use cli_utils::table::{date_time_ago, option_date_time_ago, option_string, Table};
use common::{
    chrono::{DateTime, Utc},
    inflector::Inflector,
    serde::{Deserialize, Serialize},
    serde_with::skip_serializing_none,
};

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct ApiToken {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Token")]
    pub token: String,

    #[table(title = "Tag", display_fn = "option_string")]
    pub tag: Option<String>,

    #[table(title = "Note", display_fn = "option_string")]
    pub note: Option<String>,

    #[table(title = "Expires", display_fn = "option_date_time_ago")]
    pub expires_at: Option<DateTime<Utc>>,

    #[table(title = "Last used", display_fn = "option_date_time_ago")]
    pub last_used_at: Option<DateTime<Utc>>,

    #[table(title = "Created", display_fn = "date_time_ago")]
    pub created_at: DateTime<Utc>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct LogEntry {
    #[table(title = "User", display_fn = "log_entry_user_table_display")]
    pub user: Option<OrgPersonal>,

    #[table(title = "Action", display_fn = "log_entry_action_type_table_display")]
    pub action_type: String,

    #[table(title = "Type", display_fn = "log_entry_subject_type_table_display")]
    pub subject_type: String,

    #[serde(default)]
    #[table(title = "Description")]
    pub description: String,

    #[table(title = "Time", display_fn = "date_time_ago")]
    pub created_at: DateTime<Utc>,
}

fn log_entry_user_table_display(user: &Option<OrgPersonal>) -> String {
    user.as_ref()
        .map(|user| user.short_name.clone())
        .unwrap_or_default()
}

fn log_entry_subject_type_table_display(subject: &str) -> String {
    subject.to_title_case()
}

fn log_entry_action_type_table_display(action: &str) -> String {
    match action {
        "created" => "✨ Created".to_string(),
        "updated" => "🔨 Updated".to_string(),
        "deleted" => "❌ Deleted".to_string(),
        "started" => "🚀 Started".to_string(),
        "stopped" => "🛑 Stopped".to_string(),
        _ => action.to_sentence_case(),
    }
}

/// The authenticated user
///
/// This is not used for `users find` or `users invite`, only for `user.json`.
/// It has the properties that we need to show to the current user.
#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
pub struct User {
    pub id: String,

    pub short_name: String,

    pub long_name: Option<String>,

    pub email: Option<String>,

    pub default_org: Option<u64>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct Org {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Short name")]
    pub short_name: String,

    #[table(title = "Long name", display_fn = "option_string")]
    pub long_name: Option<String>,

    #[table(title = "Role", display_fn = "option_org_role_table_display")]
    pub user_role: Option<String>,
}

fn option_org_role_table_display(role: &Option<String>) -> &str {
    role.as_deref()
        .map(project_role_table_display)
        .unwrap_or("*None*")
}

fn org_role_table_display(role: &str) -> &str {
    match role {
        "owner" => "🧰 Owner",
        "admin" => "🛠  Admin",
        "member" => "🔨 Member",
        _ => "?",
    }
}

/// A user's personal organization
///
/// Used when finding and inviting users. Differs from [`Org`] in that it displays
/// `user_id` (a string) rather than `id` (an integer)
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct OrgPersonal {
    #[table(skip)]
    pub id: u64,

    #[table(title = "ID")]
    pub user_id: String,

    #[table(title = "Short name")]
    pub short_name: String,

    #[table(title = "Long name", display_fn = "option_string")]
    pub long_name: Option<String>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct OrgMember {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "User", display_fn = "org_member_table_display")]
    pub user: User,

    #[table(title = "Role", display_fn = "org_role_table_display")]
    pub role: String,
}

fn org_member_table_display(user: &User) -> String {
    format!("👤 {} (#{})", user.short_name.as_str(), user.id)
}

#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct OrgUsedQuota {
    #[table(title = "Resource")]
    pub name: String,

    #[table(title = "Used")]
    pub used: f64,

    #[table(title = "Quota")]
    pub quota: f64,

    #[serde(default)]
    #[table(title = "Percent", display_fn = "org_used_percent_table_display")]
    pub percent: f64,
}

fn org_used_percent_table_display(percent: &f64) -> String {
    format!("{:.1}%", percent)
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct Team {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Name", display_fn = "option_string")]
    pub name: Option<String>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
pub struct ProjectLocal {
    pub id: Option<u64>,

    pub name: Option<String>,

    pub title: Option<String>,

    /// Whether the project is public or not
    ///
    /// Uses `public` rather than `isPublic` as used on Stencila Cloud.
    #[serde(alias = "isPublic")]
    pub public: Option<bool>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct ProjectRemote {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Organization", display_fn = "project_org_table_display")]
    pub org: Org,

    #[table(title = "Name")]
    pub name: String,

    #[table(title = "Title", display_fn = "project_title_table_display")]
    pub title: Option<String>,

    #[table(title = "Visibility", display_fn = "project_visibility_table_display")]
    pub is_public: bool,

    #[table(title = "Role", display_fn = "option_project_role_table_display")]
    pub user_role: Option<String>,

    #[table(title = "Updated", display_fn = "date_time_ago")]
    pub updated_at: DateTime<Utc>,

    #[table(skip, title = "Created", display_fn = "date_time_ago")]
    pub created_at: DateTime<Utc>,
}

fn project_org_table_display(org: &Org) -> String {
    format!("{} (#{})", org.short_name.as_str(), org.id)
}

fn project_title_table_display(title: &Option<String>) -> &str {
    title.as_deref().unwrap_or("*Untitled*")
}

fn project_visibility_table_display(is_public: &bool) -> &str {
    match is_public {
        true => "🌍",
        false => "🔒",
    }
}

fn option_project_role_table_display(role: &Option<String>) -> &str {
    role.as_deref()
        .map(project_role_table_display)
        .unwrap_or("*None*")
}

fn project_role_table_display(role: &str) -> &str {
    match role {
        "owner" => "🧰 Owner",
        "admin" => "🛠  Admin",
        "member" => "🔨 Member",
        _ => "?",
    }
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct ProjectMember {
    #[table(title = "ID")]
    pub id: u64,

    #[table(skip)]
    pub user: Option<User>,

    #[table(skip)]
    pub team: Option<Team>,

    #[serde(skip)]
    #[table(title = "User or team")]
    pub desc: String,

    #[table(title = "Role", display_fn = "project_role_table_display")]
    pub role: String,
}

impl ProjectMember {
    pub fn generate_desc(&mut self) -> String {
        if let Some(user) = &self.user {
            format!("👤 {} (#{})", user.short_name.as_str(), user.id)
        } else if let Some(team) = &self.team {
            let name = team.name.clone().unwrap_or_else(|| "*Unnamed*".to_string());
            format!("👥 {} (#{})", name, team.id)
        } else {
            "-".to_string()
        }
    }
}
<commit_msg>fix(Orgs): Add fields available in `orgs show` command<commit_after>//! Type definitions for Stencila Cloud API types
//!
//! To avoid drift, prefer to only only add properties that are needed here to these structs.

use cli_utils::table::{date_time_ago, option_date_time_ago, option_string, Table};
use common::{
    chrono::{DateTime, Utc},
    inflector::Inflector,
    serde::{Deserialize, Serialize},
    serde_with::skip_serializing_none,
};

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct ApiToken {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Token")]
    pub token: String,

    #[table(title = "Tag", display_fn = "option_string")]
    pub tag: Option<String>,

    #[table(title = "Note", display_fn = "option_string")]
    pub note: Option<String>,

    #[table(title = "Expires", display_fn = "option_date_time_ago")]
    pub expires_at: Option<DateTime<Utc>>,

    #[table(title = "Last used", display_fn = "option_date_time_ago")]
    pub last_used_at: Option<DateTime<Utc>>,

    #[table(title = "Created", display_fn = "date_time_ago")]
    pub created_at: DateTime<Utc>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct LogEntry {
    #[table(title = "User", display_fn = "log_entry_user_table_display")]
    pub user: Option<OrgPersonal>,

    #[table(title = "Action", display_fn = "log_entry_action_type_table_display")]
    pub action_type: String,

    #[table(title = "Type", display_fn = "log_entry_subject_type_table_display")]
    pub subject_type: String,

    #[serde(default)]
    #[table(title = "Description")]
    pub description: String,

    #[table(title = "Time", display_fn = "date_time_ago")]
    pub created_at: DateTime<Utc>,
}

fn log_entry_user_table_display(user: &Option<OrgPersonal>) -> String {
    user.as_ref()
        .map(|user| user.short_name.clone())
        .unwrap_or_default()
}

fn log_entry_subject_type_table_display(subject: &str) -> String {
    subject.to_title_case()
}

fn log_entry_action_type_table_display(action: &str) -> String {
    match action {
        "created" => "✨ Created".to_string(),
        "updated" => "🔨 Updated".to_string(),
        "deleted" => "❌ Deleted".to_string(),
        "started" => "🚀 Started".to_string(),
        "stopped" => "🛑 Stopped".to_string(),
        _ => action.to_sentence_case(),
    }
}

/// The authenticated user
///
/// This is not used for `users find` or `users invite`, only for `user.json`.
/// It has the properties that we need to show to the current user.
#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
pub struct User {
    pub id: String,

    pub short_name: String,

    pub long_name: Option<String>,

    pub email: Option<String>,

    pub default_org: Option<u64>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct Org {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Short name")]
    pub short_name: String,

    #[table(title = "Long name", display_fn = "option_string")]
    pub long_name: Option<String>,

    #[table(title = "Role", display_fn = "option_org_role_table_display")]
    pub user_role: Option<String>,

    // The following fields are only available for orgs that
    // a user is a member of and only for `show` command (not `list`)
    #[table(skip)]
    pub created_by: Option<String>,

    #[table(skip)]
    pub created_at: Option<DateTime<Utc>>,

    #[table(skip)]
    pub updated_at: Option<DateTime<Utc>>,

    #[table(skip)]
    pub is_public: Option<bool>,

    #[table(skip)]
    pub plan_id: Option<u64>,

    #[table(skip)]
    pub extra_compute_time: Option<bool>,

    #[table(skip)]
    pub extra_network_egress: Option<bool>,
}

fn option_org_role_table_display(role: &Option<String>) -> &str {
    role.as_deref()
        .map(project_role_table_display)
        .unwrap_or("*None*")
}

fn org_role_table_display(role: &str) -> &str {
    match role {
        "owner" => "🧰 Owner",
        "admin" => "🛠  Admin",
        "member" => "🔨 Member",
        _ => "?",
    }
}

/// A user's personal organization
///
/// Used when finding and inviting users. Differs from [`Org`] in that it displays
/// `user_id` (a string) rather than `id` (an integer)
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct OrgPersonal {
    #[table(skip)]
    pub id: u64,

    #[table(title = "ID")]
    pub user_id: String,

    #[table(title = "Short name")]
    pub short_name: String,

    #[table(title = "Long name", display_fn = "option_string")]
    pub long_name: Option<String>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct OrgMember {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "User", display_fn = "org_member_table_display")]
    pub user: User,

    #[table(title = "Role", display_fn = "org_role_table_display")]
    pub role: String,
}

fn org_member_table_display(user: &User) -> String {
    format!("👤 {} (#{})", user.short_name.as_str(), user.id)
}

#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct OrgUsedQuota {
    #[table(title = "Resource")]
    pub name: String,

    #[table(title = "Used")]
    pub used: f64,

    #[table(title = "Quota")]
    pub quota: f64,

    #[serde(default)]
    #[table(title = "Percent", display_fn = "org_used_percent_table_display")]
    pub percent: f64,
}

fn org_used_percent_table_display(percent: &f64) -> String {
    format!("{:.1}%", percent)
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct Team {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Name", display_fn = "option_string")]
    pub name: Option<String>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
pub struct ProjectLocal {
    pub id: Option<u64>,

    pub name: Option<String>,

    pub title: Option<String>,

    /// Whether the project is public or not
    ///
    /// Uses `public` rather than `isPublic` as used on Stencila Cloud.
    #[serde(alias = "isPublic")]
    pub public: Option<bool>,
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct ProjectRemote {
    #[table(title = "ID")]
    pub id: u64,

    #[table(title = "Organization", display_fn = "project_org_table_display")]
    pub org: Org,

    #[table(title = "Name")]
    pub name: String,

    #[table(title = "Title", display_fn = "project_title_table_display")]
    pub title: Option<String>,

    #[table(title = "Visibility", display_fn = "project_visibility_table_display")]
    pub is_public: bool,

    #[table(title = "Role", display_fn = "option_project_role_table_display")]
    pub user_role: Option<String>,

    #[table(title = "Updated", display_fn = "date_time_ago")]
    pub updated_at: DateTime<Utc>,

    #[table(skip, title = "Created", display_fn = "date_time_ago")]
    pub created_at: DateTime<Utc>,
}

fn project_org_table_display(org: &Org) -> String {
    format!("{} (#{})", org.short_name.as_str(), org.id)
}

fn project_title_table_display(title: &Option<String>) -> &str {
    title.as_deref().unwrap_or("*Untitled*")
}

fn project_visibility_table_display(is_public: &bool) -> &str {
    match is_public {
        true => "🌍",
        false => "🔒",
    }
}

fn option_project_role_table_display(role: &Option<String>) -> &str {
    role.as_deref()
        .map(project_role_table_display)
        .unwrap_or("*None*")
}

fn project_role_table_display(role: &str) -> &str {
    match role {
        "owner" => "🧰 Owner",
        "admin" => "🛠  Admin",
        "member" => "🔨 Member",
        _ => "?",
    }
}

#[skip_serializing_none]
#[derive(Serialize, Deserialize, Table)]
#[serde(rename_all = "camelCase", crate = "common::serde")]
#[table(crate = "cli_utils::cli_table")]
pub struct ProjectMember {
    #[table(title = "ID")]
    pub id: u64,

    #[table(skip)]
    pub user: Option<User>,

    #[table(skip)]
    pub team: Option<Team>,

    #[serde(skip)]
    #[table(title = "User or team")]
    pub desc: String,

    #[table(title = "Role", display_fn = "project_role_table_display")]
    pub role: String,
}

impl ProjectMember {
    pub fn generate_desc(&mut self) -> String {
        if let Some(user) = &self.user {
            format!("👤 {} (#{})", user.short_name.as_str(), user.id)
        } else if let Some(team) = &self.team {
            let name = team.name.clone().unwrap_or_else(|| "*Unnamed*".to_string());
            format!("👥 {} (#{})", name, team.id)
        } else {
            "-".to_string()
        }
    }
}
<|endoftext|>