planetkit 0.0.1

High-level toolkit for building games based around voxel globes.
Documentation
use specs;
use specs::{ReadStorage, WriteStorage};
use specs::Entities;
use slog::Logger;

use grid::PosInOwningRoot;
use super::{Globe, ChunkOrigin};
use cell_dweller::CellDweller;

// NOTE: this is currently all pretty awful. See comments throughout.

/// Loads and unloads `Chunk`s for a `Globe`.
///
/// The `Chunk`s may be loaded from disk, or generated fresh if
/// they have never existed before.
pub struct ChunkSystem {
    log: Logger,
    // When we go higher than this many chunks loaded...
    max_chunks_loaded_per_globe: usize,
    // ...we will unload chunks to leave only this many.
    // This is a kind of hysteresis. I haven't yet validated
    // that this actually improves performance _at all_.
    //
    // TODO: instead have a budget for chunks, have the chunk
    // creation logic know what it is and _target_ it when creating
    // new chunks, so that we never end up with thrashing by
    // creating too many then immediately deleting them repeatedly
    // each frame. (You should only go over if you won't do it again
    // immediately after cleaning up.) Then also complain loudly
    // if there's not enough budget left for the really essential chunks
    // for a given CellDweller.
    cull_chunks_down_to: usize,
}

impl ChunkSystem {
    pub fn new(parent_log: &Logger) -> ChunkSystem {
        ChunkSystem {
            log: parent_log.new(o!()),
            // TODO: accept as arguments.
            //
            // There appears to be at least ~110
            // loaded at a minimum the way I have it at the moment;
            // have to be super careful to get these numbers right
            // so we don't unnecessarily churn chunks.
            //
            // TODO: how to make sure this is automatically right?
            // (See comments above; have a budget, work to that.)
            // These values were originally 200 and 150 before I bumped
            // it up to allow for 3 players. This solution really
            // isn't going to fly very long. It's time to decouple what
            // chunk _views_ exist from what chunks exist, so we can
            // load the essential chunks for each player character,
            // and the desirable views for each client. (Etc.)
            max_chunks_loaded_per_globe: 300,
            cull_chunks_down_to: 250,
        }
    }

