planetkit 0.0.1

High-level toolkit for building games based around voxel globes.
Documentation
use std::collections::HashMap;

use specs;

use grid::{GridPoint3, PosInOwningRoot, Neighbors};
use super::{origin_of_chunk_owning, origin_of_chunk_in_same_root_containing};
use super::ChunkOrigin;
use super::chunk::{Chunk, Cell};
use super::spec::Spec;
use super::gen::Gen;
use super::chunk_pair::{ChunkPairOrigins, ChunkPair};

// TODO: split out a WorldGen type that handles all the procedural
// generation, because none of that really needs to be tangled
// with the realised Globe.
pub struct Globe {
    spec: Spec,
    // TODO: temporarily making this public because I'm planning to
    // rip it out of `Globe` anyway.
    pub gen: Gen,
    // Map chunk origins to chunks.
    //
    // TODO: you'll probably also want to store some lower-res
    // pseudo-chunks for rendering planets at a distance.
    // But maybe you can put that off? Or maybe that's an entirely
    // different type of Globe-oid?
    chunks: HashMap<ChunkOrigin, Chunk>,
    // Track which chunks are up-to-date with authoritative data for cells
    // they share with a neighbor.
    chunk_pairs: HashMap<ChunkPairOrigins, ChunkPair>,
}

