proxisim/src/log.rs

459 lines
13 KiB
Rust

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<Option<DynamicLogMessage>>);
impl From<DynamicLogMessage> 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<LogValue>),
Map(Vec<(&'static str, LogValue)>),
}
impl From<String> 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<f32> for LogValue {
fn from(value: f32) -> Self {
Self::Float(value)
}
}
impl From<u32> for LogValue {
fn from(value: u32) -> Self {
Self::Unsigned(value)
}
}
impl From<u16> for LogValue {
fn from(value: u16) -> Self {
Self::Unsigned(value.into())
}
}
impl From<i16> for LogValue {
fn from(value: i16) -> Self {
Self::Signed(value.into())
}
}
impl From<bool> for LogValue {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl From<Entity> for LogValue {
fn from(value: Entity) -> Self {
Self::Entity(value)
}
}
impl<T> From<Option<T>> for LogValue
where
T: Into<LogValue>,
{
fn from(value: Option<T>) -> Self {
match value {
Some(val) => val.into(),
None => LogValue::OptionNone,
}
}
}
impl<V> From<Vec<V>> for LogValue
where
V: Into<LogValue>,
{
fn from(value: Vec<V>) -> Self {
LogValue::Array(value.into_iter().map(Into::into).collect())
}
}
impl<V> From<Vec<(&'static str, V)>> for LogValue
where
V: Into<LogValue>,
{
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<V> From<Result<V, QuerySingleError>> for LogValue
where
V: Into<LogValue>,
{
fn from(value: Result<V, QuerySingleError>) -> 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<DynamicLogMessage>,
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::<Vec<_>>()
})
}
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<B>(&mut self, body: B)
where
B: FnOnce() -> DynamicLogMessage,
{
if self.logging.0 {
self.event_writer.write(body().into());
}
}
}
fn logging_enabled(logging: Res<Logging>) -> bool {
logging.0
}
fn append_log_messages(mut events: MessageReader<LogEvent>, mut log: ResMut<Log>) {
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: StatMarker>(
stat_q: Query<
(Entity, &BaselineStat<Stat>, &EffectiveStat<Stat>, &Children),
Changed<EffectiveStat<Stat>>,
>,
add_q: Query<&AdditiveBonus<Stat>>,
mult_q: Query<&MultiplicativeBonus<Stat>>,
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: SimpleStatMarker>(
stat_q: Query<
(
Entity,
&SimpleStatBaseline<Stat>,
&SimpleStatEffective<Stat>,
&Children,
),
Changed<SimpleStatEffective<Stat>>,
>,
bonus_q: Query<&SimpleStatBonus<Stat>>,
mut logger: Logger,
) where
Stat::ValueType: Into<LogValue>,
Stat::BonusType: Into<LogValue>,
{
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::<Stat>(),
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::<LogEvent>();
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::<Strength>,
log_stat_changes::<Defence>,
log_stat_changes::<Speed>,
log_stat_changes::<Dexterity>,
log_simple_stat_changes::<CritRate>,
log_simple_stat_changes::<WeaponAccuracy>,
log_simple_stat_changes::<DamageBonus>,
log_simple_stat_changes::<Clips>,
log_simple_stat_changes::<ClipSize>,
log_simple_stat_changes::<ArmourBonusValue>,
)
.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"));
}
}