459 lines
13 KiB
Rust
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"));
|
|
}
|
|
}
|