// Allowing sibling modules to reach into semi-private parts
// of the Globe struct.
pub trait GlobeGuts<'a> {
    fn chunks(&'a self) -> &'a HashMap<ChunkOrigin, Chunk>;
    fn chunks_mut(&'a mut self) -> &'a mut HashMap<ChunkOrigin, Chunk>;
}

impl<'a> GlobeGuts<'a> for Globe {
    fn chunks(&'a self) -> &'a HashMap<ChunkOrigin, Chunk> {
        &self.chunks
    }

    fn chunks_mut(&'a mut self) -> &'a mut HashMap<ChunkOrigin, Chunk> {
        &mut self.chunks
    }
}

impl Globe {
    pub fn new(spec: Spec) -> Globe {
        Globe {
            spec: spec,
            gen: Gen::new(spec),
            chunks: HashMap::new(),
            chunk_pairs: HashMap::new(),
        }
    }

    pub fn new_example() -> Globe {
        Globe::new(Spec {
            seed: 14,
            floor_radius: 25.0,
            ocean_radius: 66.6,
            block_height: 0.65,
            root_resolution: [64, 128],
            // Chunks should probably be taller, but short chunks are a bit
            // better for now in exposing bugs visually.
            chunk_resolution: [16, 16, 4],
        })
    }

    pub fn new_earth_scale_example() -> Globe {
        Globe::new(Spec::new_earth_scale_example())
    }

    pub fn spec(&self) -> Spec {
        self.spec
    }

    /// Copy shared cells owned by a chunk for any loaded downstream chunks
    /// that have an outdated copy.
    ///
    /// Panics if the given chunk is not loaded.
    pub fn push_shared_cells_for_chunk(&mut self, source_chunk_origin: ChunkOrigin) {
        // BEWARE: HACKS BELOW to get around borrowck. There has to be
        // a better way around this!

        // Temporarily remove the source chunk from the globe, so that we can simultaneously
        // read from it and write to a bunch of other chunks.
        //
        // TODO: at very least avoid this most of the time by doing a read-only pass over
        // all neighbours and bailing out if they're completely up-to-date.
        let source_chunk = self.chunks.remove(&source_chunk_origin).expect(
            "Tried to push shared cells for a chunk that isn't loaded.",
        );

        // For each of this chunk's downstream neighbors, see if it has up-to-date
        // copies of the data we share with that neighbor. Otherwise, copy it over.
        for downstream_neighbor in &source_chunk.downstream_neighbors {
            let sink_chunk = match self.chunks.get_mut(&downstream_neighbor.origin) {
                None => {
                    // No worries; the chunk isn't loaded. Do nothing.
                    continue;
                }
                Some(chunk) => chunk,
            };

            // Look it up to see if it is already up-to-date.
            let chunk_pair_origins = ChunkPairOrigins {
                source: source_chunk.origin,
                sink: sink_chunk.origin,
            };
            let chunk_pair = self.chunk_pairs.get_mut(&chunk_pair_origins).expect(
                "Chunk pair for chunk should have been present.",
            );
            if chunk_pair.last_upstream_edge_version_known_downstream ==
                source_chunk.owned_edge_version
            {
                // Sink chunk is already up-to-date; move on to next neighbor.
                continue;
            }

            // Copy over each cell, one-by-one.
            for point_pair in &chunk_pair.point_pairs {
                let source_cell = *source_chunk.cell(point_pair.source.into());
                let target_cell = sink_chunk.cell_mut(point_pair.sink);
                // Copy source -> target.
                *target_cell = source_cell;
            }

            // Downstream chunk now has most recent changes from upstream.
            chunk_pair.last_upstream_edge_version_known_downstream = source_chunk
                .owned_edge_version;

            // If we got this far, then it means we needed to update something.
            // So mark the downstream chunk as having its view out-of-date.
            sink_chunk.mark_view_as_dirty();
        }

        // Put the source chunk back into the world!
        self.chunks.insert(source_chunk_origin, source_chunk);
    }

    /// Copy shared cells not owned by a chunk from any loaded upstream chunks
    /// that have a more current copy.
    ///
    /// Panics if the given chunk is not loaded.
    pub fn pull_shared_cells_for_chunk(&mut self, sink_chunk_origin: ChunkOrigin) {
        // BEWARE: HACKS BELOW to get around borrowck. There has to be
        // a better way around this!

        // Temporarily remove the sink chunk from the globe, so that we can simultaneously
        // write to it and read from a bunch of other chunks.
        //
        // TODO: at very least avoid this most of the time by doing a read-only pass over
        // all neighbours and bailing out if we're completely up-to-date.
        let mut sink_chunk = self.chunks.remove(&sink_chunk_origin).expect(
            "Tried to pull shared cells for a chunk that isn't loaded.",
        );

        // Temporarily remove list of neighbours from sink chunk so that we can
        // both read from it and update the chunk's data.
        let mut upstream_neighbors = Vec::<super::chunk::UpstreamNeighbor>::new();
        use std::mem::swap;
        swap(&mut upstream_neighbors, &mut sink_chunk.upstream_neighbors);

        // For each of this chunk's upstream neighbors, see if it has a newer copy of the data
        // we share with that neighbor. Otherwise, copy it into the sink chunk.
        for upstream_neighbor in &upstream_neighbors {
            let source_chunk = match self.chunks.get_mut(&upstream_neighbor.origin) {
                None => {
                    // No worries; the chunk isn't loaded. Do nothing.
                    continue;
                }
                Some(chunk) => chunk,
            };

            // Look it up to see if it has any newer data.
            let chunk_pair_origins = ChunkPairOrigins {
                source: source_chunk.origin,
                sink: sink_chunk.origin,
            };
            let chunk_pair = self.chunk_pairs.get_mut(&chunk_pair_origins).expect(
                "Chunk pair for chunk should have been present.",
            );
            if chunk_pair.last_upstream_edge_version_known_downstream ==
                source_chunk.owned_edge_version
            {
                // Sink chunk is already up-to-date; move on to next neighbor.
                continue;
            }

            // Copy over each cell, one-by-one.
            for point_pair in &chunk_pair.point_pairs {
                let source_cell = *source_chunk.cell(point_pair.source.into());
                let target_cell = sink_chunk.cell_mut(point_pair.sink);
                // Copy source -> target.
                *target_cell = source_cell;
            }

            // Downstream chunk now has most recent changes from upstream.
            chunk_pair.last_upstream_edge_version_known_downstream = source_chunk
                .owned_edge_version;

            // If we got this far, then it means we needed to update something.
            // So mark the downstream chunk as having its view out-of-date.
            sink_chunk.mark_view_as_dirty();
        }

        // Put the list of neighbors back into the sink chunk.
        swap(&mut upstream_neighbors, &mut sink_chunk.upstream_neighbors);

        // Put the sink chunk back into the world!
        self.chunks.insert(sink_chunk_origin, sink_chunk);
    }

    pub fn origin_of_chunk_owning(&self, pos: PosInOwningRoot) -> ChunkOrigin {
        origin_of_chunk_owning(pos, self.spec.root_resolution, self.spec.chunk_resolution)
    }

    // NOTE: chunk returned probably won't _own_ `pos`.
    pub fn origin_of_chunk_in_same_root_containing(&self, pos: GridPoint3) -> ChunkOrigin {
        // Figure out what chunk this is in.
        origin_of_chunk_in_same_root_containing(
            pos,
            self.spec.root_resolution,
            self.spec.chunk_resolution,
        )
    }

    /// Most `Chunks`s will have an associated `ChunkView`. Indicate that the
    /// chunk (or something else affecting its visibility) has been modified
    /// since the view was last updated.
    pub fn mark_chunk_views_affected_by_cell_as_dirty(&mut self, pos: GridPoint3) {
        // Translate into owning root.
        // TODO: wrapper types so we don't have to do
        // this sort of thing defensively!
        // TODO: is it even necessary at all here? All we really care about
        // is consistency in which chunk we look at for the centre cell,
        // and the one.....
        // TODO: really, just rewrite this whole function. It doesn't really work.
        let pos_in_owning_root = PosInOwningRoot::new(pos, self.spec.root_resolution);

        // TODO: this (Vec) is super slow! Replace with a less brain-dead solution.
        // Just committing this one now to patch over a kinda-regression in that
        // the existing bug of not doing this at all just become a lot more
        // obvious now that I'm doing a better job of culling cells.
        let mut dirty_cells: Vec<PosInOwningRoot> = Vec::new();
        dirty_cells.push(pos_in_owning_root);
        dirty_cells.extend(
            Neighbors::new(pos_in_owning_root.into(), self.spec.root_resolution)
                .map(|neighbor_pos| {
                    PosInOwningRoot::new(neighbor_pos, self.spec.root_resolution)
                }),
        );
        // Gah, filthy hacks. This is to get around not having a way to query for
        // "all chunks containing this cell".
        //
        // TODO: replace now that you DO have that. See other comment about `AllChunksContainingPoint`.
        let mut cells_in_dirty_chunks: Vec<PosInOwningRoot> = Vec::new();
        for dirty_cell in dirty_cells {
            cells_in_dirty_chunks.extend(
                Neighbors::new(dirty_cell.into(), self.spec.root_resolution)
                    .map(|neighbor_pos| {
                        PosInOwningRoot::new(neighbor_pos, self.spec.root_resolution)
                    }),
            );
        }
        for dirty_pos in cells_in_dirty_chunks {
            let chunk_origin = self.origin_of_chunk_owning(dirty_pos);
            // It's fine for the chunk to not be loaded.
            if let Some(chunk) = self.chunks.get_mut(&chunk_origin) {
                chunk.mark_view_as_dirty();
            }
        }
    }

    pub fn increment_chunk_owned_edge_version_for_cell(&mut self, pos: PosInOwningRoot) {
        let chunk_origin = self.origin_of_chunk_owning(pos.into());
        let chunk = self.chunks.get_mut(&chunk_origin).expect(
            "Uh oh, I don't know how to handle chunks that aren't loaded yet.",
        );
        chunk.owned_edge_version += 1;
    }

    /// Add the given chunk to the globe.
    ///
    /// This may have been freshly generated, or loaded from disk.
    ///
    /// # Panics
    ///
    /// Panics if there was already a chunk loaded for the same chunk origin.
    pub fn add_chunk(&mut self, chunk: Chunk) {
        // TODO: You could assert that the chunk actually belongs to _this globe_.
        // It could store a UUID associated with its generator and complain
        // if you try to load a chunk for the wrong globe even if they have
        // the same resolution.

        self.ensure_all_chunk_pairs_present_for(&chunk);

        let chunk_origin = chunk.origin;
        if self.chunks.insert(chunk_origin, chunk).is_some() {
            panic!("There was already a chunk loaded at the same origin!");
        }
    }

    /// Remove the chunk at the given chunk origin. Returns the removed chunk.
    ///
    /// # Panics
    ///
    /// Panics if there was no chunk loaded at the given chunk origin.
    pub fn remove_chunk(&mut self, chunk_origin: ChunkOrigin) -> Chunk {
        let chunk = self.chunks.remove(&chunk_origin).expect(
            "Attempted to remove a chunk that was not loaded",
        );
        self.remove_all_chunk_pairs_for(&chunk);
        chunk
    }

    // TODO: consider moving `load_or_build_chunk`, `ensure_chunk_present`,
    // and `find_lowest_cell_containing` back out into a smarter component
    // so that `Globe` can be dumber, or move more of `Globe` down into a new
    // dumber component, e.g., `GlobeVoxMap`.

    pub fn load_or_build_chunk(&mut self, origin: ChunkOrigin) {
        use rand;
        use rand::Rng;

        let spec = self.spec();

        let mut cells: Vec<Cell> = Vec::new();
        // Include cells _on_ the far edge of the chunk;
        // even though we don't own them we'll need to draw part of them.
        let end_x = origin.pos().x + spec.chunk_resolution[0];
        let end_y = origin.pos().y + spec.chunk_resolution[1];
        // Chunks don't share cells in the z-direction,
        // but do in the x- and y-directions.
        let end_z = origin.pos().z + spec.chunk_resolution[2] - 1;
        for cell_z in origin.pos().z..(end_z + 1) {
            for cell_y in origin.pos().y..(end_y + 1) {
                for cell_x in origin.pos().x..(end_x + 1) {
                    let grid_point = GridPoint3::new(origin.pos().root, cell_x, cell_y, cell_z);
                    let mut cell = self.gen.cell_at(grid_point);
                    // Temp hax?
                    let mut rng = rand::thread_rng();
                    cell.shade = 1.0 - 0.5 * rng.next_f32();
                    cells.push(cell);
                }
            }
        }
        self.add_chunk(Chunk::new(
            origin,
            cells,
            spec.root_resolution,
            spec.chunk_resolution,
        ));
    }

    /// Ensures the specified chunk is present.
    ///
    /// If the chunk is already present, then do nothing. Otherwise, the chunk
    /// may be either loaded from disk, or generated fresh if it has never been
    /// saved.
    ///
    /// This pays no regard to preferred limits on the number of chunks that should
    /// be loaded, and chunks added through this mechanism may well be unloaded
    /// immediately the next time this system is invoked, making this only suitable
    /// for immediate actions.
    //
    // TODO: this definitely belongs elsewhere; somewhere that knows about
    // loading things from disk. The simpler version of just making sure the
    // voxmap buffer exists should exist on a dumber struct extracted from `Globe`.
    pub fn ensure_chunk_present(&mut self, chunk_origin: ChunkOrigin) {
        if self.chunk_at(chunk_origin).is_some() {
            return;
        }
        self.load_or_build_chunk(chunk_origin);

        // Make sure this chunk has up-to-date data for edge cells that it doesn't own.
        self.pull_shared_cells_for_chunk(chunk_origin);

        // Make sure that neighboring chunks have up-to-date data for edge cells owned
        // by this chunk.
        self.push_shared_cells_for_chunk(chunk_origin);
    }

    /// Make sure we are tracking the currency of shared data in all chunks
    /// upstream or downstream of this chunk.
    fn ensure_all_chunk_pairs_present_for(&mut self, chunk: &Chunk) {
        use globe::chunk_pair::{ChunkPairOrigins, ChunkPair};
        // Copy cached upstream and downstream neighbors from
        // chunks rather than re-computing them for each pair now.
        for upstream_neighbor in &chunk.upstream_neighbors {
            let chunk_pair_origins = ChunkPairOrigins {
                source: upstream_neighbor.origin,
                sink: chunk.origin,
            };
            self.chunk_pairs.entry(chunk_pair_origins).or_insert(
                ChunkPair {
                    point_pairs: upstream_neighbor.shared_cells.clone(),
                    last_upstream_edge_version_known_downstream: 0,
                },
            );
        }
        for downstream_neighbor in &chunk.downstream_neighbors {
            let chunk_pair_origins = ChunkPairOrigins {
                source: chunk.origin,
                sink: downstream_neighbor.origin,
            };
            self.chunk_pairs.entry(chunk_pair_origins).or_insert(
                ChunkPair {
                    point_pairs: downstream_neighbor.shared_cells.clone(),
                    last_upstream_edge_version_known_downstream: 0,
                },
            );
        }
    }

    /// Clean up any chunk pairs where either the source or sink is this chunk.
    fn remove_all_chunk_pairs_for(&mut self, chunk: &Chunk) {
        // TODO: HashMap::retain was stabilised in Rust 1.18;
        // replace this as soon as you update.
        for upstream_neighbor in &chunk.upstream_neighbors {
            let chunk_pair_origins = ChunkPairOrigins {
                source: upstream_neighbor.origin,
                sink: chunk.origin,
            };
            // Note that chunk pair will only be present if _both_ chunks it refers to were loaded.
            self.chunk_pairs.remove(&chunk_pair_origins);
        }
        for downstream_neighbor in &chunk.downstream_neighbors {
            let chunk_pair_origins = ChunkPairOrigins {
                source: chunk.origin,
                sink: downstream_neighbor.origin,
            };
            // Note that chunk pair will only be present if _both_ chunks it refers to were loaded.
            self.chunk_pairs.remove(&chunk_pair_origins);
        }
    }
}

impl<'a> Globe {
    pub fn chunk_at(&'a self, chunk_origin: ChunkOrigin) -> Option<&'a Chunk> {
        self.chunks.get(&chunk_origin)
    }

    // TODO: this naming is horrible. :)

    pub fn authoritative_cell(&'a self, pos: PosInOwningRoot) -> &'a Cell {
        let chunk_origin = self.origin_of_chunk_owning(pos);
        let chunk = self.chunks.get(&chunk_origin).expect(
            "Uh oh, I don't know how to handle chunks that aren't loaded yet.",
        );
        chunk.cell(pos.into())
    }

    pub fn authoritative_cell_mut(&'a mut self, pos: PosInOwningRoot) -> &'a mut Cell {
        let chunk_origin = self.origin_of_chunk_owning(pos);
        let chunk = self.chunks.get_mut(&chunk_origin).expect(
            "Uh oh, I don't know how to handle chunks that aren't loaded yet.",
        );
        chunk.cell_mut(pos.into())
    }

    pub fn maybe_non_authoritative_cell(&'a self, pos: GridPoint3) -> &'a Cell {
        let chunk_origin = self.origin_of_chunk_in_same_root_containing(pos);
        let chunk = self.chunks.get(&chunk_origin).expect(
            "Uh oh, I don't know how to handle chunks that aren't loaded yet.",
        );
        chunk.cell(pos)
    }
}

impl specs::Component for Globe {
    type Storage = specs::HashMapStorage<Globe>;
}