bevy_log_events 0.7.0

A Bevy plugin that helps to log events.
Documentation
use std::{
    any::type_name,
    collections::BTreeMap,
    error::Error,
    fmt::Write,
    fs::{File, create_dir_all},
    ops::DerefMut,
    path::{Path, PathBuf},
};

use bevy::{
    ecs::{change_detection::MaybeLocation, component::ComponentId},
    log::Level,
    prelude::*,
};

use ron::{de::from_reader, ser::PrettyConfig};

use crate::{
    EventSettings, LogEventsPlugin, LogEventsPluginSettings, LogMessagesSystems,
    LoggedEventSettings,
    utils::{LoggedEventsSettings, get_log_settings_by_id, trigger_name},
};

const CRATE: &str = "bevy_log_events";

impl Plugin for LogEventsPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(LogEventsPluginSettings::new(self))
            .insert_resource(LogSettingsIds::default())
            .configure_sets(Last, LogMessagesSystems.run_if(plugin_enabled))
            .add_systems(PostUpdate, save_settings.run_if(on_message::<AppExit>))
            .add_plugins(crate::settings_window::plugin);
    }
}

impl LogEventsPluginSettings {
    fn new(log_plugin: &LogEventsPlugin) -> Self {
        let path = &log_plugin.settings_path;
        match Self::load_saved_settings(path) {
            Ok(new) => new,
            Err(err) => {
                warn!(target: CRATE, "Error while trying to load settings from {:?}: {}. Using default settings instead.", path, err);
                LogEventsPluginSettings::default(path)
            }
        }
    }

    fn default(path: &Path) -> Self {
        Self {
            enabled: true,
            show_window: false,
            saved_settings: path.to_path_buf(),
            previous_settings: BTreeMap::new(),
        }
    }

    fn load_saved_settings(path: &PathBuf) -> Result<Self, Box<dyn Error>> {
        let file = File::open(path)?;
        let saved_settings: LoggedEventsSettings = from_reader(file)?;
        let new = Self {
            enabled: saved_settings.plugin_enabled,
            show_window: false,
            saved_settings: path.to_path_buf(),
            previous_settings: saved_settings.events_settings,
        };
        Ok(new)
    }
}

fn plugin_enabled(plugin_settings: Res<LogEventsPluginSettings>) -> bool {
    plugin_settings.enabled
}

#[derive(Resource, Default, Deref, DerefMut)]
pub(crate) struct LogSettingsIds(BTreeMap<String, ComponentId>);

impl LogSettingsIds {
    fn registered(&self, name: &str) -> bool {
        self.get(name).is_some()
    }

    fn register(&mut self, name: String, id: ComponentId) {
        self.insert(name, id);
    }

    pub(crate) fn iter_ids(&self) -> impl Iterator<Item = (&String, &ComponentId)> {
        self.iter()
    }
}

pub(crate) fn register_event<T>(world: &mut World, name: String) -> bool
where
    T: Resource + Default + DerefMut<Target = EventSettings>,
{
    world.resource_scope(|world, mut log_settings_ids: Mut<LogSettingsIds>| {
        if log_settings_ids.registered(&name) {
            false
        } else {
            world.insert_resource(T::default());
            world.resource_scope(|world, plugin_settings: Mut<LogEventsPluginSettings>| {
                if let Some(previous) = plugin_settings.previous_settings.get(&name) {
                    let mut event_settings = world.resource_mut::<T>();
                    **event_settings = *previous;
                }
            });
            let id = world.components().resource_id::<T>().unwrap();
            log_settings_ids.register(name, id);
            true
        }
    })
}

fn log(level: Level, to_log: &str) {
    match level {
        Level::ERROR => error!(target: CRATE, "{to_log}"),
        Level::WARN => warn!(target: CRATE, "{to_log}"),
        Level::INFO => info!(target: CRATE, "{to_log}"),
        Level::DEBUG => debug!(target: CRATE, "{to_log}"),
        Level::TRACE => trace!(target: CRATE, "{to_log}"),
    }
}

