#![warn(missing_docs)]
#[cfg(feature = "editor_window")]
mod editor_window;
mod utils;
use std::{
any::type_name,
collections::BTreeMap,
error::Error,
fs::File,
io::Write,
marker::PhantomData,
path::{Path, PathBuf},
};
use bevy::{ecs::component::ComponentId, log::Level, prelude::*};
use ron::{de::from_reader, ser::PrettyConfig};
use serde::{Deserialize, Serialize};
use utils::{deserialize_level, get_log_settings_by_id, serialize_level, LoggedEventsSettings};
pub mod prelude {
pub use super::{
EventSettings, LogEvent, LogEventsPlugin, LogEventsPluginSettings, LogEventsSet,
LoggedEventSettings,
};
}
pub struct LogEventsPlugin {
pub settings_path: PathBuf,
}
impl LogEventsPlugin {
pub fn new(settings_path: impl Into<PathBuf>) -> Self {
Self {
settings_path: settings_path.into(),
}
}
}
impl Default for LogEventsPlugin {
fn default() -> Self {
Self {
settings_path: "assets/log_settings.ron".into(),
}
}
}
impl Plugin for LogEventsPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(LogEventsPluginSettings::new(self))
.insert_resource(LogSettingsIds::default())
.configure_sets(Last, LogEventsSet.run_if(plugin_enabled))
.add_systems(PostUpdate, save_settings.run_if(on_event::<AppExit>()));
#[cfg(feature = "editor_window")]
{
app.add_plugins(editor_window::plugin);
}
}
}
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LogEventsSet;
#[derive(Resource, Default, Deref, DerefMut)]
struct LogSettingsIds(BTreeMap<String, ComponentId>);
#[derive(Clone, Copy, Deserialize, Serialize)]
pub struct EventSettings {
pub enabled: bool,
pub pretty: bool,
#[serde(
serialize_with = "serialize_level",
deserialize_with = "deserialize_level"
)]
pub level: Level,
}
impl Default for EventSettings {
fn default() -> Self {
Self {
enabled: true,
level: Level::INFO,
pretty: true,
}
}
}
#[derive(Resource)]
pub struct LogEventsPluginSettings {
pub enabled: bool,
saved_settings: PathBuf,
previous_settings: BTreeMap<String, EventSettings>,
}
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!("Error while trying to load settings from {:?}: {}. Using default settings instead.", path, err);
LogEventsPluginSettings::default(path)
}
}
}
fn default(path: &Path) -> Self {
Self {
enabled: true,
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,
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, Deref, DerefMut)]
pub struct LoggedEventSettings<T: Event> {
#[deref]
pub settings: EventSettings,
_phantom: PhantomData<T>,
}
impl<T: Event> Default for LoggedEventSettings<T> {
fn default() -> Self {
Self {
settings: EventSettings::default(),
_phantom: PhantomData,
}
}
}
pub trait LogEvent {
fn log_event<T>(&mut self) -> &mut Self
where
T: Event + std::fmt::Debug;
fn add_and_log_event<T>(&mut self) -> &mut Self
where
T: Event + std::fmt::Debug;
}
impl LogEvent for App {
fn log_event<T>(&mut self) -> &mut Self
where
T: Event + std::fmt::Debug,
{
self.insert_resource(LoggedEventSettings::<T>::default())
.add_systems(Startup, register_event::<T>)
.add_systems(Last, log_event::<T>.in_set(LogEventsSet))
}
fn add_and_log_event<T>(&mut self) -> &mut Self
where
T: Event + std::fmt::Debug,
{
self.add_event::<T>().log_event::<T>()
}
}
fn register_event<T: Event>(world: &mut World) {
let name = type_name::<T>().to_string();
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::<LoggedEventSettings<T>>();
**event_settings = *previous;
}
});
world.resource_scope(|world, mut log_settings_ids: Mut<LogSettingsIds>| {
let id = world
.components()
.resource_id::<LoggedEventSettings<T>>()
.unwrap();
log_settings_ids.insert(name, id);
});
}
fn log_event<T>(settings: Res<LoggedEventSettings<T>>, mut events: EventReader<T>)
where
T: Event + std::fmt::Debug,
{
if !settings.enabled {
return;
}
for event in events.read() {
let to_log = if settings.pretty {
format!("{}: {:#?}", type_name::<T>(), event)
} else {
format!("{}: {:?}", type_name::<T>(), event)
};
match settings.level {
Level::ERROR => error!("{}", to_log),
Level::WARN => warn!("{}", to_log),
Level::INFO => info!("{}", to_log),
Level::DEBUG => debug!("{}", to_log),
Level::TRACE => trace!("{}", to_log),
}
}
}
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() {
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 Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let mut file = File::create(path).unwrap();
let serialized = ron::ser::to_string_pretty(
&to_serialize,
PrettyConfig::default()
.struct_names(true)
.separate_tuple_members(true),
)
.unwrap();
file.write_all(serialized.as_bytes()).unwrap();
}