use std::sync::Mutex; use bevy_ecs::{prelude::*, query::QuerySingleError, system::SystemParam}; use proxisim_models::bundle::{ stat::{ AdditiveBonus, ArmourBonusValue, BaselineStat, ClipSize, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy, }, weapon::WeaponVerb, }; use crate::{Stages, entity_registry::EntityRegistry}; #[derive(Resource)] pub struct Logging(pub bool); #[derive(Message)] struct LogEvent(Mutex>); impl From for LogEvent { fn from(value: DynamicLogMessage) -> Self { LogEvent(Mutex::new(Some(value))) } } #[derive(Debug)] pub struct PlayerInfo { pub name: String, } #[derive(Debug)] pub struct WeaponInfo { pub name: String, pub verb: WeaponVerb, } #[derive(Clone)] pub enum LogValue { Float(f32), Unsigned(u32), Signed(i32), Bool(bool), String(String), Entity(Entity), OptionNone, Player(Entity), Weapon(Entity), Array(Vec), Map(Vec<(&'static str, LogValue)>), } impl From for LogValue { fn from(value: String) -> Self { Self::String(value) } } impl<'a> From<&'a str> for LogValue { fn from(value: &'a str) -> Self { Self::String(value.to_owned()) } } impl From for LogValue { fn from(value: f32) -> Self { Self::Float(value) } } impl From for LogValue { fn from(value: u32) -> Self { Self::Unsigned(value) } } impl From for LogValue { fn from(value: u16) -> Self { Self::Unsigned(value.into()) } } impl From for LogValue { fn from(value: i16) -> Self { Self::Signed(value.into()) } } impl From for LogValue { fn from(value: bool) -> Self { Self::Bool(value) } } impl From for LogValue { fn from(value: Entity) -> Self { Self::Entity(value) } } impl From> for LogValue where T: Into, { fn from(value: Option) -> Self { match value { Some(val) => val.into(), None => LogValue::OptionNone, } } } impl From> for LogValue where V: Into, { fn from(value: Vec) -> Self { LogValue::Array(value.into_iter().map(Into::into).collect()) } } impl From> for LogValue where V: Into, { fn from(value: Vec<(&'static str, V)>) -> Self { LogValue::Array( value .into_iter() .map(|(k, v)| LogValue::Array(vec![LogValue::String(k.to_string()), v.into()])) .collect(), ) } } impl From> for LogValue where V: Into, { fn from(value: Result) -> Self { match value { Ok(value) => value.into(), Err(_) => LogValue::String("No found".to_owned()), } } } #[cfg(feature = "json")] impl LogValue { fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value { match self { LogValue::OptionNone => serde_json::Value::Null, LogValue::Float(val) => { serde_json::Value::Number(serde_json::Number::from_f64(*val as f64).unwrap()) } LogValue::String(val) => serde_json::Value::String(val.clone()), LogValue::Bool(val) => serde_json::Value::Bool(*val), LogValue::Unsigned(val) => serde_json::Value::Number(serde_json::Number::from(*val)), LogValue::Signed(val) => serde_json::Value::Number(serde_json::Number::from(*val)), LogValue::Player(id) => { serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap() } LogValue::Weapon(id) => { serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap() } LogValue::Entity(id) => { serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap() } LogValue::Array(items) => serde_json::Value::Array( items.iter().map(|v| v.to_value(entity_registry)).collect(), ), LogValue::Map(map) => serde_json::Value::Object( map.iter() .map(|(k, v)| ((*k).to_owned(), v.to_value(entity_registry))) .collect(), ), } } } trait LogMessage: Send + Sync + 'static { fn tag(&self) -> &'static str; fn entries(&self) -> Vec<(&'static str, LogValue)>; } #[derive(Resource, Default)] pub struct Log { entries: Vec, pub expanded: bool, } impl Log { #[cfg(feature = "json")] pub fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value { use serde_json::json; serde_json::json!({ "entries": self.entries.iter().map(|e| json!({ "type": e.tag(), "values": serde_json::Value::Object( e.entries().iter().map(|e| (e.0.to_owned(), e.1.to_value(entity_registry))).collect() ) }) ).collect::>() }) } pub fn clear(&mut self) { self.entries.clear(); } } impl std::fmt::Display for Log { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut iter = self.entries.iter().peekable(); while let Some(entry) = iter.next() { write!(f, "{}: {{ ", entry.tag())?; let mut fields = entry.entries().into_iter().peekable(); while let Some(field) = fields.next() { if self.expanded { write!(f, "\n ")?; } write!(f, "{} = ", field.0)?; match field.1 { LogValue::String(val) => write!(f, "\"{val}\"")?, LogValue::Float(val) => write!(f, "{val}")?, LogValue::Bool(val) => write!(f, "{val}")?, LogValue::Unsigned(val) => write!(f, "{val}")?, LogValue::Signed(val) => write!(f, "{val}")?, LogValue::OptionNone => write!(f, "None")?, LogValue::Player(id) | LogValue::Weapon(id) | LogValue::Entity(id) => { write!(f, "{id}")? } LogValue::Array(_) | LogValue::Map(_) => (), }; if fields.peek().is_some() { write!(f, ", ")?; } } if self.expanded { writeln!(f)?; } else { write!(f, " ")?; } if iter.peek().is_some() { writeln!(f, "}}")?; } else { write!(f, "}}")?; } } Ok(()) } } #[derive(SystemParam)] pub struct Logger<'w> { event_writer: MessageWriter<'w, LogEvent>, logging: Res<'w, Logging>, } impl<'w> Logger<'w> { pub fn log(&mut self, body: B) where B: FnOnce() -> DynamicLogMessage, { if self.logging.0 { self.event_writer.write(body().into()); } } } fn logging_enabled(logging: Res) -> bool { logging.0 } fn append_log_messages(mut events: MessageReader, mut log: ResMut) { for event in events.read() { if let Some(entry) = event.0.lock().unwrap().take() { log.entries.push(entry); } } } pub struct DynamicLogMessage { pub label: &'static str, pub entries: Vec<(&'static str, LogValue)>, } impl LogMessage for DynamicLogMessage { fn tag(&self) -> &'static str { self.label } fn entries(&self) -> Vec<(&'static str, LogValue)> { self.entries.clone() } } #[macro_export] macro_rules! log { ($logger:expr, $tag:literal, { $($fields:tt)* }) => { $logger.log(|| $crate::log_message!($tag, { $($fields)* })) }; } #[macro_export] macro_rules! log_message { ($tag:literal, { $($fields:tt)* }) => { $crate::log::DynamicLogMessage { label: $tag, entries: $crate::log_values!($($fields)*) } }; } #[macro_export] macro_rules! log_values { (@ { $(,)* $($out:expr),* $(,)* } $(,)*) => { vec![$($out),*] }; (@ { $(,)* $($out:expr),* } $label:ident: $val:expr, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($label),$val.into()) } $($rest)*) }; (@ { $(,)* $($out:expr),* } $label:ident: ?$val:expr, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($label),$crate::log::LogValue::String(format!("{:?}",$val))) } $($rest)*) }; (@ { $(,)* $($out:expr),* } $label:ident: %$val:expr, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($label),$crate::log::LogValue::String(format!("{}",$val))) } $($rest)*) }; (@ { $(,)* $($out:expr),* } $value:ident, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($value),$value.into()) } $($rest)*) }; (@ { $(,)* $($out:expr),* } ?$value:ident, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($value),$crate::log::LogValue::String(format!("{:?}",$val))) } $($rest)*) }; (@ { $(,)* $($out:expr),* } %$value:ident, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($value),$crate::log::LogValue::String(format!("{}",$val))) } $($rest)*) }; ($($args:tt)* ) => { $crate::log_values!(@ { } $($args)*,) }; } fn log_stat_changes( stat_q: Query< (Entity, &BaselineStat, &EffectiveStat, &Children), Changed>, >, add_q: Query<&AdditiveBonus>, mult_q: Query<&MultiplicativeBonus>, mut logger: Logger, ) { for (player, baseline, effective, children) in stat_q.iter() { let effects_add: Vec<_> = add_q .iter_many(children) .map(|bonus| (bonus.label, 100.0 * bonus.value)) .collect(); let effects_mult: Vec<_> = mult_q .iter_many(children) .map(|bonus| (bonus.label, 100.0 * bonus.value)) .collect(); log!(logger, "stat_change", { target: player, stat: ?Stat::stat_type(), effects_add, effects_mult, baseline: baseline.value, effective: effective.value, }); } } fn log_simple_stat_changes( stat_q: Query< ( Entity, &SimpleStatBaseline, &SimpleStatEffective, &Children, ), Changed>, >, bonus_q: Query<&SimpleStatBonus>, mut logger: Logger, ) where Stat::ValueType: Into, Stat::BonusType: Into, { for (target, baseline, effective, children) in stat_q.iter() { let bonuses: Vec<_> = bonus_q .iter_many(children) .map(|bonus| (bonus.label, Stat::denormalise_bonus(bonus.value))) .collect(); log!(logger, "applied_bonus", { target, stat: std::any::type_name::(), baseline: Stat::denormalise_value(baseline.value), effective: Stat::denormalise_value(effective.value), bonuses }); } } pub(crate) fn configure(stages: &mut Stages) { stages.world.insert_resource(Log::default()); stages.world.insert_resource(Logging(false)); stages.add_event::(); stages .post_turn .add_systems(append_log_messages.run_if(logging_enabled)); stages .post_fight .add_systems(append_log_messages.run_if(logging_enabled)); stages.turn.add_systems( ( log_stat_changes::, log_stat_changes::, log_stat_changes::, log_stat_changes::, log_simple_stat_changes::, log_simple_stat_changes::, log_simple_stat_changes::, log_simple_stat_changes::, log_simple_stat_changes::, log_simple_stat_changes::, ) .run_if(logging_enabled), ); } #[cfg(test)] mod test { use super::*; #[test] fn log_keys() { let message = log_message!("test", { foo: 12u32, bar: ?17 }); assert_eq!(message.label, "test"); let mut entries = message.entries.into_iter(); let next = entries.next(); assert!(matches!(next, Some(("foo", LogValue::Unsigned(12))))); let next = entries.next(); assert!(matches!(next, Some(("bar", LogValue::String(val))) if val == "17")); } }