fn format_event<E>(
    settings: &EventSettings,
    event: &E,
    location: MaybeLocation,
) -> Result<String, Box<dyn Error>>
where
    E: std::fmt::Debug,
{
    let name = type_name::<E>();
    let mut to_log = String::new();
    to_log.write_str(name)?;
    if let Some(location) = location.into_option() {
        to_log.write_fmt(format_args!(" at {location}"))?;
    }
    to_log.write_str(": ")?;
    if settings.pretty {
        to_log.write_fmt(format_args!("{event:#?}"))?;
    } else {
        to_log.write_fmt(format_args!("{event:?}"))?;
    }
    Ok(to_log)
}

fn format_entity_and_component<C>(
    settings: &EventSettings,
    event_name: &str,
    entity_name: &Option<&Name>,
    entity: Entity,
    component: &C,
    location: MaybeLocation,
) -> Result<String, Box<dyn Error>>
where
    C: std::fmt::Debug,
{
    let mut to_log = String::new();
    to_log.write_fmt(format_args!("{event_name} on "))?;
    if let Some(name) = entity_name {
        to_log.write_fmt(format_args!("{name}({entity})"))?;
    } else {
        to_log.write_fmt(format_args!("{entity}"))?;
    }
    if let Some(location) = location.into_option() {
        to_log.write_fmt(format_args!(" at {location}"))?;
    }
    to_log.write_str(": ")?;
    if settings.pretty {
        to_log.write_fmt(format_args!("{component:#?}"))?;
    } else {
        to_log.write_fmt(format_args!("{component:?}"))?;
    }
    Ok(to_log)
}

pub(crate) fn log_message<M>(settings: Res<LoggedEventSettings<M>>, mut messages: MessageReader<M>)
where
    M: Message + std::fmt::Debug,
{
    if !settings.enabled {
        return;
    }
    for (message, id) in messages.read_with_id() {
        if let Ok(to_log) = format_event(&settings, message, id.caller) {
            log(settings.level, &to_log);
        }
    }
}

pub(crate) fn log_event<E>(
    event: On<E>,
    plugin_settings: Res<LogEventsPluginSettings>,
    settings: Res<LoggedEventSettings<E>>,
) where
    E: Event + std::fmt::Debug,
{
    if !plugin_settings.enabled || !settings.enabled {
        return;
    }
    if let Ok(to_log) = format_event(&settings, event.event(), event.caller()) {
        log(settings.level, &to_log);
    }
}

pub(crate) fn log_component<E, C>(
    event: On<E, C>,
    plugin_settings: Res<LogEventsPluginSettings>,
    settings: Res<LoggedEventSettings<E, C>>,
    query: Query<(&C, Option<&Name>)>,
) where
    E: EntityEvent,
    C: Component + std::fmt::Debug,
{
    if !plugin_settings.enabled || !settings.enabled {
        return;
    }
    let target = event.event_target();
    if let Ok((component, name)) = query.get(target)
        && let Ok(to_log) = format_entity_and_component::<C>(
            &settings,
            &trigger_name::<E, C>(),
            &name,
            target,
            component,
            event.caller(),
        )
    {
        log(settings.level, &to_log);
    }
}

fn serialize_settings(
    path: &PathBuf,
    to_serialize: LoggedEventsSettings,
) -> Result<(), Box<dyn Error>> {
    if let Some(parent) = path.parent() {
        create_dir_all(parent)?;
    }
    let mut file = File::create(path)?;
    let config = PrettyConfig::default().struct_names(true);
    let serialized = ron::ser::to_string_pretty(&to_serialize, config)?;
    std::io::Write::write_all(&mut file, serialized.as_bytes())?;
    Ok(())
}

fn save_settings(world: &mut World) {
    let log_settings_ids = world.resource::<LogSettingsIds>();
    let mut all_settings = BTreeMap::new();
    for (name, id) in log_settings_ids.iter_ids() {
        let event_settings = get_log_settings_by_id(world, id);
        all_settings.insert(name.clone(), *event_settings);
    }
    let plugin_settings = world.resource::<LogEventsPluginSettings>();
    let to_serialize = LoggedEventsSettings {
        plugin_enabled: plugin_settings.enabled,
        events_settings: all_settings,
    };
    let path = plugin_settings.saved_settings.clone();
    if let Err(e) = serialize_settings(&path, to_serialize) {
        error!(
            target: CRATE,
            "Could not save {} at {:?} due to {:?}",
            type_name::<LoggedEventsSettings>(),
            path,
            e
        );
    }
}