mirage-engine 0.2.0

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
//! What a build's pulls did not get, for the one startup error.

use core::fmt;
use std::collections::BTreeSet;

use crate::Error;
use crate::mesh::MeshError;
use crate::skybox::SkyboxError;

/// The mark that separates a source's file name from an item name it
/// shares with another source.
pub(crate) const QUALIFIER: char = '#';

/// Everything one build's pulls did not get, which the value that build
/// returned carries until a catalog run takes it.
///
/// One line per thing, however many builds pulled it.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct Unresolved(BTreeSet<Missing>);

impl Unresolved {
    /// The record of one thing a build did not get.
    pub(crate) fn of(missing: Missing) -> Self {
        Self(BTreeSet::from([missing]))
    }

    /// Records everything `other` holds beside what this holds.
    pub(crate) fn record(&mut self, other: Self) {
        self.0.extend(other.0);
    }

    /// This record, taken out of the value holding it, which is left with
    /// none.
    pub(crate) fn taken(&mut self) -> Self {
        Self(core::mem::take(&mut self.0))
    }

    /// Whether nothing is recorded: every pull resolved.
    pub(crate) fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Logs every line of it at debug level: what a build past the catalog
    /// run reports, since the startup error is already written by then.
    pub(crate) fn logged(&self) {
        for missing in &self.0 {
            log::debug!("{missing}");
        }
    }

    /// The one startup error naming everything recorded, or nothing where
    /// every pull resolved.
    pub(crate) fn error(&self) -> Option<Error> {
        (!self.is_empty()).then(|| {
            let mut lines: Vec<String> = self.0.iter().map(Missing::to_string).collect();
            lines.sort();

            Error::msg(format!(
                "the game's assets did not resolve: {}",
                lines.join("; ")
            ))
        })
    }
}

/// One thing a build did not get.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum Missing {
    /// No source holds an item of that name and kind.
    Absent { name: String },
    /// More than one source holds an item of that name, which is read
    /// qualified.
    Ambiguous { name: String, sources: Vec<String> },
    /// No material of the asset resolves to the part.
    PartUnnamed { asset: String, part: String },
    /// Two materials of the asset resolve to the one part.
    PartTwice {
        asset: String,
        part: String,
        first: String,
        second: String,
    },
    /// No animation of the asset resolves to the clip.
    ClipUnnamed { asset: String, clip: String },
    /// Two animations of the asset resolve to the one clip.
    ClipTwice {
        asset: String,
        clip: String,
        first: String,
        second: String,
    },
    /// The animation the clip resolves to moves no joint of the asset.
    ClipStill {
        asset: String,
        clip: String,
        animation: String,
    },
    /// The animation the clip resolves to moves one node of the asset along
    /// two curves at once.
    ClipRepeated {
        asset: String,
        clip: String,
        animation: String,
        node: String,
    },
    /// A mesh was built with an error in it.
    Mesh {
        mesh: &'static str,
        error: MeshError,
    },
    /// A skybox was built over an image that is no sky at all.
    Skybox { skybox: String, error: SkyboxError },
}

impl Missing {
    /// A name nothing resolves to.
    pub(crate) fn absent(name: &str) -> Self {
        Self::Absent {
            name: name.to_owned(),
        }
    }

    /// What `name` reads as under the source `by`.
    pub(crate) fn qualified(by: &str, name: &str) -> String {
        format!("{by}{QUALIFIER}{name}")
    }
}

impl fmt::Display for Missing {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Absent { name } => write!(f, "no asset is named `{name}`"),
            Self::Ambiguous { name, sources } => {
                let alternatives: Vec<String> = sources
                    .iter()
                    .map(|source| format!("`{}`", Self::qualified(source, name)))
                    .collect();
                write!(
                    f,
                    "several sources call something `{name}`; ask for {}",
                    alternatives.join(" or ")
                )
            }
            Self::PartUnnamed { asset, part } => write!(
                f,
                "the asset `{asset}` has no material that resolves to the part {part}"
            ),
            Self::PartTwice {
                asset,
                part,
                first,
                second,
            } => write!(
                f,
                "the asset `{asset}` has two materials that resolve to the part {part}: \
                 `{first}` and `{second}`"
            ),
            Self::ClipUnnamed { asset, clip } => write!(
                f,
                "the asset `{asset}` has no animation that resolves to the clip {clip}"
            ),
            Self::ClipTwice {
                asset,
                clip,
                first,
                second,
            } => write!(
                f,
                "the asset `{asset}` has two animations that resolve to the clip {clip}: \
                 `{first}` and `{second}`"
            ),
            Self::ClipStill {
                asset,
                clip,
                animation,
            } => write!(
                f,
                "the asset `{asset}` has the animation `{animation}` for the clip {clip}, which \
                 moves no joint of it"
            ),
            Self::ClipRepeated {
                asset,
                clip,
                animation,
                node,
            } => write!(
                f,
                "the asset `{asset}` has the animation `{animation}` for the clip {clip}, which \
                 moves the node `{node}` along two curves at once"
            ),
            Self::Mesh { mesh, error } => write!(f, "the mesh `{mesh}` {error}"),
            Self::Skybox { skybox, error } => write!(f, "the skybox `{skybox}` {error}"),
        }
    }
}