    fn unload_excess_chunks_if_necessary<'a>(
        &mut self,
        globe: &mut Globe,
        globe_entity: specs::Entity,
        cds: &specs::ReadStorage<'a, CellDweller>,
    ) {
        use super::globe::GlobeGuts;

        if globe.chunks().len() <= self.max_chunks_loaded_per_globe {
            // We're under the limit; nothing to do.
            return;
        }

        // Get all the CellDweller positions relative to the globe.
        // We don't care which CellDweller is which, so just store
        // them as a Vec of points.
        use specs::Join;
        let cd_positions: Vec<_> = cds.join()
            // Only consider CellDwellers from this globe.
            .filter(|cd| cd.globe_entity == Some(globe_entity))
            .map(|cd| {
                cd.real_transform_without_setting_clean().translation.vector
            })
            .collect();

        // There are no cell dwellers, so no interesting terrain.
        // (If a tree falls in a forest...)
        if cd_positions.len() == 0 {
            return;
        }

        // Unload the chunks that are most distant from their nearest CellDweller.
        //
        // TODO: Don't allocate memory all the time here.
        // At very least use a persistent scratch buffer instead
        // of allocating every time!
        let mut chunk_distances: Vec<(ChunkOrigin, f64)> = globe
            .chunks()
            .keys()
            .map(|chunk_origin| {
                // TODO: don't use chunk origin; use the middle cell,
                // or otherwise whatever the closest corner is.
                // Or even a bounding sphere.
                // (Cache this per Chunk).
                let chunk_origin_pos = globe.spec().cell_bottom_center(*chunk_origin.pos());
                let distance_from_closest_cd = cd_positions.iter()
                    // TODO: norm_squared; it'll be quicker.
                    .map(|cd_pos| (cd_pos - chunk_origin_pos.coords).norm())
                    .min_by(|a, b| a.partial_cmp(b).expect("Really shouldn't be possible to get NaN etc. here"))
                    .expect("We already ensured there is at least one CellDweller");
                (*chunk_origin, distance_from_closest_cd)
            })
            .collect();
        // Farthest away chunks come first.
        chunk_distances.sort_by(|a, b| {
            b.1.partial_cmp(&a.1).expect(
                "All chunk origins and CellDwellers should be real distances from each other!",
            )
        });
        let chunks_to_remove = self.max_chunks_loaded_per_globe - self.cull_chunks_down_to;
        chunk_distances.truncate(chunks_to_remove);

        for (chunk_origin, _distance) in chunk_distances {
            globe.remove_chunk(chunk_origin);
        }
    }

    fn ensure_essential_chunks_for_cell_dweller_present<'a>(
        &mut self,
        cd: &CellDweller,
        globes: &mut specs::WriteStorage<'a, Globe>,
    ) {
        if let Some(globe_entity) = cd.globe_entity {
            // Get the associated globe, complaining loudly if we fail.
            // TODO: this is becoming a common pattern; factor out.
            let globe = match globes.get_mut(globe_entity) {
                Some(globe) => globe,
                None => {
                    warn!(
                        self.log,
                        "The globe associated with this CellDweller is not alive! Can't proceed!"
                    );
                    return;
                }
            };

            // TODO: throttle, and do in background.
            // (Except that the truly essential chunks really do need to be loaded _now_.)

            // TODO: see remarks in `Chunk::list_accessible_chunks`
            // about this actually being an inappropriate way to approach
            // this problem; we'll load a bunch of chunks we don't need to yet
            // in a desperate attempt to not miss the ones we do need.

            // Load all the chunks that we could possibly try
            // to move into from this chunk within two steps.
            //
            // Takes into account that a single user action could lead to
            // multiple cell jumps, e.g., stepping up a small ledge.
            //
            // TODO: this is all a bit finicky and fragile.

            // TODO: this is also just plain wrong.
            // You don't need the neighbouring chunks of the neighbouring chunks.
            // You just need all the chunks containing neighbouring cells of
            // neighbouring cells. No wonder there are so many chunks loaded
            // at the moment. :)
            let cd_pos_in_owning_root = PosInOwningRoot::new(cd.pos, globe.spec().root_resolution);
            let chunk_origin = globe.origin_of_chunk_owning(cd_pos_in_owning_root);
            globe.ensure_chunk_present(chunk_origin);
            let accessible_chunks = {
                use super::globe::GlobeGuts;
                let chunk = globe.chunks().get(&chunk_origin).expect(
                    "We just ensured this chunk is loaded.",
                );
                // TODO: Gah, such slow!
                chunk.accessible_chunks.clone()
            };
            for accessible_chunk_origin in accessible_chunks {
                globe.ensure_chunk_present(accessible_chunk_origin);

                // Repeat this from each immediately accessible chunk.
                let next_level_accessible_chunks = {
                    use super::globe::GlobeGuts;
                    let chunk = globe.chunks().get(&accessible_chunk_origin).expect(
                        "We just ensured this chunk is loaded.",
                    );
                    // TODO: Gah, such slow!
                    chunk.accessible_chunks.clone()
                };
                for next_level_accessible_chunk_origin in next_level_accessible_chunks {
                    globe.ensure_chunk_present(next_level_accessible_chunk_origin);
                }
            }
        }
    }
}

impl<'a> specs::System<'a> for ChunkSystem {
    type SystemData = (Entities<'a>, WriteStorage<'a, Globe>, ReadStorage<'a, CellDweller>);

    fn run(&mut self, data: Self::SystemData) {
        use specs::Join;

        let (entities, mut globes, cds) = data;

        // If we have too many chunks loaded, then unload some of them.
        for (mut globe, globe_entity) in (&mut globes, &*entities).join() {
            self.unload_excess_chunks_if_necessary(&mut globe, globe_entity, &cds);
        }
        // Make sure the chunks under/near the player are present.
        for cd in cds.join() {
            self.ensure_essential_chunks_for_cell_dweller_present(cd, &mut globes);
        }
    }
}